Skip to content

Commit 6943d74

Browse files
committed
feat: sign
1 parent baf31f7 commit 6943d74

File tree

7 files changed

+1040
-0
lines changed

7 files changed

+1040
-0
lines changed
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
import { Request, Response } from "express";
2+
import { CharterSigningService } from "../services/CharterSigningService";
3+
4+
export class CharterSigningController {
5+
private signingService: CharterSigningService | null = null;
6+
7+
constructor() {
8+
try {
9+
this.signingService = new CharterSigningService();
10+
console.log("CharterSigningController initialized successfully");
11+
} catch (error) {
12+
console.error("Failed to initialize CharterSigningService:", error);
13+
this.signingService = null;
14+
}
15+
}
16+
17+
private ensureService(): CharterSigningService {
18+
if (!this.signingService) {
19+
throw new Error("CharterSigningService not initialized");
20+
}
21+
return this.signingService;
22+
}
23+
24+
testConnection(): boolean {
25+
if (!this.signingService) {
26+
return false;
27+
}
28+
return this.signingService.testConnection();
29+
}
30+
31+
// Create a new signing session for a charter
32+
async createSigningSession(req: Request, res: Response) {
33+
try {
34+
const { groupId, charterData } = req.body;
35+
const userId = (req as any).user?.id;
36+
37+
if (!groupId || !charterData || !userId) {
38+
return res.status(400).json({
39+
error: "Missing required fields: groupId, charterData, or userId"
40+
});
41+
}
42+
43+
const session = await this.ensureService().createSession(groupId, charterData, userId);
44+
45+
res.json({
46+
sessionId: session.sessionId,
47+
qrData: session.qrData,
48+
expiresAt: session.expiresAt
49+
});
50+
} catch (error) {
51+
console.error("Error creating signing session:", error);
52+
res.status(500).json({ error: "Failed to create signing session" });
53+
}
54+
}
55+
56+
// Get signing session status via SSE
57+
async getSigningSessionStatus(req: Request, res: Response) {
58+
try {
59+
const { sessionId } = req.params;
60+
61+
if (!sessionId) {
62+
return res.status(400).json({ error: "Session ID is required" });
63+
}
64+
65+
// Set up SSE headers
66+
res.writeHead(200, {
67+
'Content-Type': 'text/event-stream',
68+
'Cache-Control': 'no-cache',
69+
'Connection': 'keep-alive',
70+
'Access-Control-Allow-Origin': '*',
71+
'Access-Control-Allow-Headers': 'Cache-Control'
72+
});
73+
74+
// Send initial status
75+
const session = await this.ensureService().getSessionStatus(sessionId);
76+
if (session) {
77+
res.write(`data: ${JSON.stringify({ type: "status", status: session.status })}\n\n`);
78+
}
79+
80+
// Poll for status changes
81+
const interval = setInterval(async () => {
82+
const session = await this.ensureService().getSessionStatus(sessionId);
83+
84+
if (session) {
85+
if (session.status === "completed") {
86+
res.write(`data: ${JSON.stringify({
87+
type: "signed",
88+
status: "completed",
89+
groupId: session.groupId
90+
})}\n\n`);
91+
clearInterval(interval);
92+
res.end();
93+
} else if (session.status === "expired") {
94+
res.write(`data: ${JSON.stringify({ type: "expired" })}\n\n`);
95+
clearInterval(interval);
96+
res.end();
97+
}
98+
} else {
99+
res.write(`data: ${JSON.stringify({ type: "error", message: "Session not found" })}\n\n`);
100+
clearInterval(interval);
101+
res.end();
102+
}
103+
}, 1000);
104+
105+
// Clean up on client disconnect
106+
req.on('close', () => {
107+
clearInterval(interval);
108+
res.end();
109+
});
110+
111+
} catch (error) {
112+
console.error("Error getting signing session status:", error);
113+
res.status(500).json({ error: "Failed to get signing session status" });
114+
}
115+
}
116+
117+
// Handle signed payload callback from eID Wallet
118+
async handleSignedPayload(req: Request, res: Response) {
119+
try {
120+
const { sessionId, signature, publicKey, message } = req.body;
121+
122+
// Validate required fields
123+
const missingFields = [];
124+
if (!sessionId) missingFields.push('sessionId');
125+
if (!signature) missingFields.push('signature');
126+
if (!publicKey) missingFields.push('publicKey');
127+
if (!message) missingFields.push('message');
128+
129+
if (missingFields.length > 0) {
130+
return res.status(400).json({
131+
error: `Missing required fields: ${missingFields.join(', ')}`
132+
});
133+
}
134+
135+
// Process the signed payload
136+
const result = await this.ensureService().processSignedPayload(
137+
sessionId,
138+
signature,
139+
publicKey,
140+
message
141+
);
142+
143+
res.json({
144+
success: true,
145+
message: "Signature verified and charter signed",
146+
data: result
147+
});
148+
149+
} catch (error) {
150+
console.error("Error processing signed payload:", error);
151+
res.status(500).json({ error: "Failed to process signed payload" });
152+
}
153+
}
154+
155+
// Get signing session by ID
156+
async getSigningSession(req: Request, res: Response) {
157+
try {
158+
const { sessionId } = req.params;
159+
160+
if (!sessionId) {
161+
return res.status(400).json({ error: "Session ID is required" });
162+
}
163+
164+
const session = await this.ensureService().getSession(sessionId);
165+
166+
if (!session) {
167+
return res.status(404).json({ error: "Session not found" });
168+
}
169+
170+
res.json(session);
171+
172+
} catch (error) {
173+
console.error("Error getting signing session:", error);
174+
res.status(500).json({ error: "Failed to get signing session" });
175+
}
176+
}
177+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import {
2+
Entity,
3+
CreateDateColumn,
4+
UpdateDateColumn,
5+
PrimaryGeneratedColumn,
6+
Column,
7+
ManyToOne,
8+
JoinColumn,
9+
} from "typeorm";
10+
import { Group } from "./Group";
11+
import { User } from "./User";
12+
13+
@Entity()
14+
export class CharterSignature {
15+
@PrimaryGeneratedColumn("uuid")
16+
id!: string;
17+
18+
@Column()
19+
groupId!: string;
20+
21+
@Column()
22+
userId!: string;
23+
24+
@Column({ type: "text" })
25+
charterHash!: string; // Hash of the charter content to track versions
26+
27+
@Column({ type: "text" })
28+
signature!: string; // Cryptographic signature
29+
30+
@Column({ type: "text" })
31+
publicKey!: string; // User's public key
32+
33+
@Column({ type: "text" })
34+
message!: string; // Original message that was signed
35+
36+
@ManyToOne(() => Group)
37+
@JoinColumn({ name: "groupId" })
38+
group!: Group;
39+
40+
@ManyToOne(() => User)
41+
@JoinColumn({ name: "userId" })
42+
user!: User;
43+
44+
@CreateDateColumn()
45+
createdAt!: Date;
46+
47+
@UpdateDateColumn()
48+
updatedAt!: Date;
49+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import { MigrationInterface, QueryRunner } from "typeorm";
2+
3+
export class Migration1755598750354 implements MigrationInterface {
4+
name = 'Migration1755598750354'
5+
6+
public async up(queryRunner: QueryRunner): Promise<void> {
7+
await queryRunner.query(`CREATE TABLE "charter_signature" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "groupId" uuid NOT NULL, "userId" uuid NOT NULL, "charterHash" text NOT NULL, "signature" text NOT NULL, "publicKey" text NOT NULL, "message" text NOT NULL, "createdAt" TIMESTAMP NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP NOT NULL DEFAULT now(), CONSTRAINT "PK_d566749a8805a43c54ad028deef" PRIMARY KEY ("id"))`);
8+
await queryRunner.query(`ALTER TABLE "charter_signature" ADD CONSTRAINT "FK_e1f768c9d467cd20b0f45321626" FOREIGN KEY ("groupId") REFERENCES "group"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
9+
await queryRunner.query(`ALTER TABLE "charter_signature" ADD CONSTRAINT "FK_fb0db27afde8d484139b66628fd" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE NO ACTION ON UPDATE NO ACTION`);
10+
}
11+
12+
public async down(queryRunner: QueryRunner): Promise<void> {
13+
await queryRunner.query(`ALTER TABLE "charter_signature" DROP CONSTRAINT "FK_fb0db27afde8d484139b66628fd"`);
14+
await queryRunner.query(`ALTER TABLE "charter_signature" DROP CONSTRAINT "FK_e1f768c9d467cd20b0f45321626"`);
15+
await queryRunner.query(`DROP TABLE "charter_signature"`);
16+
}
17+
18+
}
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
import { AppDataSource } from "../database/data-source";
2+
import { CharterSignature } from "../database/entities/CharterSignature";
3+
import { Group } from "../database/entities/Group";
4+
import { User } from "../database/entities/User";
5+
import crypto from "crypto";
6+
7+
export class CharterSignatureService {
8+
private signatureRepository = AppDataSource.getRepository(CharterSignature);
9+
private groupRepository = AppDataSource.getRepository(Group);
10+
private userRepository = AppDataSource.getRepository(User);
11+
12+
// Create a hash of the charter content to track versions
13+
createCharterHash(charterContent: string): string {
14+
return crypto.createHash('sha256').update(charterContent).digest('hex');
15+
}
16+
17+
// Record a new signature
18+
async recordSignature(
19+
groupId: string,
20+
userId: string,
21+
charterContent: string,
22+
signature: string,
23+
publicKey: string,
24+
message: string
25+
): Promise<CharterSignature> {
26+
const charterHash = this.createCharterHash(charterContent);
27+
28+
const charterSignature = this.signatureRepository.create({
29+
groupId,
30+
userId,
31+
charterHash,
32+
signature,
33+
publicKey,
34+
message
35+
});
36+
37+
return await this.signatureRepository.save(charterSignature);
38+
}
39+
40+
// Get all signatures for a specific charter version
41+
async getSignaturesForCharter(groupId: string, charterContent: string): Promise<CharterSignature[]> {
42+
const charterHash = this.createCharterHash(charterContent);
43+
44+
return await this.signatureRepository.find({
45+
where: {
46+
groupId,
47+
charterHash
48+
},
49+
relations: ['user'],
50+
order: {
51+
createdAt: 'ASC'
52+
}
53+
});
54+
}
55+
56+
// Get all signatures for a group (latest version)
57+
async getLatestSignaturesForGroup(groupId: string): Promise<CharterSignature[]> {
58+
const group = await this.groupRepository.findOne({
59+
where: { id: groupId },
60+
select: ['charter']
61+
});
62+
63+
if (!group?.charter) {
64+
return [];
65+
}
66+
67+
return await this.getSignaturesForCharter(groupId, group.charter);
68+
}
69+
70+
// Check if a user has signed the current charter version
71+
async hasUserSignedCharter(groupId: string, userId: string, charterContent: string): Promise<boolean> {
72+
const charterHash = this.createCharterHash(charterContent);
73+
74+
const signature = await this.signatureRepository.findOne({
75+
where: {
76+
groupId,
77+
userId,
78+
charterHash
79+
}
80+
});
81+
82+
return !!signature;
83+
}
84+
85+
// Get signing status for all participants in a group
86+
async getGroupSigningStatus(groupId: string): Promise<{
87+
participants: any[];
88+
signatures: CharterSignature[];
89+
charterHash: string;
90+
isSigned: boolean;
91+
}> {
92+
const group = await this.groupRepository.findOne({
93+
where: { id: groupId },
94+
relations: ['participants']
95+
});
96+
97+
if (!group) {
98+
throw new Error("Group not found");
99+
}
100+
101+
const charterHash = group.charter ? this.createCharterHash(group.charter) : "";
102+
const signatures = charterHash ? await this.getSignaturesForCharter(groupId, group.charter) : [];
103+
104+
// Create a map of signed user IDs for quick lookup
105+
const signedUserIds = new Set(signatures.map(s => s.userId));
106+
107+
// Add signing status and role information to each participant
108+
const participantsWithStatus = group.participants.map(participant => ({
109+
...participant,
110+
hasSigned: signedUserIds.has(participant.id),
111+
isAdmin: group.admins?.includes(participant.id) || false,
112+
isOwner: group.owner === participant.id
113+
}));
114+
115+
return {
116+
participants: participantsWithStatus,
117+
signatures,
118+
charterHash,
119+
isSigned: signatures.length > 0
120+
};
121+
}
122+
123+
// Get all signatures for a user across all groups
124+
async getUserSignatures(userId: string): Promise<CharterSignature[]> {
125+
return await this.signatureRepository.find({
126+
where: { userId },
127+
relations: ['group'],
128+
order: {
129+
createdAt: 'DESC'
130+
}
131+
});
132+
}
133+
}

0 commit comments

Comments
 (0)