Skip to content

Commit 89ff564

Browse files
Merge pull request eu-digital-identity-wallet#11 from niscy-eudiw/feat/document-signing
feat: implement QES document signing with EUDI Wallet integration
2 parents 3e6a36b + b8b6062 commit 89ff564

File tree

22 files changed

+1696
-48
lines changed

22 files changed

+1696
-48
lines changed

.husky/pre-commit

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
#!/usr/bin/env sh
2-
. "$(dirname -- "$0")/_/husky.sh"
32

4-
npx lint-staged
3+
# Run lint-staged from node_modules (works for everyone)
4+
./node_modules/.bin/lint-staged

README.md

Lines changed: 112 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -13,10 +13,15 @@ This project showcases how traditional recruitment processes can be enhanced wit
1313
- **Two-Stage Verification**:
1414
- Initial PID verification (required for all applications)
1515
- Optional extras verification (diploma/seafarer certificates)
16+
- **Document Signing with QES**: Sign employment contracts using Qualified Electronic Signatures (eIDAS QES)
17+
- Professional PDF contract generation with pdf-lib
18+
- SHA-256 document hashing for integrity verification
19+
- EUDI Wallet-based signing with qualified certificates
20+
- Real-time signing status polling
1621
- **Multi-Device Support**: Same-device deep links and cross-device QR code flows
1722
- **Independent Credential Tracking**: Each verified credential has its own transaction and status
1823
- **Credential Management**: Receive verifiable employment credentials in wallet
19-
- **Real-Time Polling**: 1-second polling intervals for verification status updates
24+
- **Real-Time Polling**: 1.5-second polling intervals for verification and signing status updates
2025
- **Professional Branding**: Consistent European Commission and EUDI Wallet branding
2126
- **Modern UI**: Clean, responsive Material-UI design with accessibility features
2227

@@ -40,10 +45,12 @@ This project showcases how traditional recruitment processes can be enhanced wit
4045

4146
### Digital Identity & Security
4247

43-
- **JOSE** - JSON Web Token handling
48+
- **JOSE** - JSON Web Token handling (JWT signing with ES256)
4449
- **CBOR-X** - Efficient credential encoding/decoding
4550
- **JKS-JS** - Java KeyStore integration
4651
- **QRCode** - QR code generation for verification flows
52+
- **pdf-lib** - Professional PDF generation for contracts
53+
- **crypto** - SHA-256 document hashing and cryptographic operations
4754

4855
### Development Tools
4956

@@ -200,18 +207,31 @@ User → Next.js Page → API Route → Service → Repository → Database
200207
src/
201208
├── app/ # Next.js App Router
202209
│ ├── api/ # API routes
203-
│ │ └── applications/ # Application-related endpoints
204-
│ │ ├── create/ # POST: Create new applications
205-
│ │ ├── verification/[id]/ # GET: PID verification status polling
206-
│ │ ├── verification-extras/[id]/ # GET: Extras verification status polling
207-
│ │ ├── qr/[id]/ # GET: Generate PID verification QR code
208-
│ │ ├── qr-extras/[id]/ # GET: Generate extras verification QR code
209-
│ │ ├── qr-issue/[id]/ # GET: Generate credential offer QR code
210-
│ │ └── [id]/
211-
│ │ ├── extras/ # POST: Request additional credentials
212-
│ │ └── issue-receipt/ # POST: Issue application receipt credential
210+
│ │ ├── applications/ # Application-related endpoints
211+
│ │ │ ├── create/ # POST: Create new applications
212+
│ │ │ ├── verification/[id]/ # GET: PID verification status polling
213+
│ │ │ ├── verification-extras/[id]/ # GET: Extras verification status polling
214+
│ │ │ ├── signing-status/[id]/ # GET: Document signing status polling
215+
│ │ │ ├── qr/[id]/ # GET: Generate PID verification QR code
216+
│ │ │ ├── qr-extras/[id]/ # GET: Generate extras verification QR code
217+
│ │ │ ├── qr-sign/[id]/ # GET: Generate document signing QR code
218+
│ │ │ ├── qr-issue/[id]/ # GET: Generate credential offer QR code
219+
│ │ │ └── [id]/
220+
│ │ │ ├── extras/ # POST: Request additional credentials
221+
│ │ │ ├── sign-document/ # POST: Initiate contract signing
222+
│ │ │ └── issue-receipt/ # POST: Issue application receipt credential
223+
│ │ ├── documents/[state]/ # GET: Serve PDF documents for signing
224+
│ │ ├── request.jwt/[state]/ # GET: Retrieve JWT signing request
225+
│ │ └── signed-document/[state]/ # POST: Receive signed documents from wallet
213226
│ ├── jobs/ # Job listing and detail pages
214227
│ ├── applications/ # Application management and status pages
228+
│ │ └── [id]/
229+
│ │ ├── page.tsx # PID verification QR display
230+
│ │ ├── callback/ # Verification callback handler
231+
│ │ ├── confirmation/ # Post-verification confirmation
232+
│ │ ├── extras/ # Additional credentials QR display
233+
│ │ ├── sign-contract/ # Contract signing QR display
234+
│ │ └── employee/ # Credential issuance QR display
215235
│ ├── layout.tsx # Root layout with providers
216236
│ └── page.tsx # Homepage (redirects to /jobs)
217237
├── components/ # Reusable UI components
@@ -228,7 +248,9 @@ src/
228248
│ │ ├── issuance/ # Credential issuance domain
229249
│ │ │ └── EmployeeCredentialService.ts # Employee credential data builder
230250
│ │ ├── signing/ # Document signing domain
231-
│ │ │ └── DocumentSigningService.ts # QES document signing workflows
251+
│ │ │ ├── DocumentSigningService.ts # QES signing workflow orchestration
252+
│ │ │ ├── ContractPdfGeneratorService.ts # Professional PDF generation
253+
│ │ │ └── DocumentHashService.ts # SHA-256 hashing & verification
232254
│ │ ├── ApplicationService.ts # Main application workflow orchestrator
233255
│ │ ├── JobService.ts # Job posting operations
234256
│ │ ├── VerifierService.ts # EUDI verifier API integration
@@ -240,7 +262,8 @@ src/
240262
│ │ ├── ApplicationRepository.ts # Application lifecycle management
241263
│ │ ├── JobRepository.ts # Job CRUD operations
242264
│ │ ├── CredentialRepository.ts # Issued credentials tracking
243-
│ │ └── VerifiedCredentialRepository.ts # Verified credentials from wallet
265+
│ │ ├── VerifiedCredentialRepository.ts # Verified credentials from wallet
266+
│ │ └── SignedDocumentRepository.ts # Signed documents (optimized queries)
244267
│ ├── schemas/ # Input Validation (Zod)
245268
│ │ ├── application.ts # Application creation, verification schemas
246269
│ │ └── job.ts # Job validation schemas
@@ -257,10 +280,13 @@ src/
257280
└── theme.ts # Material-UI theme configuration
258281
259282
prisma/
260-
├── schema.prisma # Database schema (JobPosting, Application, IssuedCredential, VerifiedCredential)
283+
├── schema.prisma # Database schema (JobPosting, Application, IssuedCredential, VerifiedCredential, SignedDocument)
261284
├── migrations/ # Database migration history
262285
└── seed.ts # Database seeding script
263286
287+
scripts/ # Testing and utility scripts
288+
└── clear-signed-documents.ts # Database cleanup for signed documents
289+
264290
development/ # Development utilities and scripts
265291
266292
env.ts # Environment variable validation and types
@@ -290,7 +316,9 @@ Services are now organized into domain-specific modules for better separation of
290316
- **Issuance Services** (`/services/issuance/`):
291317
- `EmployeeCredentialService`: Builds employee credential data for issuance
292318
- **Signing Services** (`/services/signing/`):
293-
- `DocumentSigningService`: Handles document signing workflows with qualified electronic signatures (QES)
319+
- `DocumentSigningService`: Orchestrates QES document signing workflows and prepares EUDI signing requests
320+
- `ContractPdfGeneratorService`: Generates professional PDF employment contracts using pdf-lib
321+
- `DocumentHashService`: Calculates and verifies SHA-256 document hashes for integrity
294322
- **Core Services**:
295323
- `ApplicationService`: Orchestrates the complete application workflow (creation → verification → issuance)
296324
- `VerifierService`: EUDI verifier API integration
@@ -306,6 +334,7 @@ Services are now organized into domain-specific modules for better separation of
306334
- `JobRepository`: Job posting CRUD operations
307335
- `CredentialRepository`: Issued credentials tracking (for wallet claims)
308336
- `VerifiedCredentialRepository`: Verified credentials from wallet (PID, Diploma, Seafarer)
337+
- `SignedDocumentRepository`: Document signing sessions and signed contracts (optimized to avoid ArrayBuffer detachment)
309338
- **Schemas**: Zod-based input validation with decorator support
310339
- **Types**: EUDI-specific type definitions and JWT structures
311340

@@ -352,10 +381,10 @@ Each verified credential is tracked independently with status: `PENDING → VERI
352381
- **EUDI Branding**: European Commission and EUDI Wallet logos
353382
- **Personal Data**: Shows all extracted information from PID
354383
- **Credential Status**: Visual chips showing verified PID credential
355-
- **Additional Information Section**: If job requires diploma/seafarer certificate:
356-
- User can choose to provide additional credentials
357-
- "Provide Diploma", "Provide Seafarer Certificate", or "Provide Both" buttons
358-
- Optional - user can skip and proceed to credential issuance
384+
- **Additional Information Section**:
385+
- **Option 1**: Provide additional credentials (diploma/seafarer certificates) - Optional
386+
- **Option 2**: Sign employment contract with QES - Proceeds to contract signing
387+
- **Option 3**: Skip to credential issuance - Direct to employee ID issuance
359388

360389
### 4. Additional Credentials Verification (Optional)
361390

@@ -365,7 +394,26 @@ Each verified credential is tracked independently with status: `PENDING → VERI
365394
- **Polling**: Application polls extras verification endpoint every 1 second
366395
- **Return to Confirmation**: After successful verification, redirects back to confirmation page
367396

368-
### 5. Application Receipt Issuance
397+
### 5. Contract Signing with QES (Optional)
398+
399+
- **Sign Contract Page** (`/applications/[id]/sign-contract`): Document signing workflow
400+
- **PDF Generation**: Professional employment contract generated with pdf-lib
401+
- **Document Display**: QR code for EUDI Wallet to access signing request
402+
- **Signing Process**:
403+
1. Application initiates signing by calling `/api/applications/[id]/sign-document`
404+
2. PDF contract generated with candidate and job details
405+
3. SHA-256 hash calculated for document integrity
406+
4. `SignedDocument` record created with state UUID and document content
407+
5. JWT signing request created with document hash and location
408+
6. User scans QR code with EUDI Wallet
409+
7. Wallet retrieves document from `/api/documents/[state]`
410+
8. Wallet signs document with qualified certificate
411+
9. Signed document posted back to `/api/signed-document/[state]`
412+
- **Real-Time Polling**: Status updates every 1.5 seconds via `/api/applications/signing-status/[id]`
413+
- **Success Flow**: Redirects to confirmation page showing "Issue Employee ID" button
414+
- **Error Handling**: Toast notification with retry option on signing failure
415+
416+
### 6. Application Receipt Issuance
369417

370418
- **Employee Page** (`/applications/[id]/employee`): Final step
371419
- **Credential Offer**: QR code for receiving employment credential
@@ -388,6 +436,30 @@ npm run lint # Run ESLint
388436
npx prisma studio # Open Prisma Studio (database GUI)
389437
npx prisma generate # Regenerate Prisma client
390438
npx prisma db push # Push schema changes to database
439+
440+
# Testing & Utilities
441+
442+
```
443+
444+
### Testing Document Signing
445+
446+
You can test the document signing endpoints manually with curl:
447+
448+
```bash
449+
# Create signing session
450+
curl -X POST http://localhost:3000/api/applications/{id}/sign-document
451+
452+
# Download PDF document
453+
curl http://localhost:3000/api/documents/{state} -o contract.pdf
454+
455+
# Retrieve JWT signing request (debug)
456+
curl http://localhost:3000/api/request.jwt/{state}/debug
457+
458+
# Check signing status
459+
curl http://localhost:3000/api/applications/signing-status/{id}
460+
461+
# Clear all signed documents (cleanup utility)
462+
npx tsx scripts/clear-signed-documents.ts
391463
```
392464

393465
### Code Quality
@@ -422,6 +494,15 @@ The application integrates with EUDI-compliant verifier and issuer services:
422494
- `GET /api/applications/verification-extras/{id}` - Poll extras verification status
423495
- `GET /api/applications/qr-extras/{id}` - Generate extras verification QR code
424496

497+
#### Document Signing
498+
499+
- `POST /api/applications/{id}/sign-document` - Initiate contract signing (generates PDF, creates signing session)
500+
- `GET /api/applications/qr-sign/{id}` - Generate document signing QR code
501+
- `GET /api/applications/signing-status/{id}` - Poll document signing status
502+
- `GET /api/request.jwt/{state}` - Retrieve JWT signing request for wallet
503+
- `GET /api/documents/{state}` - Serve PDF document for signing
504+
- `POST /api/signed-document/{state}` - Receive signed document from wallet
505+
425506
#### Credential Issuance
426507

427508
- `GET /api/applications/qr-issue/{id}` - Generate credential offer QR code
@@ -451,7 +532,16 @@ The application integrates with EUDI-compliant verifier and issuer services:
451532

452533
- Tracks application lifecycle: `CREATED → VERIFIED → ISSUED`
453534
- Stores candidate personal data from PID verification
454-
- Relations: JobPosting, IssuedCredentials, VerifiedCredentials
535+
- Relations: JobPosting, IssuedCredentials, VerifiedCredentials, SignedDocuments
536+
537+
#### SignedDocument
538+
539+
- Tracks document signing sessions and signed contracts
540+
- Fields: documentHash (SHA-256), documentType, documentLabel, documentContent (Bytes)
541+
- Transaction tracking: state (UUID), nonce (replay protection)
542+
- Signature data: documentWithSignature (signed PDF), signatureObject, signerCertificate
543+
- Status: `PENDING → SIGNED` or `FAILED`
544+
- **Optimization**: Repository excludes large binary fields by default to prevent ArrayBuffer detachment issues
455545

456546
#### VerifiedCredential
457547

package-lock.json

Lines changed: 43 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@
3030
"jks-js": "^1.1.4",
3131
"jose": "^6.1.0",
3232
"next": "15.5.2",
33+
"pdf-lib": "^1.17.1",
3334
"qrcode": "^1.5.4",
3435
"react": "19.1.0",
3536
"react-dom": "19.1.0",

prisma/schema.prisma

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ model Application {
5050
// Relations
5151
issuedCredentials IssuedCredential[]
5252
verifiedCredentials VerifiedCredential[]
53+
signedDocuments SignedDocument[]
5354
5455
createdAt DateTime @default(now())
5556
updatedAt DateTime @updatedAt
@@ -101,4 +102,38 @@ model VerifiedCredential {
101102
@@index([applicationId])
102103
@@index([verifierTransactionId])
103104
@@index([applicationId, credentialType])
105+
}
106+
107+
// Stores documents to be signed and their signing state
108+
model SignedDocument {
109+
id String @id @default(cuid())
110+
applicationId String
111+
application Application @relation(fields: [applicationId], references: [id], onDelete: Cascade)
112+
113+
// Document information
114+
documentHash String // SHA-256 hash of the document
115+
documentType String // e.g., "employment_contract"
116+
documentLabel String // Display name, e.g., "Contract.pdf"
117+
documentContent Bytes? // PDF bytes stored in database
118+
119+
// Transaction state for signing flow
120+
state String @unique // UUID for this signing transaction
121+
nonce String // Random UUID for replay protection
122+
123+
// Signature data (populated after wallet callback)
124+
documentWithSignature Bytes? // Signed document bytes
125+
signatureObject String? // Signature object as string (if provided separately)
126+
signatureQualifier String? // e.g., "eu_eidas_qes"
127+
signerCertificate String? // X.509 certificate from signer
128+
129+
// Status tracking
130+
status String @default("PENDING") // PENDING, SIGNED, FAILED
131+
errorCode String? // Error code if signing failed
132+
signedAt DateTime?
133+
134+
createdAt DateTime @default(now())
135+
136+
@@index([applicationId])
137+
@@index([state])
138+
@@index([status])
104139
}

scripts/clear-signed-documents.ts

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
#!/usr/bin/env tsx
2+
/**
3+
* Clear all signed documents from the database
4+
* Use this to clean up old/corrupted document data
5+
*/
6+
7+
import { PrismaClient } from '@prisma/client';
8+
9+
const prisma = new PrismaClient();
10+
11+
async function main() {
12+
console.log('Clearing all signed documents...');
13+
14+
const result = await prisma.signedDocument.deleteMany({});
15+
16+
console.log(`Deleted ${result.count} signed document(s)`);
17+
console.log('Database is now clean!');
18+
}
19+
20+
main()
21+
.then(() => prisma.$disconnect())
22+
.catch((error) => {
23+
console.error('Error:', error);
24+
process.exit(1);
25+
});

0 commit comments

Comments
 (0)