diff --git a/apps/api/src/middleware/auth.ts b/apps/api/src/middleware/auth.ts index 8655910..9090f35 100644 --- a/apps/api/src/middleware/auth.ts +++ b/apps/api/src/middleware/auth.ts @@ -3,9 +3,10 @@ import type {NextFunction, Request, Response} from 'express'; import jsonwebtoken from 'jsonwebtoken'; import {JWT_SECRET, PLUNK_ENABLED} from '../app/constants.js'; -import {prisma} from '../database/prisma.js'; import {ErrorCode, HttpException, NotAuthenticated} from '../exceptions/index.js'; import {MembershipService} from '../services/MembershipService.js'; +import {ProjectService} from '../services/ProjectService.js'; +import {UserService} from '../services/UserService.js'; export interface AuthResponse { type: 'jwt' | 'apiKey'; @@ -81,65 +82,6 @@ export function parseJwt(request: Request): string { return id; } -/** - * Middleware to require project access - * Validates that the user is authenticated and has access to the project specified in X-Project-Id header - * @param req - * @param res - * @param next - */ -export const requireProjectAccess = async (req: Request, res: Response, next: NextFunction) => { - try { - // First authenticate the user - const userId = parseJwt(req); - - // Get project ID from header - const projectId = req.headers['x-project-id'] as string | undefined; - - if (!projectId) { - throw new HttpException(400, 'Project ID is required in X-Project-Id header', ErrorCode.BAD_REQUEST); - } - - // Verify user has access to this project and get project status - const [membership, project] = await Promise.all([ - MembershipService.getMembership(userId, projectId), - prisma.project.findUnique({ - where: {id: projectId}, - select: {disabled: true}, - }), - ]); - - if (!membership) { - throw new HttpException(403, 'You do not have access to this project', ErrorCode.PROJECT_ACCESS_DENIED); - } - - // Set auth response with project ID (before disabled check so it's available for logging) - res.locals.auth = { - type: 'jwt', - userId, - projectId, - } as AuthResponse; - - // Check if project is disabled - block write operations - if (project?.disabled) { - const method = req.method.toUpperCase(); - const isWriteOperation = ['POST', 'PUT', 'PATCH', 'DELETE'].includes(method); - - if (isWriteOperation) { - throw new HttpException( - 403, - 'Project is disabled due to security violations. All write operations are blocked.', - ErrorCode.PROJECT_DISABLED, - ); - } - } - - next(); - } catch (error) { - next(error); - } -}; - /** * Middleware to require public API key authentication (for /v1/track endpoint only) * Validates that the request has a valid public key and sets the project @@ -168,12 +110,12 @@ export const requirePublicKey = async (req: Request, res: Response, next: NextFu const apiKey = parts[1]; + if (!apiKey) { + throw new HttpException(401, 'API key is required in Authorization header', ErrorCode.MISSING_AUTH); + } + // Look up project by public key only - const project = await prisma.project.findFirst({ - where: { - public: apiKey, - }, - }); + const project = await ProjectService.public(apiKey); if (!project) { throw new HttpException( @@ -237,12 +179,12 @@ export const requireSecretKey = async (req: Request, res: Response, next: NextFu const apiKey = parts[1]; + if (!apiKey) { + throw new HttpException(401, 'API key is required in Authorization header', ErrorCode.MISSING_AUTH); + } + // Look up project by secret key only - const project = await prisma.project.findFirst({ - where: { - secret: apiKey, - }, - }); + const project = await ProjectService.secret(apiKey); if (!project) { throw new HttpException( @@ -304,12 +246,12 @@ export const requireAuth = async (req: Request, res: Response, next: NextFunctio } const apiKey = parts[1]; + + if (!apiKey) { + throw new HttpException(401, 'API key is required in Authorization header', ErrorCode.MISSING_AUTH); + } // Look up project by secret key only (public keys not allowed) - const project = await prisma.project.findFirst({ - where: { - secret: apiKey, - }, - }); + const project = await ProjectService.secret(apiKey); if (!project) { throw new HttpException( @@ -355,10 +297,7 @@ export const requireAuth = async (req: Request, res: Response, next: NextFunctio // Verify user has access to this project and get project status const [membership, project] = await Promise.all([ MembershipService.getMembership(userId, projectId), - prisma.project.findUnique({ - where: {id: projectId}, - select: {disabled: true}, - }), + ProjectService.id(projectId), ]); if (!membership) { @@ -411,10 +350,7 @@ export const requireEmailVerified = async (req: Request, res: Response, next: Ne throw new NotAuthenticated(); } - const user = await prisma.user.findUnique({ - where: {id: auth.userId}, - select: {emailVerified: true, type: true}, - }); + const user = await UserService.id(auth.userId); if (!user) { throw new NotAuthenticated(); diff --git a/apps/api/src/services/ProjectService.ts b/apps/api/src/services/ProjectService.ts new file mode 100644 index 0000000..231867e --- /dev/null +++ b/apps/api/src/services/ProjectService.ts @@ -0,0 +1,31 @@ +import {Keys} from './keys.js'; +import {wrapRedis} from '../database/redis.js'; +import {prisma} from '../database/prisma.js'; + +export class ProjectService { + public static async id(id: string) { + return wrapRedis(Keys.Project.id(id), async () => { + return prisma.project.findUnique({where: {id}}); + }); + } + + public static async secret(key: string) { + return wrapRedis(Keys.Project.secret(key), async () => { + return prisma.project.findUnique({ + where: { + secret: key, + }, + }); + }); + } + + public static async public(key: string) { + return wrapRedis(Keys.Project.public(key), async () => { + return prisma.project.findUnique({ + where: { + public: key, + }, + }); + }); + } +} diff --git a/apps/api/src/services/keys.ts b/apps/api/src/services/keys.ts index 39e0691..1f1758e 100644 --- a/apps/api/src/services/keys.ts +++ b/apps/api/src/services/keys.ts @@ -84,4 +84,15 @@ export const Keys = { return `membership:owner:${projectId}`; }, }, + Project: { + id(id: string): string { + return `project:id:${id}`; + }, + secret(key: string): string { + return `project:secret:${key}`; + }, + public(key: string): string { + return `project:public:${key}`; + }, + }, } as const;