Files
timetracker/backend/src/middleware/auth.ts
simon.franken 64211e6a49 feat: add MCP endpoint and API key management
- Add ApiKey Prisma model (SHA-256 hash, prefix, lastUsedAt) with migration
- Implement ApiKeyService (create, list, delete, verify)
- Extend requireAuth middleware to accept sk_-prefixed API keys alongside JWTs
- Add GET/POST /api-keys routes for creating and revoking keys
- Add stateless Streamable HTTP MCP server at POST/GET /mcp exposing all 20
  time-tracking tools (clients, projects, time entries, timer, statistics,
  client targets and corrections)
- Frontend: ApiKey types, apiKeys API module, useApiKeys hook
- Frontend: ApiKeysPage with key table, one-time raw-key reveal modal, and
  inline revoke confirmation
- Wire /api-keys route and add API Keys link to Management dropdown in Navbar
2026-03-16 15:26:09 +01:00

97 lines
3.0 KiB
TypeScript

import { Request, Response, NextFunction } from 'express';
import { prisma } from '../prisma/client';
import type { AuthenticatedRequest, AuthenticatedUser } from '../types';
import { verifyBackendJwt } from '../auth/jwt';
import { ApiKeyService } from '../services/apiKey.service';
const apiKeyService = new ApiKeyService();
export async function requireAuth(
req: AuthenticatedRequest,
res: Response,
next: NextFunction
): Promise<void> {
const tag = `[requireAuth] ${req.method} ${req.path}`;
// 1. Session-based auth (web frontend)
if (req.session?.user) {
console.log(`${tag} -> session auth OK (user: ${req.session.user.id})`);
req.user = req.session.user as AuthenticatedUser;
return next();
}
// 2. Bearer token auth (JWT or API key)
const authHeader = req.headers.authorization;
if (authHeader?.startsWith('Bearer ')) {
const token = authHeader.slice(7);
console.log(`${tag} -> Bearer token present (first 20 chars: ${token.slice(0, 20)}…)`);
// 2a. API key — detected by the "sk_" prefix
if (token.startsWith('sk_')) {
try {
const user = await apiKeyService.verify(token);
if (!user) {
console.warn(`${tag} -> API key verification failed: key not found`);
res.status(401).json({ error: 'Unauthorized: invalid API key' });
return;
}
req.user = user;
console.log(`${tag} -> API key auth OK (user: ${req.user.id})`);
return next();
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
console.warn(`${tag} -> API key verification error: ${message}`);
res.status(401).json({ error: `Unauthorized: ${message}` });
return;
}
}
// 2b. JWT (iOS / native clients)
try {
req.user = verifyBackendJwt(token);
console.log(`${tag} -> JWT auth OK (user: ${req.user.id})`);
return next();
} catch (err) {
const message = err instanceof Error ? err.message : String(err);
console.warn(`${tag} -> JWT verification failed: ${message}`);
res.status(401).json({ error: `Unauthorized: ${message}` });
return;
}
}
if (authHeader) {
console.warn(`${tag} -> Authorization header present but not a Bearer token: "${authHeader.slice(0, 30)}…"`);
} else {
console.warn(`${tag} -> No session and no Authorization header`);
}
res.status(401).json({ error: 'Unauthorized' });
}
export function optionalAuth(
req: AuthenticatedRequest,
res: Response,
next: NextFunction
): void {
if (req.session?.user) {
req.user = req.session.user as AuthenticatedUser;
}
next();
}
export async function syncUser(user: AuthenticatedUser): Promise<void> {
await prisma.user.upsert({
where: { id: user.id },
update: {
username: user.username,
fullName: user.fullName,
email: user.email,
},
create: {
id: user.id,
username: user.username,
fullName: user.fullName,
email: user.email,
},
});
}