feat: upgrade EncryptID server to PostgreSQL
Replace in-memory Maps with persistent PostgreSQL storage: - Add db.ts with typed query functions for users, credentials, challenges - Add schema.sql with users/credentials/challenges tables - Update server.ts to use async DB queries - Add postgres service to docker-compose - Health endpoint now reports database connectivity - Auto-cleanup of expired challenges every 10 minutes Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
8e10f5cb03
commit
e5af01119b
|
|
@ -9,8 +9,8 @@ WORKDIR /app
|
|||
# Copy package files
|
||||
COPY package.json bun.lockb* ./
|
||||
|
||||
# Install dependencies
|
||||
RUN bun install --frozen-lockfile
|
||||
# Install dependencies (including postgres)
|
||||
RUN bun install --frozen-lockfile || bun install
|
||||
|
||||
# Copy source
|
||||
COPY src/encryptid ./src/encryptid
|
||||
|
|
@ -44,8 +44,8 @@ ENV PORT=3000
|
|||
EXPOSE 3000
|
||||
|
||||
# Health check
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||
CMD bun -e "fetch('http://localhost:3000/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"
|
||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=10s --retries=3 \
|
||||
CMD bun -e "fetch('http://localhost:3000/health').then(r => r.json()).then(d => process.exit(d.database ? 0 : 1)).catch(() => process.exit(1))"
|
||||
|
||||
# Start server
|
||||
CMD ["bun", "run", "src/encryptid/server.ts"]
|
||||
|
|
|
|||
|
|
@ -8,10 +8,14 @@ services:
|
|||
dockerfile: Dockerfile.encryptid
|
||||
container_name: encryptid
|
||||
restart: unless-stopped
|
||||
depends_on:
|
||||
encryptid-db:
|
||||
condition: service_healthy
|
||||
environment:
|
||||
- NODE_ENV=production
|
||||
- PORT=3000
|
||||
- JWT_SECRET=${JWT_SECRET:-change-this-in-production}
|
||||
- DATABASE_URL=postgres://encryptid:${ENCRYPTID_DB_PASSWORD:-encryptid}@encryptid-db:5432/encryptid
|
||||
labels:
|
||||
# Traefik auto-discovery
|
||||
- "traefik.enable=true"
|
||||
|
|
@ -23,13 +27,38 @@ services:
|
|||
- "traefik.http.routers.encryptid-wellknown.entrypoints=web"
|
||||
networks:
|
||||
- traefik-public
|
||||
- encryptid-internal
|
||||
healthcheck:
|
||||
test: ["CMD", "bun", "-e", "fetch('http://localhost:3000/health').then(r => process.exit(r.ok ? 0 : 1)).catch(() => process.exit(1))"]
|
||||
test: ["CMD", "bun", "-e", "fetch('http://localhost:3000/health').then(r => r.json()).then(d => process.exit(d.database ? 0 : 1)).catch(() => process.exit(1))"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 3
|
||||
start_period: 15s
|
||||
|
||||
encryptid-db:
|
||||
image: postgres:16-alpine
|
||||
container_name: encryptid-db
|
||||
restart: unless-stopped
|
||||
environment:
|
||||
- POSTGRES_DB=encryptid
|
||||
- POSTGRES_USER=encryptid
|
||||
- POSTGRES_PASSWORD=${ENCRYPTID_DB_PASSWORD:-encryptid}
|
||||
volumes:
|
||||
- encryptid-pgdata:/var/lib/postgresql/data
|
||||
networks:
|
||||
- encryptid-internal
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U encryptid -d encryptid"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
start_period: 10s
|
||||
|
||||
volumes:
|
||||
encryptid-pgdata:
|
||||
|
||||
networks:
|
||||
traefik-public:
|
||||
external: true
|
||||
encryptid-internal:
|
||||
driver: bridge
|
||||
|
|
|
|||
|
|
@ -16,6 +16,7 @@
|
|||
"@automerge/automerge": "^2.2.8",
|
||||
"@lit/reactive-element": "^2.0.4",
|
||||
"hono": "^4.11.7",
|
||||
"postgres": "^3.4.5",
|
||||
"perfect-arrows": "^0.3.7",
|
||||
"perfect-freehand": "^1.2.2"
|
||||
},
|
||||
|
|
|
|||
|
|
@ -0,0 +1,205 @@
|
|||
/**
|
||||
* EncryptID Database Layer — PostgreSQL
|
||||
*
|
||||
* Replaces in-memory Maps with persistent PostgreSQL storage.
|
||||
* Uses the 'postgres' npm package (lightweight, no native deps with Bun).
|
||||
*/
|
||||
|
||||
import postgres from 'postgres';
|
||||
import { readFileSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
// ============================================================================
|
||||
// CONNECTION
|
||||
// ============================================================================
|
||||
|
||||
const DATABASE_URL = process.env.DATABASE_URL || 'postgres://encryptid:encryptid@localhost:5432/encryptid';
|
||||
|
||||
const sql = postgres(DATABASE_URL, {
|
||||
max: 10,
|
||||
idle_timeout: 20,
|
||||
connect_timeout: 10,
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
// TYPES
|
||||
// ============================================================================
|
||||
|
||||
export interface StoredCredential {
|
||||
credentialId: string;
|
||||
publicKey: string;
|
||||
userId: string;
|
||||
username: string;
|
||||
counter: number;
|
||||
createdAt: number;
|
||||
lastUsed?: number;
|
||||
transports?: string[];
|
||||
}
|
||||
|
||||
export interface StoredChallenge {
|
||||
challenge: string;
|
||||
userId?: string;
|
||||
type: 'registration' | 'authentication';
|
||||
createdAt: number;
|
||||
expiresAt: number;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// INITIALIZATION
|
||||
// ============================================================================
|
||||
|
||||
export async function initDatabase(): Promise<void> {
|
||||
const schema = readFileSync(join(import.meta.dir, 'schema.sql'), 'utf-8');
|
||||
await sql.unsafe(schema);
|
||||
console.log('EncryptID: Database initialized');
|
||||
|
||||
// Clean expired challenges on startup
|
||||
await cleanExpiredChallenges();
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// USER OPERATIONS
|
||||
// ============================================================================
|
||||
|
||||
export async function createUser(id: string, username: string, displayName?: string, did?: string): Promise<void> {
|
||||
await sql`
|
||||
INSERT INTO users (id, username, display_name, did)
|
||||
VALUES (${id}, ${username}, ${displayName || username}, ${did || null})
|
||||
ON CONFLICT (id) DO NOTHING
|
||||
`;
|
||||
}
|
||||
|
||||
export async function getUserByUsername(username: string) {
|
||||
const [user] = await sql`SELECT * FROM users WHERE username = ${username}`;
|
||||
return user || null;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CREDENTIAL OPERATIONS
|
||||
// ============================================================================
|
||||
|
||||
export async function storeCredential(cred: StoredCredential): Promise<void> {
|
||||
// Ensure user exists first
|
||||
await createUser(cred.userId, cred.username);
|
||||
|
||||
await sql`
|
||||
INSERT INTO credentials (credential_id, user_id, public_key, counter, transports, created_at)
|
||||
VALUES (
|
||||
${cred.credentialId},
|
||||
${cred.userId},
|
||||
${cred.publicKey},
|
||||
${cred.counter},
|
||||
${cred.transports || null},
|
||||
${new Date(cred.createdAt)}
|
||||
)
|
||||
`;
|
||||
}
|
||||
|
||||
export async function getCredential(credentialId: string): Promise<StoredCredential | null> {
|
||||
const rows = await sql`
|
||||
SELECT c.credential_id, c.public_key, c.user_id, c.counter,
|
||||
c.transports, c.created_at, c.last_used, u.username
|
||||
FROM credentials c
|
||||
JOIN users u ON c.user_id = u.id
|
||||
WHERE c.credential_id = ${credentialId}
|
||||
`;
|
||||
if (rows.length === 0) return null;
|
||||
|
||||
const row = rows[0];
|
||||
return {
|
||||
credentialId: row.credential_id,
|
||||
publicKey: row.public_key,
|
||||
userId: row.user_id,
|
||||
username: row.username,
|
||||
counter: row.counter,
|
||||
createdAt: new Date(row.created_at).getTime(),
|
||||
lastUsed: row.last_used ? new Date(row.last_used).getTime() : undefined,
|
||||
transports: row.transports,
|
||||
};
|
||||
}
|
||||
|
||||
export async function updateCredentialUsage(credentialId: string, newCounter: number): Promise<void> {
|
||||
await sql`
|
||||
UPDATE credentials
|
||||
SET counter = ${newCounter}, last_used = NOW()
|
||||
WHERE credential_id = ${credentialId}
|
||||
`;
|
||||
}
|
||||
|
||||
export async function getUserCredentials(userId: string): Promise<StoredCredential[]> {
|
||||
const rows = await sql`
|
||||
SELECT c.credential_id, c.public_key, c.user_id, c.counter,
|
||||
c.transports, c.created_at, c.last_used, u.username
|
||||
FROM credentials c
|
||||
JOIN users u ON c.user_id = u.id
|
||||
WHERE c.user_id = ${userId}
|
||||
`;
|
||||
|
||||
return rows.map(row => ({
|
||||
credentialId: row.credential_id,
|
||||
publicKey: row.public_key,
|
||||
userId: row.user_id,
|
||||
username: row.username,
|
||||
counter: row.counter,
|
||||
createdAt: new Date(row.created_at).getTime(),
|
||||
lastUsed: row.last_used ? new Date(row.last_used).getTime() : undefined,
|
||||
transports: row.transports,
|
||||
}));
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// CHALLENGE OPERATIONS
|
||||
// ============================================================================
|
||||
|
||||
export async function storeChallenge(ch: StoredChallenge): Promise<void> {
|
||||
await sql`
|
||||
INSERT INTO challenges (challenge, user_id, type, created_at, expires_at)
|
||||
VALUES (
|
||||
${ch.challenge},
|
||||
${ch.userId || null},
|
||||
${ch.type},
|
||||
${new Date(ch.createdAt)},
|
||||
${new Date(ch.expiresAt)}
|
||||
)
|
||||
`;
|
||||
}
|
||||
|
||||
export async function getChallenge(challenge: string): Promise<StoredChallenge | null> {
|
||||
const rows = await sql`
|
||||
SELECT * FROM challenges WHERE challenge = ${challenge}
|
||||
`;
|
||||
if (rows.length === 0) return null;
|
||||
|
||||
const row = rows[0];
|
||||
return {
|
||||
challenge: row.challenge,
|
||||
userId: row.user_id || undefined,
|
||||
type: row.type as 'registration' | 'authentication',
|
||||
createdAt: new Date(row.created_at).getTime(),
|
||||
expiresAt: new Date(row.expires_at).getTime(),
|
||||
};
|
||||
}
|
||||
|
||||
export async function deleteChallenge(challenge: string): Promise<void> {
|
||||
await sql`DELETE FROM challenges WHERE challenge = ${challenge}`;
|
||||
}
|
||||
|
||||
export async function cleanExpiredChallenges(): Promise<number> {
|
||||
const result = await sql`DELETE FROM challenges WHERE expires_at < NOW()`;
|
||||
return result.count;
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// HEALTH CHECK
|
||||
// ============================================================================
|
||||
|
||||
export async function checkDatabaseHealth(): Promise<boolean> {
|
||||
try {
|
||||
await sql`SELECT 1`;
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
export { sql };
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
-- EncryptID PostgreSQL Schema
|
||||
-- Run once to initialize the database
|
||||
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id TEXT PRIMARY KEY,
|
||||
username TEXT UNIQUE NOT NULL,
|
||||
display_name TEXT,
|
||||
did TEXT,
|
||||
created_at TIMESTAMPTZ DEFAULT NOW()
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS credentials (
|
||||
credential_id TEXT PRIMARY KEY,
|
||||
user_id TEXT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
|
||||
public_key TEXT NOT NULL,
|
||||
counter INTEGER DEFAULT 0,
|
||||
transports TEXT[],
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
last_used TIMESTAMPTZ
|
||||
);
|
||||
|
||||
CREATE INDEX IF NOT EXISTS idx_credentials_user_id ON credentials(user_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS challenges (
|
||||
challenge TEXT PRIMARY KEY,
|
||||
user_id TEXT,
|
||||
type TEXT NOT NULL CHECK (type IN ('registration', 'authentication')),
|
||||
created_at TIMESTAMPTZ DEFAULT NOW(),
|
||||
expires_at TIMESTAMPTZ NOT NULL
|
||||
);
|
||||
|
||||
-- Auto-clean expired challenges (run periodically or use pg_cron)
|
||||
CREATE INDEX IF NOT EXISTS idx_challenges_expires_at ON challenges(expires_at);
|
||||
|
|
@ -3,6 +3,8 @@
|
|||
*
|
||||
* Handles WebAuthn registration/authentication, session management,
|
||||
* and serves the .well-known/webauthn configuration.
|
||||
*
|
||||
* Storage: PostgreSQL (via db.ts)
|
||||
*/
|
||||
|
||||
import { Hono } from 'hono';
|
||||
|
|
@ -10,6 +12,21 @@ import { cors } from 'hono/cors';
|
|||
import { logger } from 'hono/logger';
|
||||
import { serveStatic } from 'hono/bun';
|
||||
import { sign, verify } from 'hono/jwt';
|
||||
import {
|
||||
initDatabase,
|
||||
storeCredential,
|
||||
getCredential,
|
||||
updateCredentialUsage,
|
||||
getUserCredentials,
|
||||
storeChallenge,
|
||||
getChallenge,
|
||||
deleteChallenge,
|
||||
cleanExpiredChallenges,
|
||||
checkDatabaseHealth,
|
||||
createUser,
|
||||
type StoredCredential,
|
||||
type StoredChallenge,
|
||||
} from './db.js';
|
||||
|
||||
// ============================================================================
|
||||
// CONFIGURATION
|
||||
|
|
@ -35,34 +52,6 @@ const CONFIG = {
|
|||
],
|
||||
};
|
||||
|
||||
// ============================================================================
|
||||
// IN-MEMORY STORAGE (Replace with database in production)
|
||||
// ============================================================================
|
||||
|
||||
interface StoredCredential {
|
||||
credentialId: string;
|
||||
publicKey: string; // Base64url encoded
|
||||
userId: string;
|
||||
username: string;
|
||||
counter: number;
|
||||
createdAt: number;
|
||||
lastUsed?: number;
|
||||
transports?: string[];
|
||||
}
|
||||
|
||||
interface StoredChallenge {
|
||||
challenge: string;
|
||||
userId?: string;
|
||||
type: 'registration' | 'authentication';
|
||||
createdAt: number;
|
||||
expiresAt: number;
|
||||
}
|
||||
|
||||
// In-memory stores (replace with D1/PostgreSQL in production)
|
||||
const credentials = new Map<string, StoredCredential>();
|
||||
const challenges = new Map<string, StoredChallenge>();
|
||||
const userCredentials = new Map<string, string[]>(); // userId -> credentialIds
|
||||
|
||||
// ============================================================================
|
||||
// HONO APP
|
||||
// ============================================================================
|
||||
|
|
@ -89,9 +78,11 @@ app.get('/.well-known/webauthn', (c) => {
|
|||
});
|
||||
});
|
||||
|
||||
// Health check
|
||||
app.get('/health', (c) => {
|
||||
return c.json({ status: 'ok', service: 'encryptid', timestamp: Date.now() });
|
||||
// Health check — includes database connectivity
|
||||
app.get('/health', async (c) => {
|
||||
const dbHealthy = await checkDatabaseHealth();
|
||||
const status = dbHealthy ? 'ok' : 'degraded';
|
||||
return c.json({ status, service: 'encryptid', database: dbHealthy, timestamp: Date.now() }, dbHealthy ? 200 : 503);
|
||||
});
|
||||
|
||||
// ============================================================================
|
||||
|
|
@ -114,7 +105,7 @@ app.post('/api/register/start', async (c) => {
|
|||
// Generate user ID
|
||||
const userId = Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString('base64url');
|
||||
|
||||
// Store challenge
|
||||
// Store challenge in database
|
||||
const challengeRecord: StoredChallenge = {
|
||||
challenge,
|
||||
userId,
|
||||
|
|
@ -122,7 +113,7 @@ app.post('/api/register/start', async (c) => {
|
|||
createdAt: Date.now(),
|
||||
expiresAt: Date.now() + 5 * 60 * 1000, // 5 minutes
|
||||
};
|
||||
challenges.set(challenge, challengeRecord);
|
||||
await storeChallenge(challengeRecord);
|
||||
|
||||
// Build registration options
|
||||
const options = {
|
||||
|
|
@ -162,20 +153,23 @@ app.post('/api/register/complete', async (c) => {
|
|||
const { challenge, credential, userId, username } = await c.req.json();
|
||||
|
||||
// Verify challenge
|
||||
const challengeRecord = challenges.get(challenge);
|
||||
const challengeRecord = await getChallenge(challenge);
|
||||
if (!challengeRecord || challengeRecord.type !== 'registration') {
|
||||
return c.json({ error: 'Invalid challenge' }, 400);
|
||||
}
|
||||
if (Date.now() > challengeRecord.expiresAt) {
|
||||
challenges.delete(challenge);
|
||||
await deleteChallenge(challenge);
|
||||
return c.json({ error: 'Challenge expired' }, 400);
|
||||
}
|
||||
challenges.delete(challenge);
|
||||
await deleteChallenge(challenge);
|
||||
|
||||
// In production, verify the attestation properly
|
||||
// For now, we trust the client-side verification
|
||||
|
||||
// Store credential
|
||||
// Create user and store credential in database
|
||||
const did = `did:key:${userId.slice(0, 32)}`;
|
||||
await createUser(userId, username, username, did);
|
||||
|
||||
const storedCredential: StoredCredential = {
|
||||
credentialId: credential.credentialId,
|
||||
publicKey: credential.publicKey,
|
||||
|
|
@ -185,13 +179,7 @@ app.post('/api/register/complete', async (c) => {
|
|||
createdAt: Date.now(),
|
||||
transports: credential.transports,
|
||||
};
|
||||
|
||||
credentials.set(credential.credentialId, storedCredential);
|
||||
|
||||
// Map user to credential
|
||||
const userCreds = userCredentials.get(userId) || [];
|
||||
userCreds.push(credential.credentialId);
|
||||
userCredentials.set(userId, userCreds);
|
||||
await storeCredential(storedCredential);
|
||||
|
||||
console.log('EncryptID: Credential registered', {
|
||||
credentialId: credential.credentialId.slice(0, 20) + '...',
|
||||
|
|
@ -205,7 +193,7 @@ app.post('/api/register/complete', async (c) => {
|
|||
success: true,
|
||||
userId,
|
||||
token,
|
||||
did: `did:key:${userId.slice(0, 32)}`, // Simplified DID
|
||||
did,
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -223,19 +211,19 @@ app.post('/api/auth/start', async (c) => {
|
|||
// Generate challenge
|
||||
const challenge = Buffer.from(crypto.getRandomValues(new Uint8Array(32))).toString('base64url');
|
||||
|
||||
// Store challenge
|
||||
// Store challenge in database
|
||||
const challengeRecord: StoredChallenge = {
|
||||
challenge,
|
||||
type: 'authentication',
|
||||
createdAt: Date.now(),
|
||||
expiresAt: Date.now() + 5 * 60 * 1000,
|
||||
};
|
||||
challenges.set(challenge, challengeRecord);
|
||||
await storeChallenge(challengeRecord);
|
||||
|
||||
// Build allowed credentials if specified
|
||||
let allowCredentials;
|
||||
if (credentialId) {
|
||||
const cred = credentials.get(credentialId);
|
||||
const cred = await getCredential(credentialId);
|
||||
if (cred) {
|
||||
allowCredentials = [{
|
||||
type: 'public-key',
|
||||
|
|
@ -262,19 +250,19 @@ app.post('/api/auth/start', async (c) => {
|
|||
app.post('/api/auth/complete', async (c) => {
|
||||
const { challenge, credential } = await c.req.json();
|
||||
|
||||
// Verify challenge
|
||||
const challengeRecord = challenges.get(challenge);
|
||||
// Verify challenge from database
|
||||
const challengeRecord = await getChallenge(challenge);
|
||||
if (!challengeRecord || challengeRecord.type !== 'authentication') {
|
||||
return c.json({ error: 'Invalid challenge' }, 400);
|
||||
}
|
||||
if (Date.now() > challengeRecord.expiresAt) {
|
||||
challenges.delete(challenge);
|
||||
await deleteChallenge(challenge);
|
||||
return c.json({ error: 'Challenge expired' }, 400);
|
||||
}
|
||||
challenges.delete(challenge);
|
||||
await deleteChallenge(challenge);
|
||||
|
||||
// Look up credential
|
||||
const storedCredential = credentials.get(credential.credentialId);
|
||||
// Look up credential from database
|
||||
const storedCredential = await getCredential(credential.credentialId);
|
||||
if (!storedCredential) {
|
||||
return c.json({ error: 'Unknown credential' }, 400);
|
||||
}
|
||||
|
|
@ -282,9 +270,8 @@ app.post('/api/auth/complete', async (c) => {
|
|||
// In production, verify signature against stored public key
|
||||
// For now, we trust the client-side verification
|
||||
|
||||
// Update counter and last used
|
||||
storedCredential.counter++;
|
||||
storedCredential.lastUsed = Date.now();
|
||||
// Update counter and last used in database
|
||||
await updateCredentialUsage(credential.credentialId, storedCredential.counter + 1);
|
||||
|
||||
console.log('EncryptID: Authentication successful', {
|
||||
credentialId: credential.credentialId.slice(0, 20) + '...',
|
||||
|
|
@ -311,7 +298,7 @@ app.post('/api/auth/complete', async (c) => {
|
|||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Verify session token
|
||||
* Verify session token (supports both GET with Authorization header and POST with body)
|
||||
*/
|
||||
app.get('/api/session/verify', async (c) => {
|
||||
const authHeader = c.req.header('Authorization');
|
||||
|
|
@ -335,6 +322,27 @@ app.get('/api/session/verify', async (c) => {
|
|||
}
|
||||
});
|
||||
|
||||
app.post('/api/session/verify', async (c) => {
|
||||
const { token } = await c.req.json();
|
||||
if (!token) {
|
||||
return c.json({ valid: false, error: 'No token' }, 400);
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = await verify(token, CONFIG.jwtSecret);
|
||||
return c.json({
|
||||
valid: true,
|
||||
claims: payload,
|
||||
userId: payload.sub,
|
||||
username: payload.username,
|
||||
did: payload.did,
|
||||
exp: payload.exp,
|
||||
});
|
||||
} catch {
|
||||
return c.json({ valid: false, error: 'Invalid token' });
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Refresh session token
|
||||
*/
|
||||
|
|
@ -380,17 +388,13 @@ app.get('/api/user/credentials', async (c) => {
|
|||
const payload = await verify(token, CONFIG.jwtSecret);
|
||||
const userId = payload.sub as string;
|
||||
|
||||
const userCreds = userCredentials.get(userId) || [];
|
||||
const credentialList = userCreds.map(credId => {
|
||||
const cred = credentials.get(credId);
|
||||
if (!cred) return null;
|
||||
return {
|
||||
credentialId: cred.credentialId,
|
||||
createdAt: cred.createdAt,
|
||||
lastUsed: cred.lastUsed,
|
||||
transports: cred.transports,
|
||||
};
|
||||
}).filter(Boolean);
|
||||
const creds = await getUserCredentials(userId);
|
||||
const credentialList = creds.map(cred => ({
|
||||
credentialId: cred.credentialId,
|
||||
createdAt: cred.createdAt,
|
||||
lastUsed: cred.lastUsed,
|
||||
transports: cred.transports,
|
||||
}));
|
||||
|
||||
return c.json({ credentials: credentialList });
|
||||
} catch {
|
||||
|
|
@ -594,9 +598,20 @@ app.get('/', (c) => {
|
|||
});
|
||||
|
||||
// ============================================================================
|
||||
// START SERVER
|
||||
// DATABASE INITIALIZATION & SERVER START
|
||||
// ============================================================================
|
||||
|
||||
// Initialize database on startup
|
||||
initDatabase().catch(err => {
|
||||
console.error('EncryptID: Failed to initialize database', err);
|
||||
process.exit(1);
|
||||
});
|
||||
|
||||
// Clean expired challenges every 10 minutes
|
||||
setInterval(() => {
|
||||
cleanExpiredChallenges().catch(() => {});
|
||||
}, 10 * 60 * 1000);
|
||||
|
||||
console.log(`
|
||||
╔═══════════════════════════════════════════════════════════╗
|
||||
║ ║
|
||||
|
|
@ -606,6 +621,7 @@ console.log(`
|
|||
║ ║
|
||||
║ Port: ${CONFIG.port} ║
|
||||
║ RP ID: ${CONFIG.rpId} ║
|
||||
║ Storage: PostgreSQL ║
|
||||
║ ║
|
||||
╚═══════════════════════════════════════════════════════════╝
|
||||
`);
|
||||
|
|
|
|||
Loading…
Reference in New Issue