summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPawel Zelawski <pawel.zelawski@outlook.com>2025-04-10 10:50:57 +0200
committerPawel Zelawski <pawel.zelawski@outlook.com>2025-04-10 10:50:57 +0200
commit326caf949ec8622c04b0e3352c5eac5370f161e4 (patch)
tree5b4d1dbeeb693e25dce770e393645a60f1e409a9
parentef435a15ca67c829ce6cd8551ac45c419cb9792e (diff)
feat(server): implement basic backend API
- Added basic Express server setup in `src/server/main.ts`. - Configured `dotenv` to load environment variables. - Implemented in-memory storage for session state (pending, success, failed). - Created `/api/digiid/start` endpoint: - Generates session ID and nonce. - Constructs callback URL from `PUBLIC_URL`. - Determines `unsecure` flag based on URL scheme. - Stores initial session state. - Generates QR code data URL. - Returns `sessionId` and `qrCodeDataUrl`. - Includes placeholder for `digiidTs.generateDigiIDUri`. - Created `/api/digiid/callback` endpoint: - Receives address, uri, signature from DigiID app. - Parses nonce from received URI. - Looks up session by nonce. - Reconstructs expected callback URL. - Updates session state based on placeholder verification. - Includes placeholder for `digiidTs.verifyDigiIDCallback`. - Responds 200 OK as per DigiID protocol. - Created `/api/digiid/status/:sessionId` endpoint: - Retrieves and returns session status, address, and error. - Added address type helper `getDigiByteAddressType` in `src/server/utils.ts` (with placeholder logic for DGA). - Added `dev:backend` script to `package.json` using `nodemon` and `ts-node/esm`. - Added `"type": "module"` to `package.json`. - Installed `@types/dotenv`. - (Note: Outstanding TypeScript linter errors related to async Express handlers require further investigation).
-rw-r--r--package-lock.json11
-rw-r--r--package.json5
-rw-r--r--src/server/main.ts219
-rw-r--r--src/server/utils.ts20
4 files changed, 254 insertions, 1 deletions
diff --git a/package-lock.json b/package-lock.json
index 2532abc..76e7a97 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -9,6 +9,7 @@
"version": "1.0.0",
"license": "ISC",
"devDependencies": {
+ "@types/dotenv": "^6.1.1",
"@types/express": "^5.0.1",
"@types/node": "^22.14.0",
"@types/qrcode": "^1.5.5",
@@ -1528,6 +1529,16 @@
"@types/node": "*"
}
},
+ "node_modules/@types/dotenv": {
+ "version": "6.1.1",
+ "resolved": "https://registry.npmjs.org/@types/dotenv/-/dotenv-6.1.1.tgz",
+ "integrity": "sha512-ftQl3DtBvqHl9L16tpqqzA4YzCSXZfi7g8cQceTz5rOlYtk/IZbFjAv3mLOQlNIgOaylCQWQoBdDQHPgEBJPHg==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "@types/node": "*"
+ }
+ },
"node_modules/@types/estree": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
diff --git a/package.json b/package.json
index b0e3c03..9224785 100644
--- a/package.json
+++ b/package.json
@@ -3,8 +3,10 @@
"version": "1.0.0",
"description": "",
"main": "index.js",
+ "type": "module",
"scripts": {
- "test": "echo \"Error: no test specified\" && exit 1"
+ "test": "echo \"Error: no test specified\" && exit 1",
+ "dev:backend": "nodemon --watch src/server --ext ts --exec \"node --loader ts-node/esm src/server/main.ts\""
},
"repository": {
"type": "git",
@@ -18,6 +20,7 @@
},
"homepage": "https://github.com/pawelzelawski/digiid-ts-demo#readme",
"devDependencies": {
+ "@types/dotenv": "^6.1.1",
"@types/express": "^5.0.1",
"@types/node": "^22.14.0",
"@types/qrcode": "^1.5.5",
diff --git a/src/server/main.ts b/src/server/main.ts
new file mode 100644
index 0000000..e94fcb0
--- /dev/null
+++ b/src/server/main.ts
@@ -0,0 +1,219 @@
+import dotenv from 'dotenv';
+import express, { Request, Response } from 'express';
+import { randomBytes } from 'crypto';
+import qrcode from 'qrcode';
+// TODO: Import digiidTs library once linked
+// import * as digiidTs from 'digiid-ts';
+
+// Load environment variables from .env file
+dotenv.config();
+
+const app = express();
+const PORT = process.env.PORT || 3001; // Default to 3001 if PORT is not set
+
+// In-memory storage for demo purposes
+// NOTE: In a production application, use a persistent store (e.g., Redis, database)
+interface SessionState {
+ nonce: string;
+ status: 'pending' | 'success' | 'failed';
+ address?: string;
+ error?: string;
+}
+const sessionStore = new Map<string, SessionState>();
+const nonceToSessionMap = new Map<string, string>();
+
+// Middleware
+app.use(express.json()); // Parse JSON bodies
+
+console.log('Server starting...');
+console.log(`Attempting to listen on port: ${PORT}`);
+console.log(`Configured PUBLIC_URL: ${process.env.PUBLIC_URL}`);
+
+// TODO: Implement DigiID API endpoints
+
+// Endpoint to initiate the DigiID authentication flow
+app.get('/api/digiid/start', async (req: Request, res: Response) => {
+ try {
+ const sessionId = randomBytes(16).toString('hex');
+ const nonce = randomBytes(16).toString('hex');
+
+ const publicUrl = process.env.PUBLIC_URL;
+ if (!publicUrl) {
+ console.error('PUBLIC_URL environment variable is not set.');
+ return res.status(500).json({ error: 'Server configuration error: PUBLIC_URL is missing.' });
+ }
+
+ let callbackUrl: string;
+ try {
+ const baseUrl = new URL(publicUrl);
+ callbackUrl = new URL('/api/digiid/callback', baseUrl).toString();
+ } catch (error) {
+ console.error('Invalid PUBLIC_URL format:', publicUrl, error);
+ return res.status(500).json({ error: 'Server configuration error: Invalid PUBLIC_URL format.' });
+ }
+
+ // Determine if the callback URL is insecure (HTTP)
+ const unsecure = callbackUrl.startsWith('http://');
+
+ // Placeholder for digiidTs.generateDigiIDUri call
+ // const digiIdUri = digiidTs.generateDigiIDUri({ callbackUrl, unsecure, nonce });
+ const digiIdUri = `digiid://example.com?x=${nonce}&unsecure=${unsecure ? 1 : 0}&callback=${encodeURIComponent(callbackUrl)}`; // TEMPORARY PLACEHOLDER
+ console.log(`Generated DigiID URI: ${digiIdUri} for session ${sessionId}`);
+
+ // Store session state
+ sessionStore.set(sessionId, { nonce, status: 'pending' });
+ nonceToSessionMap.set(nonce, sessionId);
+ console.log(`Stored pending session: ${sessionId}, nonce: ${nonce}`);
+
+ // Generate QR code
+ const qrCodeDataUrl = await qrcode.toDataURL(digiIdUri);
+
+ res.json({ sessionId, qrCodeDataUrl });
+
+ } catch (error) {
+ console.error('Error in /api/digiid/start:', error);
+ res.status(500).json({ error: 'Internal server error' });
+ }
+});
+
+// Callback endpoint for the DigiID mobile app
+app.post('/api/digiid/callback', async (req: Request, res: Response) => {
+ const { address, uri, signature } = req.body;
+
+ console.log('Received callback:', { address, uri, signature });
+
+ if (!address || !uri || !signature) {
+ console.warn('Callback missing required fields.');
+ // Note: DigiID protocol doesn't expect a body response on failure here,
+ // just a non-200 status, but sending JSON for easier debugging.
+ return res.status(400).json({ error: 'Missing required callback parameters.' });
+ }
+
+ let receivedNonce: string | null = null;
+ try {
+ // Parse the nonce (parameter 'x') from the received URI
+ const parsedUri = new URL(uri);
+ receivedNonce = parsedUri.searchParams.get('x');
+ } catch (error) {
+ console.warn('Error parsing received URI:', uri, error);
+ return res.status(400).json({ error: 'Invalid URI format.' });
+ }
+
+ if (!receivedNonce) {
+ console.warn('Nonce (x parameter) not found in received URI:', uri);
+ return res.status(400).json({ error: 'Nonce not found in URI.' });
+ }
+
+ // Find the session ID associated with the nonce
+ const sessionId = nonceToSessionMap.get(receivedNonce);
+ if (!sessionId) {
+ console.warn('Received nonce does not correspond to any active session:', receivedNonce);
+ // This could happen if the session expired or the nonce is invalid/reused
+ return res.status(404).json({ error: 'Session not found or expired for this nonce.' });
+ }
+
+ // Retrieve the session state
+ const session = sessionStore.get(sessionId);
+ if (!session || session.status !== 'pending') {
+ console.warn('Session not found or not in pending state for ID:', sessionId);
+ // Should ideally not happen if nonceToSessionMap is consistent with sessionStore
+ return res.status(404).json({ error: 'Session not found or already completed/failed.' });
+ }
+
+ // Construct the expected callback URL (must match the one used in /start)
+ const publicUrl = process.env.PUBLIC_URL;
+ if (!publicUrl) {
+ // This should have been caught in /start, but double-check
+ console.error('PUBLIC_URL is unexpectedly missing during callback.');
+ session.status = 'failed';
+ session.error = 'Server configuration error: PUBLIC_URL missing.';
+ return res.status(500).send(); // Send 500 internal server error
+ }
+ let expectedCallbackUrl: string;
+ try {
+ expectedCallbackUrl = new URL('/api/digiid/callback', publicUrl).toString();
+ } catch (error) {
+ console.error('Invalid PUBLIC_URL format during callback:', publicUrl, error);
+ session.status = 'failed';
+ session.error = 'Server configuration error: Invalid PUBLIC_URL.';
+ return res.status(500).send();
+ }
+
+ const expectedNonce = session.nonce;
+
+ try {
+ console.log('Verifying callback with:', {
+ address,
+ uri,
+ signature,
+ expectedCallbackUrl,
+ expectedNonce,
+ });
+
+ // Placeholder for digiidTs.verifyDigiIDCallback call
+ // const isValid = await digiidTs.verifyDigiIDCallback({
+ // address,
+ // uri,
+ // signature,
+ // callbackUrl: expectedCallbackUrl,
+ // nonce: expectedNonce,
+ // });
+
+ // --- TEMPORARY PLACEHOLDER VERIFICATION ---
+ // Simulating verification: check if nonce matches and URI contains expected callback
+ const isValid = receivedNonce === expectedNonce && uri.includes(expectedCallbackUrl);
+ console.log(`Placeholder verification result: ${isValid}`);
+ // --- END PLACEHOLDER ---
+
+ if (isValid) {
+ console.log(`Verification successful for session ${sessionId}, address: ${address}`);
+ session.status = 'success';
+ session.address = address;
+ // Clean up nonce lookup map once verified successfully
+ nonceToSessionMap.delete(expectedNonce);
+ } else {
+ console.warn(`Verification failed for session ${sessionId}`);
+ session.status = 'failed';
+ session.error = 'Signature verification failed.';
+ // Keep nonce in map for potential debugging, or clean up based on policy
+ // nonceToSessionMap.delete(expectedNonce);
+ }
+
+ // Update the session store
+ sessionStore.set(sessionId, session);
+
+ // DigiID protocol expects a 200 OK on success/failure after processing
+ res.status(200).send();
+
+ } catch (error) {
+ console.error('Error during callback verification process for session:', sessionId, error);
+ // Update session state to reflect the error
+ session.status = 'failed';
+ session.error = 'Internal server error during verification.';
+ sessionStore.set(sessionId, session);
+ // Don't expose internal errors via status code if possible, but log them
+ res.status(200).send(); // Still send 200 as processing happened, but log indicates issue
+ }
+});
+
+// Endpoint to check the status of an authentication session
+app.get('/api/digiid/status/:sessionId', (req: Request, res: Response) => {
+ const { sessionId } = req.params;
+ const session = sessionStore.get(sessionId);
+
+ if (!session) {
+ return res.status(404).json({ status: 'not_found' });
+ }
+
+ // Return only the necessary fields to the client
+ const { status, address, error } = session;
+ res.json({ status, address, error }); // address and error will be undefined if not set
+});
+
+app.get('/', (_: Request, res: Response) => {
+ res.send('DigiID Demo Backend Running!');
+});
+
+app.listen(PORT, () => {
+ console.log(`Server listening on port ${PORT}`);
+}); \ No newline at end of file
diff --git a/src/server/utils.ts b/src/server/utils.ts
new file mode 100644
index 0000000..0b1333f
--- /dev/null
+++ b/src/server/utils.ts
@@ -0,0 +1,20 @@
+// Simple utility to determine DigiByte address type based on prefix
+
+export type DigiByteAddressType = 'DigiByte (DGB)' | 'DigiAsset (DGA)' | 'Unknown';
+
+export function getDigiByteAddressType(address: string): DigiByteAddressType {
+ if (address.startsWith('dgb1')) {
+ return 'DigiByte (DGB)';
+ }
+ // Add other prefixes if DigiAssets use a distinct one, e.g., 'dga1'
+ // For now, assume non-DGB is DigiAsset, but this might need refinement
+ // depending on actual DigiAsset address formats.
+ // If the digiid-ts library provides a helper for this, use that instead.
+ else if (address) { // Basic check to differentiate from empty/null
+ // Assuming DigiAssets might start differently or be the fallback
+ // This is a placeholder assumption.
+ // A more robust check based on DigiAsset address specification is needed.
+ return 'DigiAsset (DGA)'; // Placeholder - ADJUST BASED ON ACTUAL DGA PREFIX
+ }
+ return 'Unknown';
+} \ No newline at end of file