chore: Consolidate membership checks in single service

This commit is contained in:
Dries Augustyns
2025-12-31 16:20:17 +01:00
parent 19554e6e8f
commit be2eb57369
10 changed files with 484 additions and 434 deletions
+5 -46
View File
@@ -8,6 +8,7 @@ import type {AuthResponse} from '../middleware/auth.js';
import {isAuthenticated, requireEmailVerified} from '../middleware/auth.js';
import {DomainService} from '../services/DomainService.js';
import {Keys} from '../services/keys.js';
import {MembershipService} from '../services/MembershipService.js';
import {prisma} from '../database/prisma.js';
import {CatchAsync} from '../utils/asyncHandler.js';
@@ -24,16 +25,7 @@ export class Domains {
const {projectId} = DomainSchemas.projectId.parse(req.params);
// Verify user has access to this project
const membership = await prisma.membership.findFirst({
where: {
userId: auth.userId,
projectId,
},
});
if (!membership) {
throw new NotFound('Project not found or you do not have access');
}
await MembershipService.requireAccess(auth.userId!, projectId);
const domains = await DomainService.getProjectDomains(projectId);
@@ -55,19 +47,7 @@ export class Domains {
}
// Verify user has admin access to this project
const membership = await prisma.membership.findFirst({
where: {
userId: auth.userId,
projectId,
role: {
in: ['ADMIN', 'OWNER'],
},
},
});
if (!membership) {
throw new NotFound('Project not found or you do not have permission');
}
await MembershipService.requireAdminAccess(auth.userId!, projectId);
// Check if domain is already linked to another project
const ownershipCheck = await DomainService.checkDomainOwnership(domain, auth.userId);
@@ -117,16 +97,7 @@ export class Domains {
}
// Verify user has access to the project this domain belongs to
const membership = await prisma.membership.findFirst({
where: {
userId: auth.userId,
projectId: domain.projectId,
},
});
if (!membership) {
throw new NotFound('Domain not found or you do not have access');
}
await MembershipService.requireAccess(auth.userId!, domain.projectId);
const verificationStatus = await DomainService.checkVerification(id);
@@ -154,19 +125,7 @@ export class Domains {
}
// Verify user has admin access to the project this domain belongs to
const membership = await prisma.membership.findFirst({
where: {
userId: auth.userId,
projectId: domain.projectId,
role: {
in: ['ADMIN', 'OWNER'],
},
},
});
if (!membership) {
throw new NotFound('Domain not found or you do not have permission');
}
await MembershipService.requireAdminAccess(auth.userId!, domain.projectId);
await DomainService.removeDomain(id);
+15 -166
View File
@@ -6,6 +6,7 @@ import {prisma} from '../database/prisma.js';
import {HttpException} from '../exceptions/index.js';
import type {AuthResponse} from '../middleware/auth.js';
import {requireAuth, requireEmailVerified} from '../middleware/auth.js';
import {MembershipService} from '../services/MembershipService.js';
import {SecurityService} from '../services/SecurityService.js';
import {CatchAsync} from '../utils/asyncHandler.js';
@@ -23,16 +24,7 @@ export class Projects {
const {id} = UtilitySchemas.id.parse(req.params);
// Verify user has access to this project
const membership = await prisma.membership.findFirst({
where: {
userId: auth.userId,
projectId: id,
},
});
if (!membership) {
throw new HttpException(404, 'Project not found or you do not have access');
}
await MembershipService.requireAccess(auth.userId!, id);
// Get project with relevant data
const project = await prisma.project.findUnique({
@@ -96,16 +88,7 @@ export class Projects {
const {id} = UtilitySchemas.id.parse(req.params);
// Verify user has access to this project
const membership = await prisma.membership.findFirst({
where: {
userId: auth.userId,
projectId: id,
},
});
if (!membership) {
throw new HttpException(404, 'Project not found or you do not have access');
}
await MembershipService.requireAccess(auth.userId!, id);
// Use existing SecurityService
const metrics = await SecurityService.getProjectSecurityMetrics(id);
@@ -128,39 +111,14 @@ export class Projects {
const {id} = UtilitySchemas.id.parse(req.params);
// Verify user has access to this project
const membership = await prisma.membership.findFirst({
where: {
userId: auth.userId,
projectId: id,
},
});
if (!membership) {
throw new HttpException(404, 'Project not found or you do not have access');
}
await MembershipService.requireAccess(auth.userId!, id);
// Get all members of the project
const members = await prisma.membership.findMany({
where: {
projectId: id,
},
include: {
user: {
select: {
id: true,
email: true,
},
},
},
});
const members = await MembershipService.getMembers(id);
return res.json({
success: true,
data: members.map(m => ({
userId: m.user.id,
email: m.user.email,
role: m.role,
})),
data: members,
});
}
@@ -190,19 +148,7 @@ export class Projects {
const {email, role} = parseResult.data;
// Verify current user is ADMIN or OWNER
const currentMembership = await prisma.membership.findFirst({
where: {
userId: auth.userId,
projectId: id,
role: {
in: ['ADMIN', 'OWNER'],
},
},
});
if (!currentMembership) {
throw new HttpException(403, 'Only project admins and owners can add members');
}
await MembershipService.requireAdminAccess(auth.userId!, id);
// Find user by email
const userToAdd = await prisma.user.findUnique({
@@ -214,28 +160,8 @@ export class Projects {
throw new HttpException(404, 'User with this email does not have an account');
}
// Check if user is already a member
const existingMembership = await prisma.membership.findUnique({
where: {
userId_projectId: {
userId: userToAdd.id,
projectId: id,
},
},
});
if (existingMembership) {
throw new HttpException(409, 'User is already a member of this project');
}
// Create membership
const newMembership = await prisma.membership.create({
data: {
userId: userToAdd.id,
projectId: id,
role,
},
});
// Add member to project
const newMembership = await MembershipService.addMember(id, userToAdd.id, role);
return res.json({
success: true,
@@ -276,38 +202,7 @@ export class Projects {
const {role} = parseResult.data;
// Verify current user is ADMIN or OWNER
const currentMembership = await prisma.membership.findFirst({
where: {
userId: auth.userId,
projectId: id,
role: {
in: ['ADMIN', 'OWNER'],
},
},
});
if (!currentMembership) {
throw new HttpException(403, 'Only project admins and owners can update member roles');
}
// Get target membership
const targetMembership = await prisma.membership.findUnique({
where: {
userId_projectId: {
userId,
projectId: id,
},
},
});
if (!targetMembership) {
throw new HttpException(404, 'Member not found');
}
// Cannot change OWNER role
if (targetMembership.role === 'OWNER') {
throw new HttpException(403, 'Cannot change the role of the project owner');
}
await MembershipService.requireAdminAccess(auth.userId!, id);
// Get user info
const user = await prisma.user.findUnique({
@@ -319,16 +214,8 @@ export class Projects {
throw new HttpException(404, 'User not found');
}
// Update role
await prisma.membership.update({
where: {
userId_projectId: {
userId,
projectId: id,
},
},
data: {role},
});
// Update role (service handles validation)
await MembershipService.updateRole(id, userId, role);
return res.json({
success: true,
@@ -360,53 +247,15 @@ export class Projects {
}
// Verify current user is ADMIN or OWNER
const currentMembership = await prisma.membership.findFirst({
where: {
userId: auth.userId,
projectId: id,
role: {
in: ['ADMIN', 'OWNER'],
},
},
});
if (!currentMembership) {
throw new HttpException(403, 'Only project admins and owners can remove members');
}
// Get target membership
const targetMembership = await prisma.membership.findUnique({
where: {
userId_projectId: {
userId,
projectId: id,
},
},
});
if (!targetMembership) {
throw new HttpException(404, 'Member not found');
}
// Cannot remove OWNER
if (targetMembership.role === 'OWNER') {
throw new HttpException(403, 'Cannot remove the project owner');
}
await MembershipService.requireAdminAccess(auth.userId!, id);
// Cannot remove yourself
if (userId === auth.userId) {
throw new HttpException(403, 'You cannot remove yourself from the project');
}
// Delete membership
await prisma.membership.delete({
where: {
userId_projectId: {
userId,
projectId: id,
},
},
});
// Remove member (service handles validation)
await MembershipService.removeMember(id, userId);
return res.json({
success: true,
+15 -136
View File
@@ -11,6 +11,7 @@ import {ErrorCode, HttpException, NotAuthenticated, NotFound} from '../exception
import type {AuthResponse} from '../middleware/auth.js';
import {isAuthenticated, requireEmailVerified} from '../middleware/auth.js';
import {BillingLimitService} from '../services/BillingLimitService.js';
import {MembershipService} from '../services/MembershipService.js';
import {NtfyService} from '../services/NtfyService.js';
import {SecurityService} from '../services/SecurityService.js';
import {UserService} from '../services/UserService.js';
@@ -108,20 +109,8 @@ export class Users {
const {id} = UtilitySchemas.id.parse(req.params);
const data = ProjectSchemas.update.parse(req.body);
// Verify user has access to this project
const membership = await prisma.membership.findFirst({
where: {
userId: auth.userId,
projectId: id,
role: {
in: ['ADMIN', 'OWNER'],
},
},
});
if (!membership) {
throw new NotFound('Project not found or you do not have permission to update it');
}
// Verify user has admin/owner access to this project
await MembershipService.requireAdminAccess(auth.userId!, id);
// Update the project
const project = await prisma.project.update({
@@ -140,19 +129,7 @@ export class Users {
const {id} = UtilitySchemas.id.parse(req.params);
// Verify user has admin/owner access to this project
const membership = await prisma.membership.findFirst({
where: {
userId: auth.userId,
projectId: id,
role: {
in: ['ADMIN', 'OWNER'],
},
},
});
if (!membership) {
throw new NotFound('Project not found or you do not have permission to regenerate keys');
}
await MembershipService.requireAdminAccess(auth.userId!, id);
// Generate new unique API keys
const publicKey = `pk_${randomBytes(32).toString('hex')}`;
@@ -197,20 +174,8 @@ export class Users {
return res.status(404).json({error: 'Billing is not enabled'});
}
// Verify user has access to this project
const membership = await prisma.membership.findFirst({
where: {
userId: auth.userId,
projectId: id,
role: {
in: ['ADMIN', 'OWNER'],
},
},
});
if (!membership) {
throw new NotFound('Project not found or you do not have permission to manage billing');
}
// Verify user has admin/owner access to this project
await MembershipService.requireAdminAccess(auth.userId!, id);
// Get the project
const project = await prisma.project.findUnique({
@@ -288,20 +253,8 @@ export class Users {
return res.status(404).json({error: 'Billing is not enabled'});
}
// Verify user has access to this project
const membership = await prisma.membership.findFirst({
where: {
userId: auth.userId,
projectId: id,
role: {
in: ['ADMIN', 'OWNER'],
},
},
});
if (!membership) {
throw new NotFound('Project not found or you do not have permission to manage billing');
}
// Verify user has admin/owner access to this project
await MembershipService.requireAdminAccess(auth.userId!, id);
// Get the project
const project = await prisma.project.findUnique({
@@ -342,16 +295,7 @@ export class Users {
}
// Verify user has access to this project
const membership = await prisma.membership.findFirst({
where: {
userId: auth.userId,
projectId: id,
},
});
if (!membership) {
throw new NotFound('Project not found or you do not have permission to view billing limits');
}
await MembershipService.requireAccess(auth.userId!, id);
// Get billing limits and usage
const limitsAndUsage = await BillingLimitService.getLimitsAndUsage(id);
@@ -377,19 +321,7 @@ export class Users {
const data = BillingLimitSchemas.update.parse(req.body);
// Verify user has admin/owner access to this project
const membership = await prisma.membership.findFirst({
where: {
userId: auth.userId,
projectId: id,
role: {
in: ['ADMIN', 'OWNER'],
},
},
});
if (!membership) {
throw new NotFound('Project not found or you do not have permission to update billing limits');
}
await MembershipService.requireAdminAccess(auth.userId!, id);
// Get the project with current limits
const project = await prisma.project.findUnique({
@@ -459,16 +391,7 @@ export class Users {
}
// Verify user has access to this project
const membership = await prisma.membership.findFirst({
where: {
userId: auth.userId,
projectId: id,
},
});
if (!membership) {
throw new NotFound('Project not found or you do not have permission to view billing');
}
await MembershipService.requireAccess(auth.userId!, id);
const project = await prisma.project.findUnique({
where: {id},
@@ -585,16 +508,7 @@ export class Users {
}
// Verify user has access to this project
const membership = await prisma.membership.findFirst({
where: {
userId: auth.userId,
projectId: id,
},
});
if (!membership) {
throw new NotFound('Project not found or you do not have permission to view billing');
}
await MembershipService.requireAccess(auth.userId!, id);
// Get the project
const project = await prisma.project.findUnique({
@@ -668,16 +582,7 @@ export class Users {
}
// Verify user has access to this project
const membership = await prisma.membership.findFirst({
where: {
userId: auth.userId,
projectId: id,
},
});
if (!membership) {
throw new NotFound('Project not found or you do not have permission to view security metrics');
}
await MembershipService.requireAccess(auth.userId!, id);
// Get security metrics
const metrics = await SecurityService.getProjectSecurityMetrics(id);
@@ -701,19 +606,7 @@ export class Users {
}
// Verify user has admin/owner access to this project
const membership = await prisma.membership.findFirst({
where: {
userId: auth.userId,
projectId: id,
role: {
in: ['ADMIN', 'OWNER'],
},
},
});
if (!membership) {
throw new NotFound('Project not found or you do not have permission to reset it');
}
await MembershipService.requireAdminAccess(auth.userId!, id);
// Check if project is disabled - block reset operation
const isDisabled = await SecurityService.isProjectDisabled(id);
@@ -787,21 +680,7 @@ export class Users {
}
// Verify user has owner or admin access to this project
const membership = await prisma.membership.findFirst({
where: {
userId: auth.userId,
projectId: id,
role: {
in: ['OWNER', 'ADMIN'],
},
},
});
if (!membership) {
throw new NotFound(
'Project not found or you do not have permission to delete it. Only project owners and admins can delete projects.',
);
}
await MembershipService.requireAdminAccess(auth.userId!, id);
// Get project to check for active subscription and disabled status
const project = await prisma.project.findUnique({