chore: Rely more on services in auth middleware
This commit is contained in:
@@ -3,9 +3,10 @@ import type {NextFunction, Request, Response} from 'express';
|
|||||||
import jsonwebtoken from 'jsonwebtoken';
|
import jsonwebtoken from 'jsonwebtoken';
|
||||||
|
|
||||||
import {JWT_SECRET, PLUNK_ENABLED} from '../app/constants.js';
|
import {JWT_SECRET, PLUNK_ENABLED} from '../app/constants.js';
|
||||||
import {prisma} from '../database/prisma.js';
|
|
||||||
import {ErrorCode, HttpException, NotAuthenticated} from '../exceptions/index.js';
|
import {ErrorCode, HttpException, NotAuthenticated} from '../exceptions/index.js';
|
||||||
import {MembershipService} from '../services/MembershipService.js';
|
import {MembershipService} from '../services/MembershipService.js';
|
||||||
|
import {ProjectService} from '../services/ProjectService.js';
|
||||||
|
import {UserService} from '../services/UserService.js';
|
||||||
|
|
||||||
export interface AuthResponse {
|
export interface AuthResponse {
|
||||||
type: 'jwt' | 'apiKey';
|
type: 'jwt' | 'apiKey';
|
||||||
@@ -81,65 +82,6 @@ export function parseJwt(request: Request): string {
|
|||||||
return id;
|
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)
|
* 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
|
* 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];
|
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
|
// Look up project by public key only
|
||||||
const project = await prisma.project.findFirst({
|
const project = await ProjectService.public(apiKey);
|
||||||
where: {
|
|
||||||
public: apiKey,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!project) {
|
if (!project) {
|
||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
@@ -237,12 +179,12 @@ export const requireSecretKey = async (req: Request, res: Response, next: NextFu
|
|||||||
|
|
||||||
const apiKey = parts[1];
|
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
|
// Look up project by secret key only
|
||||||
const project = await prisma.project.findFirst({
|
const project = await ProjectService.secret(apiKey);
|
||||||
where: {
|
|
||||||
secret: apiKey,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!project) {
|
if (!project) {
|
||||||
throw new HttpException(
|
throw new HttpException(
|
||||||
@@ -304,12 +246,12 @@ export const requireAuth = async (req: Request, res: Response, next: NextFunctio
|
|||||||
}
|
}
|
||||||
|
|
||||||
const apiKey = parts[1];
|
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)
|
// Look up project by secret key only (public keys not allowed)
|
||||||
const project = await prisma.project.findFirst({
|
const project = await ProjectService.secret(apiKey);
|
||||||
where: {
|
|
||||||
secret: apiKey,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!project) {
|
if (!project) {
|
||||||
throw new HttpException(
|
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
|
// Verify user has access to this project and get project status
|
||||||
const [membership, project] = await Promise.all([
|
const [membership, project] = await Promise.all([
|
||||||
MembershipService.getMembership(userId, projectId),
|
MembershipService.getMembership(userId, projectId),
|
||||||
prisma.project.findUnique({
|
ProjectService.id(projectId),
|
||||||
where: {id: projectId},
|
|
||||||
select: {disabled: true},
|
|
||||||
}),
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
if (!membership) {
|
if (!membership) {
|
||||||
@@ -411,10 +350,7 @@ export const requireEmailVerified = async (req: Request, res: Response, next: Ne
|
|||||||
throw new NotAuthenticated();
|
throw new NotAuthenticated();
|
||||||
}
|
}
|
||||||
|
|
||||||
const user = await prisma.user.findUnique({
|
const user = await UserService.id(auth.userId);
|
||||||
where: {id: auth.userId},
|
|
||||||
select: {emailVerified: true, type: true},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new NotAuthenticated();
|
throw new NotAuthenticated();
|
||||||
|
|||||||
@@ -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,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -84,4 +84,15 @@ export const Keys = {
|
|||||||
return `membership:owner:${projectId}`;
|
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;
|
} as const;
|
||||||
|
|||||||
Reference in New Issue
Block a user