feat: add scoped API keys for programmatic site access
Introduce ApiKey model, CRUD endpoints, and admin UI so agents can authenticate with permission-scoped keys. Normalize pubkeys to hex on login, dedupe legacy npub/hex user rows, and ignore .cursor in git. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import jwt from 'jsonwebtoken';
|
||||
import { authService, type ResolvedAccess } from '../services/auth';
|
||||
import { looksLikeApiKey, resolveApiKey } from '../services/apiKeys';
|
||||
|
||||
const JWT_SECRET = process.env.JWT_SECRET || 'change-me-in-production';
|
||||
|
||||
@@ -14,11 +15,12 @@ declare global {
|
||||
interface Request {
|
||||
user?: AuthPayload;
|
||||
access?: ResolvedAccess;
|
||||
isApiKey?: boolean;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function requireAuth(req: Request, res: Response, next: NextFunction): void {
|
||||
export async function requireAuth(req: Request, res: Response, next: NextFunction): Promise<void> {
|
||||
const header = req.headers.authorization;
|
||||
if (!header || !header.startsWith('Bearer ')) {
|
||||
res.status(401).json({ error: 'Missing or invalid authorization header' });
|
||||
@@ -26,6 +28,22 @@ export function requireAuth(req: Request, res: Response, next: NextFunction): vo
|
||||
}
|
||||
|
||||
const token = header.slice(7);
|
||||
|
||||
// API keys authenticate programmatic clients. They carry their own scoped
|
||||
// permission set rather than a role, so we pre-resolve access here.
|
||||
if (looksLikeApiKey(token)) {
|
||||
const access = await resolveApiKey(token);
|
||||
if (!access) {
|
||||
res.status(401).json({ error: 'Invalid or revoked API key' });
|
||||
return;
|
||||
}
|
||||
req.user = { pubkey: access.pubkey, role: 'apikey' };
|
||||
req.access = access;
|
||||
req.isApiKey = true;
|
||||
next();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const payload = jwt.verify(token, JWT_SECRET) as AuthPayload;
|
||||
req.user = payload;
|
||||
@@ -45,8 +63,13 @@ export function requires(permission: string) {
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const access = await authService.resolveAccess(req.user.pubkey);
|
||||
req.access = access;
|
||||
// API key access is already resolved (and scoped) in requireAuth; for JWT
|
||||
// callers, resolve their live role-based access here.
|
||||
let access = req.access;
|
||||
if (!req.isApiKey || !access) {
|
||||
access = await authService.resolveAccess(req.user.pubkey);
|
||||
req.access = access;
|
||||
}
|
||||
if (access.isSuperAdmin || access.permissions.has(permission)) {
|
||||
next();
|
||||
return;
|
||||
|
||||
Reference in New Issue
Block a user