bugfix: a new flow added to disable staffs and free the used seats.

This commit is contained in:
2026-05-18 14:56:07 +03:30
parent 81fe14823f
commit 4487d3260b
5 changed files with 184 additions and 23 deletions

View File

@@ -756,7 +756,18 @@ export class AuthService {
throw new UnauthorizedException('Access denied to this organization');
}
if (!membership.isOwner && !membership.isActive) {
throw new UnauthorizedException('Your invitation is still pending activation');
const acceptedInvite = await this.prisma.staffInvitation.findFirst({
where: {
membershipId: membership.id,
acceptedAt: { not: null },
},
select: { id: true },
});
throw new UnauthorizedException(
acceptedInvite
? 'Your access to this organization has been disabled.'
: 'Your invitation is still pending activation.',
);
}
// 2. Build payload WITH org context

View File

@@ -80,6 +80,19 @@ export class StaffController {
return this.staffService.updateMember(req.user.id, organizationId, membershipId, dto);
}
@Patch('members/:membershipId/disable')
@UseGuards(JwtAuthGuard)
@ApiOperation({
summary: 'Disable staff member (frees a seat; member cannot access this organization)',
})
disableMember(
@Req() req: { user: { id: string; organizationId?: string } },
@Param('membershipId') membershipId: string,
) {
const organizationId = this.staffService.getOrganizationIdFromUser(req.user);
return this.staffService.disableMember(req.user.id, organizationId, membershipId);
}
@Delete('members/:membershipId')
@UseGuards(JwtAuthGuard)
@ApiOperation({ summary: 'Remove staff member from organization' })

View File

@@ -399,6 +399,44 @@ export class StaffService {
return { success: true, message: 'Member updated' };
}
async disableMember(actorUserId: string, organizationId: string, membershipId: string) {
const actor = await this.getActorMembership(actorUserId, organizationId);
if (!actor || !this.canEditStaff(actor)) {
throw new ForbiddenException('You cannot manage staff');
}
const target = await this.prisma.membership.findFirst({
where: { id: membershipId, organizationId },
});
if (!target) {
throw new NotFoundException('Member not found');
}
if (target.isOwner) {
throw new ForbiddenException('Cannot disable the organization owner');
}
if (actorUserId === target.userId) {
throw new BadRequestException('You cannot disable your own access');
}
if (!target.isActive) {
throw new BadRequestException('This member is already disabled or pending activation');
}
await this.prisma.membership.update({
where: { id: membershipId },
data: { isActive: false },
});
await this.prisma.session.deleteMany({
where: { userId: target.userId },
});
return {
success: true,
message: 'Member disabled. Their seat is now available for another invite.',
};
}
async removeMember(actorUserId: string, organizationId: string, membershipId: string) {
const actor = await this.getActorMembership(actorUserId, organizationId);
if (!actor || !this.canEditStaff(actor)) {
@@ -435,11 +473,12 @@ export class StaffService {
isOwner: boolean;
isActive: boolean;
invitations: { acceptedAt: Date | null; revokedAt: Date | null; expiresAt: Date }[];
}): 'ACTIVE' | 'PENDING' | 'EXPIRED' {
if (m.isOwner || m.isActive) return 'ACTIVE';
}): 'ACTIVE' | 'PENDING' | 'EXPIRED' | 'DISABLED' {
if (m.isOwner) return 'ACTIVE';
if (m.isActive) return 'ACTIVE';
const invitation = m.invitations[0];
if (!invitation) return 'EXPIRED';
if (invitation.acceptedAt) return 'ACTIVE';
if (invitation?.acceptedAt) return 'DISABLED';
if (!invitation) return 'DISABLED';
if (invitation.revokedAt) return 'EXPIRED';
return invitation.expiresAt.getTime() > Date.now() ? 'PENDING' : 'EXPIRED';
}