Files
cannaiq/backend/src/auth/middleware.ts
2025-11-28 19:45:44 -07:00

151 lines
3.7 KiB
TypeScript
Executable File

import { Request, Response, NextFunction } from 'express';
import jwt from 'jsonwebtoken';
import bcrypt from 'bcrypt';
import { pool } from '../db/migrate';
const JWT_SECRET = process.env.JWT_SECRET || 'change_this_in_production';
export interface AuthUser {
id: number;
email: string;
role: string;
}
export interface AuthRequest extends Request {
user?: AuthUser;
apiToken?: {
id: number;
name: string;
rate_limit: number;
};
}
export function generateToken(user: AuthUser): string {
return jwt.sign(
{ id: user.id, email: user.email, role: user.role },
JWT_SECRET,
{ expiresIn: '7d' }
);
}
export function verifyToken(token: string): AuthUser | null {
try {
return jwt.verify(token, JWT_SECRET) as AuthUser;
} catch (error) {
return null;
}
}
export async function authenticateUser(email: string, password: string): Promise<AuthUser | null> {
const result = await pool.query(
'SELECT id, email, password_hash, role FROM users WHERE email = $1',
[email]
);
if (result.rows.length === 0) {
return null;
}
const user = result.rows[0];
const isValid = await bcrypt.compare(password, user.password_hash);
if (!isValid) {
return null;
}
return {
id: user.id,
email: user.email,
role: user.role
};
}
export async function authMiddleware(req: AuthRequest, res: Response, next: NextFunction) {
const authHeader = req.headers.authorization;
if (!authHeader || !authHeader.startsWith('Bearer ')) {
return res.status(401).json({ error: 'No token provided' });
}
const token = authHeader.substring(7);
// Try JWT first
const jwtUser = verifyToken(token);
if (jwtUser) {
req.user = jwtUser;
return next();
}
// If JWT fails, try API token
try {
const result = await pool.query(`
SELECT id, name, rate_limit, active, expires_at, allowed_endpoints
FROM api_tokens
WHERE token = $1
`, [token]);
if (result.rows.length === 0) {
return res.status(401).json({ error: 'Invalid token' });
}
const apiToken = result.rows[0];
// Check if token is active
if (!apiToken.active) {
return res.status(401).json({ error: 'Token is disabled' });
}
// Check if token is expired
if (apiToken.expires_at && new Date(apiToken.expires_at) < new Date()) {
return res.status(401).json({ error: 'Token has expired' });
}
// Check allowed endpoints
if (apiToken.allowed_endpoints && apiToken.allowed_endpoints.length > 0) {
const isAllowed = apiToken.allowed_endpoints.some((pattern: string) => {
// Simple wildcard matching
const regex = new RegExp('^' + pattern.replace('*', '.*') + '$');
return regex.test(req.path);
});
if (!isAllowed) {
return res.status(403).json({ error: 'Endpoint not allowed for this token' });
}
}
// Set API token on request for tracking
req.apiToken = {
id: apiToken.id,
name: apiToken.name,
rate_limit: apiToken.rate_limit
};
// Set a generic user for compatibility with existing code
req.user = {
id: apiToken.id,
email: `api-token-${apiToken.id}@system`,
role: 'api'
};
next();
} catch (error) {
console.error('Error verifying API token:', error);
return res.status(500).json({ error: 'Authentication failed' });
}
}
export function requireRole(...roles: string[]) {
return (req: AuthRequest, res: Response, next: NextFunction) => {
if (!req.user) {
return res.status(401).json({ error: 'Not authenticated' });
}
if (!roles.includes(req.user.role)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
}