Multi-environment and multi-user construction

This commit is contained in:
2026-09-15 21:37:58 +03:30
parent e61fad95c6
commit d3f97b9dc4
37 changed files with 1349 additions and 201 deletions
@@ -0,0 +1,8 @@
import { Module } from '@nestjs/common';
import { MembershipsService } from './memberships.service';
@Module({
providers: [MembershipsService],
exports: [MembershipsService],
})
export class MembershipsModule {}
@@ -0,0 +1,161 @@
import {
ForbiddenException,
Injectable,
NotFoundException,
UnauthorizedException,
} from '@nestjs/common';
import type { OrganizationMembershipDto, UserRole } from '@mento/shared';
import { Prisma } from '@prisma/client';
import { PrismaService } from '../prisma/prisma.service';
export type MembershipActor = {
userId: string;
organizationId: string;
role: UserRole;
};
@Injectable()
export class MembershipsService {
constructor(private readonly prisma: PrismaService) {}
async listActive(userId: string) {
return this.prisma.membership.findMany({
where: { userId, isActive: true },
include: {
organization: { select: { id: true, name: true } },
orgRole: { select: { name: true } },
},
orderBy: { createdAt: 'asc' },
});
}
async requireActive(
userId: string,
organizationId: string,
): Promise<MembershipActor> {
const row = await this.prisma.membership.findFirst({
where: { userId, organizationId, isActive: true },
select: { userId: true, organizationId: true, role: true },
});
if (!row) {
throw new ForbiddenException('به این محیط دسترسی ندارید');
}
return row;
}
toChoices(
rows: Array<{
organization: { id: string; name: string };
role: UserRole;
orgRole: { name: string } | null;
}>,
): OrganizationMembershipDto[] {
return rows.map((row) => ({
id: row.organization.id,
name: row.organization.name,
role: row.role,
orgRoleName: row.orgRole?.name,
}));
}
async createMembership(data: {
userId: string;
organizationId: string;
role: UserRole;
orgRoleId?: string | null;
commissionPercent?: number;
isActive?: boolean;
}) {
return this.prisma.membership.create({
data: {
userId: data.userId,
organizationId: data.organizationId,
role: data.role,
orgRoleId: data.orgRoleId ?? null,
isActive: data.isActive ?? true,
commissionPercent:
data.commissionPercent == null
? undefined
: new Prisma.Decimal(data.commissionPercent),
},
});
}
async findInOrg(organizationId: string, userId: string) {
return this.prisma.membership.findFirst({
where: { organizationId, userId },
include: {
user: true,
orgRole: { select: { id: true, name: true, color: true } },
},
});
}
async listInOrg(organizationId: string) {
return this.prisma.membership.findMany({
where: { organizationId },
include: {
user: true,
orgRole: { select: { id: true, name: true, color: true } },
},
orderBy: [{ user: { lastLoginAt: 'desc' } }, { user: { fullName: 'asc' } }],
});
}
async countActiveOwners(organizationId: string, exceptUserId?: string) {
return this.prisma.membership.count({
where: {
organizationId,
role: 'owner',
isActive: true,
...(exceptUserId ? { userId: { not: exceptUserId } } : {}),
},
});
}
async syncUserLastOrg(
userId: string,
organizationId: string,
role: UserRole,
orgRoleId?: string | null,
) {
await this.prisma.user.update({
where: { id: userId },
data: {
organizationId,
role,
...(orgRoleId !== undefined ? { orgRoleId } : {}),
},
});
}
async findUserInOrgById(organizationId: string, userId: string) {
const row = await this.findInOrg(organizationId, userId);
if (!row) throw new NotFoundException('کاربر پیدا نشد');
return row;
}
async findUserInOrgByIdentity(
organizationId: string,
where: { id?: string; email?: string; mobile?: string },
) {
const user = await this.prisma.user.findFirst({
where: {
...(where.id ? { id: where.id } : {}),
...(where.email ? { email: where.email } : {}),
...(where.mobile ? { mobile: where.mobile } : {}),
},
select: { id: true },
});
if (!user) return null;
return this.prisma.membership.findFirst({
where: { organizationId, userId: user.id },
include: { user: true },
});
}
}
export function unauthorizedIfMissing<T>(value: T | null | undefined): T {
if (!value) throw new UnauthorizedException();
return value;
}