143 lines
3.4 KiB
TypeScript
143 lines
3.4 KiB
TypeScript
import { Request, Response, NextFunction } from 'express';
|
|
import jwt from 'jsonwebtoken';
|
|
import jwksClient from 'jwks-rsa';
|
|
|
|
const OIDC_ISSUER = process.env.OIDC_ISSUER || 'http://localhost:9000/application/o/kaboot/';
|
|
const OIDC_JWKS_URI = process.env.OIDC_JWKS_URI || 'http://localhost:9000/application/o/kaboot/jwks/';
|
|
const OIDC_INTERNAL_JWKS_URI = process.env.OIDC_INTERNAL_JWKS_URI || OIDC_JWKS_URI;
|
|
|
|
const client = jwksClient({
|
|
jwksUri: OIDC_INTERNAL_JWKS_URI,
|
|
cache: true,
|
|
cacheMaxAge: 600000,
|
|
rateLimit: true,
|
|
jwksRequestsPerMinute: 10,
|
|
});
|
|
|
|
function getSigningKey(header: jwt.JwtHeader, callback: jwt.SigningKeyCallback): void {
|
|
if (!header.kid) {
|
|
callback(new Error('No kid in token header'));
|
|
return;
|
|
}
|
|
client.getSigningKey(header.kid, (err, key) => {
|
|
if (err) {
|
|
callback(err);
|
|
return;
|
|
}
|
|
const signingKey = key?.getPublicKey();
|
|
callback(null, signingKey);
|
|
});
|
|
}
|
|
|
|
export interface AuthenticatedUser {
|
|
sub: string;
|
|
preferred_username: string;
|
|
email?: string;
|
|
name?: string;
|
|
groups?: string[];
|
|
}
|
|
|
|
export interface AuthenticatedRequest extends Request {
|
|
user?: AuthenticatedUser;
|
|
}
|
|
|
|
export function requireAuth(
|
|
req: AuthenticatedRequest,
|
|
res: Response,
|
|
next: NextFunction
|
|
): void {
|
|
const authHeader = req.headers.authorization;
|
|
|
|
if (!authHeader?.startsWith('Bearer ')) {
|
|
res.status(401).json({ error: 'Missing or invalid authorization header' });
|
|
return;
|
|
}
|
|
|
|
const token = authHeader.slice(7);
|
|
|
|
jwt.verify(
|
|
token,
|
|
getSigningKey,
|
|
{
|
|
issuer: OIDC_ISSUER,
|
|
algorithms: ['RS256'],
|
|
},
|
|
(err, decoded) => {
|
|
if (err) {
|
|
console.error('Token verification failed:', err.message);
|
|
res.status(401).json({ error: 'Invalid token', details: err.message });
|
|
return;
|
|
}
|
|
|
|
const payload = decoded as jwt.JwtPayload;
|
|
req.user = {
|
|
sub: payload.sub!,
|
|
preferred_username: payload.preferred_username || payload.sub!,
|
|
email: payload.email,
|
|
name: payload.name,
|
|
groups: payload.groups || [],
|
|
};
|
|
|
|
next();
|
|
}
|
|
);
|
|
}
|
|
|
|
import { canGenerate, incrementGenerationCount, GENERATION_LIMIT } from '../services/stripe.js';
|
|
|
|
export interface AIAccessInfo {
|
|
accessType: 'group' | 'subscription' | 'none';
|
|
remaining?: number;
|
|
}
|
|
|
|
export function requireAIAccess(
|
|
req: AuthenticatedRequest,
|
|
res: Response,
|
|
next: NextFunction
|
|
): void {
|
|
if (!req.user) {
|
|
res.status(401).json({ error: 'Authentication required' });
|
|
return;
|
|
}
|
|
|
|
const groups = req.user.groups || [];
|
|
const result = canGenerate(req.user.sub, groups);
|
|
|
|
if (!result.allowed) {
|
|
res.status(403).json({
|
|
error: result.reason || 'AI access not granted for this account',
|
|
remaining: result.remaining,
|
|
});
|
|
return;
|
|
}
|
|
|
|
(req as any).aiAccessInfo = {
|
|
accessType: groups.includes('kaboot-ai-access') ? 'group' : (result.accessType || 'none'),
|
|
remaining: result.remaining,
|
|
} as AIAccessInfo;
|
|
|
|
next();
|
|
}
|
|
|
|
export function trackGeneration(
|
|
req: AuthenticatedRequest,
|
|
res: Response,
|
|
next: NextFunction
|
|
): void {
|
|
if (!req.user) {
|
|
next();
|
|
return;
|
|
}
|
|
|
|
const groups = req.user.groups || [];
|
|
if (groups.includes('kaboot-ai-access')) {
|
|
next();
|
|
return;
|
|
}
|
|
|
|
const newCount = incrementGenerationCount(req.user.sub);
|
|
const remaining = Math.max(0, GENERATION_LIMIT - newCount);
|
|
|
|
res.setHeader('X-Generations-Remaining', remaining.toString());
|
|
next();
|
|
}
|