diff options
author | Pawel Zelawski <pawel.zelawski@outlook.com> | 2025-04-10 10:50:57 +0200 |
---|---|---|
committer | Pawel Zelawski <pawel.zelawski@outlook.com> | 2025-04-10 10:50:57 +0200 |
commit | 326caf949ec8622c04b0e3352c5eac5370f161e4 (patch) | |
tree | 5b4d1dbeeb693e25dce770e393645a60f1e409a9 | |
parent | ef435a15ca67c829ce6cd8551ac45c419cb9792e (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.json | 11 | ||||
-rw-r--r-- | package.json | 5 | ||||
-rw-r--r-- | src/server/main.ts | 219 | ||||
-rw-r--r-- | src/server/utils.ts | 20 |
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 |