Skip to content

Commit 089fe25

Browse files
committed
feat: emover
1 parent 9f56234 commit 089fe25

38 files changed

+2809
-47
lines changed

infrastructure/evault-core/src/core/db/db.service.ts

Lines changed: 215 additions & 41 deletions
Large diffs are not rendered by default.

infrastructure/evault-core/src/core/http/server.ts

Lines changed: 193 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,8 @@ import type {
77
ProvisionRequest,
88
ProvisioningService,
99
} from "../../services/ProvisioningService";
10-
import type { DbService } from "../db/db.service";
10+
import { DbService } from "../db/db.service";
11+
import { connectWithRetry } from "../db/retry-neo4j";
1112
import { type TypedReply, type TypedRequest, WatcherRequest } from "./types";
1213

1314
interface WatcherSignatureRequest {
@@ -341,7 +342,7 @@ export async function registerHttpRoutes(
341342
const { payload } = await jose.jwtVerify(token, JWKS);
342343

343344
console.log(
344-
`Token validation: Token verified successfully, payload:`,
345+
"Token validation: Token verified successfully, payload:",
345346
payload,
346347
);
347348
return payload;
@@ -356,7 +357,7 @@ export async function registerHttpRoutes(
356357
);
357358
}
358359
if (error.cause) {
359-
console.error(`Token validation error cause:`, error.cause);
360+
console.error("Token validation error cause:", error.cause);
360361
}
361362
return null;
362363
}
@@ -421,8 +422,7 @@ export async function registerHttpRoutes(
421422
}
422423

423424
const authHeader =
424-
request.headers.authorization ||
425-
request.headers["Authorization"];
425+
request.headers.authorization || request.headers.Authorization;
426426
const tokenPayload = await validateToken(
427427
typeof authHeader === "string" ? authHeader : null,
428428
);
@@ -516,4 +516,192 @@ export async function registerHttpRoutes(
516516
},
517517
);
518518
}
519+
520+
// Emover endpoint - Copy metaEnvelopes to new evault instance
521+
server.post<{
522+
Body: {
523+
eName: string;
524+
targetNeo4jUri: string;
525+
targetNeo4jUser: string;
526+
targetNeo4jPassword: string;
527+
};
528+
}>(
529+
"/emover",
530+
{
531+
schema: {
532+
tags: ["migration"],
533+
description:
534+
"Copy all metaEnvelopes for an eName to a new evault instance",
535+
body: {
536+
type: "object",
537+
required: [
538+
"eName",
539+
"targetNeo4jUri",
540+
"targetNeo4jUser",
541+
"targetNeo4jPassword",
542+
],
543+
properties: {
544+
eName: { type: "string" },
545+
targetNeo4jUri: { type: "string" },
546+
targetNeo4jUser: { type: "string" },
547+
targetNeo4jPassword: { type: "string" },
548+
},
549+
},
550+
response: {
551+
200: {
552+
type: "object",
553+
properties: {
554+
success: { type: "boolean" },
555+
count: { type: "number" },
556+
message: { type: "string" },
557+
},
558+
},
559+
400: {
560+
type: "object",
561+
properties: {
562+
error: { type: "string" },
563+
},
564+
},
565+
500: {
566+
type: "object",
567+
properties: {
568+
error: { type: "string" },
569+
},
570+
},
571+
},
572+
},
573+
},
574+
async (
575+
request: TypedRequest<{
576+
eName: string;
577+
targetNeo4jUri: string;
578+
targetNeo4jUser: string;
579+
targetNeo4jPassword: string;
580+
}>,
581+
reply: TypedReply,
582+
) => {
583+
const {
584+
eName,
585+
targetNeo4jUri,
586+
targetNeo4jUser,
587+
targetNeo4jPassword,
588+
} = request.body;
589+
590+
if (!dbService) {
591+
return reply.status(500).send({
592+
error: "Database service not available",
593+
});
594+
}
595+
596+
try {
597+
console.log(
598+
`[MIGRATION] Starting migration for eName: ${eName} to target evault`,
599+
);
600+
601+
// Step 1: Validate eName exists in current evault
602+
const existingMetaEnvelopes =
603+
await dbService.findAllMetaEnvelopesByEName(eName);
604+
if (existingMetaEnvelopes.length === 0) {
605+
console.log(
606+
`[MIGRATION] No metaEnvelopes found for eName: ${eName}`,
607+
);
608+
return reply.status(400).send({
609+
error: `No metaEnvelopes found for eName: ${eName}`,
610+
});
611+
}
612+
613+
console.log(
614+
`[MIGRATION] Found ${existingMetaEnvelopes.length} metaEnvelopes for eName: ${eName}`,
615+
);
616+
617+
// Step 2: Create connection to target evault's Neo4j
618+
console.log(
619+
`[MIGRATION] Connecting to target Neo4j at: ${targetNeo4jUri}`,
620+
);
621+
const targetDriver = await connectWithRetry(
622+
targetNeo4jUri,
623+
targetNeo4jUser,
624+
targetNeo4jPassword,
625+
);
626+
const targetDbService = new DbService(targetDriver);
627+
628+
try {
629+
// Step 3: Copy all metaEnvelopes to target evault
630+
console.log(
631+
`[MIGRATION] Copying ${existingMetaEnvelopes.length} metaEnvelopes to target evault`,
632+
);
633+
const copiedCount =
634+
await dbService.copyMetaEnvelopesToNewEvaultInstance(
635+
eName,
636+
targetDbService,
637+
);
638+
639+
// Step 4: Verify copy
640+
console.log(
641+
`[MIGRATION] Verifying copy: checking ${copiedCount} metaEnvelopes in target evault`,
642+
);
643+
const targetMetaEnvelopes =
644+
await targetDbService.findAllMetaEnvelopesByEName(
645+
eName,
646+
);
647+
648+
if (
649+
targetMetaEnvelopes.length !==
650+
existingMetaEnvelopes.length
651+
) {
652+
const error = `Copy verification failed: expected ${existingMetaEnvelopes.length} metaEnvelopes, found ${targetMetaEnvelopes.length}`;
653+
console.error(`[MIGRATION ERROR] ${error}`);
654+
return reply.status(500).send({ error });
655+
}
656+
657+
// Verify IDs match
658+
const sourceIds = new Set(
659+
existingMetaEnvelopes.map((m) => m.id),
660+
);
661+
const targetIds = new Set(
662+
targetMetaEnvelopes.map((m) => m.id),
663+
);
664+
665+
if (sourceIds.size !== targetIds.size) {
666+
const error =
667+
"Copy verification failed: ID count mismatch";
668+
console.error(`[MIGRATION ERROR] ${error}`);
669+
return reply.status(500).send({ error });
670+
}
671+
672+
for (const id of sourceIds) {
673+
if (!targetIds.has(id)) {
674+
const error = `Copy verification failed: missing metaEnvelope ID: ${id}`;
675+
console.error(`[MIGRATION ERROR] ${error}`);
676+
return reply.status(500).send({ error });
677+
}
678+
}
679+
680+
console.log(
681+
`[MIGRATION] Verification successful: ${copiedCount} metaEnvelopes copied and verified`,
682+
);
683+
684+
// Close target connection
685+
await targetDriver.close();
686+
687+
return {
688+
success: true,
689+
count: copiedCount,
690+
message: `Successfully copied ${copiedCount} metaEnvelopes to target evault`,
691+
};
692+
} catch (copyError) {
693+
await targetDriver.close();
694+
throw copyError;
695+
}
696+
} catch (error) {
697+
console.error(`[MIGRATION ERROR] Migration failed:`, error);
698+
return reply.status(500).send({
699+
error:
700+
error instanceof Error
701+
? error.message
702+
: "Failed to migrate metaEnvelopes",
703+
});
704+
}
705+
},
706+
);
519707
}

platforms/emover-api/README.md

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
# Emover API
2+
3+
Backend API for the emover platform that handles evault migration operations.
4+
5+
## Features
6+
7+
- Secure authentication via eID Wallet (QR code + SSE)
8+
- View current evault host/provider information
9+
- List available provisioners
10+
- Initiate and manage evault migrations
11+
- QR code signing for migration confirmation
12+
- Real-time migration progress via SSE
13+
- Comprehensive logging at every step
14+
15+
## Environment Variables
16+
17+
- `PORT` - Server port (default: 4000)
18+
- `PUBLIC_EMOVER_BASE_URL` - Frontend base URL
19+
- `PUBLIC_REGISTRY_URL` - Registry service URL
20+
- `PROVISIONER_URL` or `PROVISIONER_URLS` - Provisioner URL(s)
21+
- `EVAULT_BASE_URI` - Base URI for evault instances
22+
- `EMOVER_DATABASE_URL` or `DATABASE_URL` - PostgreSQL connection string
23+
- `JWT_SECRET` - Secret for JWT token signing
24+
- `REGISTRY_SHARED_SECRET` - Secret for registry API authentication
25+
- `NEO4J_USER` - Neo4j username (for cross-evault operations)
26+
- `NEO4J_PASSWORD` - Neo4j password (for cross-evault operations)
27+
- `DEMO_VERIFICATION_CODE` - Demo verification code for provisioning
28+
29+
## API Endpoints
30+
31+
### Authentication
32+
- `GET /api/auth/offer` - Get QR code for login
33+
- `POST /api/auth` - Handle eID Wallet callback
34+
- `GET /api/auth/sessions/:id` - SSE stream for auth status
35+
36+
### User
37+
- `GET /api/users/me` - Get current user (protected)
38+
39+
### Evault Info
40+
- `GET /api/evault/current` - Get current evault info (protected)
41+
- `GET /api/provisioners` - List available provisioners (protected)
42+
43+
### Migration
44+
- `POST /api/migration/initiate` - Start migration (protected)
45+
- `POST /api/migration/sign` - Create signing session (protected)
46+
- `GET /api/migration/sessions/:id` - SSE stream for migration status
47+
- `POST /api/migration/callback` - Handle signed payload from eID Wallet
48+
- `GET /api/migration/status/:id` - Get migration status
49+
- `POST /api/migration/delete-old` - Delete old evault (protected)
50+
51+
## Migration Flow
52+
53+
1. User initiates migration with selected provisioner
54+
2. System provisions new evault instance
55+
3. **Copies all metaEnvelopes to new evault** (preserving IDs and eName)
56+
4. **Verifies copy** (count, IDs, integrity)
57+
5. **Updates registry mapping** (only after successful verification)
58+
6. **Verifies registry update**
59+
7. **Marks new evault as active**
60+
8. **Verifies new evault is working**
61+
9. **Deletes old evault** (only after all above steps succeed)
62+
63+
## Database
64+
65+
Uses PostgreSQL with TypeORM. Run migrations:
66+
67+
```bash
68+
npm run migration:run
69+
```
70+
71+
## Development
72+
73+
```bash
74+
npm install
75+
npm run dev
76+
```
77+

platforms/emover-api/package.json

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
{
2+
"name": "emover-api",
3+
"version": "1.0.0",
4+
"description": "Emover Platform API for evault migration",
5+
"main": "src/index.ts",
6+
"scripts": {
7+
"start": "ts-node --project tsconfig.json src/index.ts",
8+
"dev": "nodemon --exec \"npx ts-node\" src/index.ts",
9+
"build": "tsc",
10+
"typeorm": "typeorm-ts-node-commonjs",
11+
"migration:generate": "typeorm-ts-node-commonjs migration:generate src/database/migrations/migration -d src/database/data-source.ts",
12+
"migration:run": "typeorm-ts-node-commonjs migration:run -d src/database/data-source.ts",
13+
"migration:revert": "typeorm-ts-node-commonjs migration:revert -d src/database/data-source.ts"
14+
},
15+
"dependencies": {
16+
"axios": "^1.6.7",
17+
"cors": "^2.8.5",
18+
"dotenv": "^16.4.5",
19+
"express": "^4.18.2",
20+
"jsonwebtoken": "^9.0.2",
21+
"pg": "^8.11.3",
22+
"reflect-metadata": "^0.2.1",
23+
"typeorm": "^0.3.24",
24+
"uuid": "^9.0.1"
25+
},
26+
"devDependencies": {
27+
"@types/cors": "^2.8.17",
28+
"@types/express": "^4.17.21",
29+
"@types/jsonwebtoken": "^9.0.5",
30+
"@types/node": "^20.11.24",
31+
"@types/pg": "^8.11.2",
32+
"@types/uuid": "^9.0.8",
33+
"nodemon": "^3.0.3",
34+
"ts-node": "^10.9.2",
35+
"typescript": "^5.3.3"
36+
}
37+
}
38+

0 commit comments

Comments
 (0)