Super admin can set a seat limit of 3, 10, 20, or a negotiated number so an owner cannot add users past that cap. Co-authored-by: Cursor <cursoragent@cursor.com>
631 lines
22 KiB
TypeScript
631 lines
22 KiB
TypeScript
import {
|
|
BadRequestException,
|
|
ConflictException,
|
|
ForbiddenException,
|
|
Injectable,
|
|
NotFoundException,
|
|
} from '@nestjs/common';
|
|
import type {
|
|
DataScope,
|
|
DataVisibility,
|
|
InviteUserRequest,
|
|
OrgUserDto,
|
|
UpdateOrgUserRequest,
|
|
UpdateUserVisibilityRequest,
|
|
UserRole,
|
|
} from '@mento/shared';
|
|
import { DATA_SCOPES, DATA_VISIBILITIES, parseDataAccessPolicy } from '@mento/shared';
|
|
import { resolveAllSectionAccess } from '../common/data-access';
|
|
import { parseVisibilityScopes } from '../common/record-scope';
|
|
import * as bcrypt from 'bcryptjs';
|
|
import { Prisma } from '@prisma/client';
|
|
import { normalizeMobile } from '../auth/auth.service';
|
|
import { PrismaService } from '../prisma/prisma.service';
|
|
import { OrgRolesService } from '../org-roles/org-roles.service';
|
|
import { MembershipsService } from '../memberships/memberships.service';
|
|
import { BillingService } from '../billing/billing.service';
|
|
import { LocalFileStorage } from '../storage/local-file.storage';
|
|
import * as path from 'path';
|
|
|
|
function inviteEmailFromMobile(mobile: string): string {
|
|
return `${mobile}@users.mento.local`;
|
|
}
|
|
|
|
const AVATAR_EXT: Record<string, string> = {
|
|
jpeg: 'image/jpeg',
|
|
jpg: 'image/jpeg',
|
|
png: 'image/png',
|
|
webp: 'image/webp',
|
|
gif: 'image/gif',
|
|
};
|
|
|
|
type MembershipRow = {
|
|
role: UserRole;
|
|
orgRoleId: string | null;
|
|
isActive: boolean;
|
|
commissionPercent: { toString(): string } | number | null;
|
|
dataVisibility?: string | null;
|
|
dataVisibilityScopes?: unknown;
|
|
dataAccessOverride?: unknown;
|
|
priorArchiveEnabled?: boolean;
|
|
logoutTime?: string | null;
|
|
blockLoginAfterLogout?: boolean;
|
|
loginOtpOnly?: boolean;
|
|
createdAt: Date;
|
|
orgRole: { id: string; name: string; color: string; dataAccessPolicy?: unknown } | null;
|
|
user: {
|
|
id: string;
|
|
email: string;
|
|
mobile: string | null;
|
|
fullName: string;
|
|
lastLoginAt: Date | null;
|
|
avatarStorageKey: string | null;
|
|
organizationId: string;
|
|
};
|
|
};
|
|
|
|
@Injectable()
|
|
export class UsersService {
|
|
constructor(
|
|
private readonly prisma: PrismaService,
|
|
private readonly orgRoles: OrgRolesService,
|
|
private readonly memberships: MembershipsService,
|
|
private readonly files: LocalFileStorage,
|
|
private readonly billing: BillingService,
|
|
) {}
|
|
|
|
async list(organizationId: string): Promise<OrgUserDto[]> {
|
|
await this.orgRoles.ensureDefaultRoles(organizationId);
|
|
const rows = await this.memberships.listInOrg(organizationId);
|
|
return rows.map((row) => this.toDto(row));
|
|
}
|
|
|
|
async invite(
|
|
organizationId: string,
|
|
actorRole: UserRole,
|
|
dto: InviteUserRequest,
|
|
): Promise<OrgUserDto> {
|
|
if (actorRole !== 'owner' && actorRole !== 'admin') {
|
|
throw new ForbiddenException('فقط مالک یا مدیر میتواند کاربر اضافه کند');
|
|
}
|
|
|
|
const mobile = normalizeMobile(dto.mobile);
|
|
if (!mobile) {
|
|
throw new BadRequestException('شماره موبایل معتبر نیست');
|
|
}
|
|
|
|
const resolved = await this.orgRoles.resolveInviteRole(organizationId, {
|
|
orgRoleId: dto.orgRoleId,
|
|
role: dto.role,
|
|
});
|
|
if (resolved.role === 'owner' && actorRole !== 'owner') {
|
|
throw new ForbiddenException('فقط مالک میتواند نقش مدیر سیستم را واگذار کند');
|
|
}
|
|
|
|
const existingUser = await this.prisma.user.findFirst({ where: { mobile } });
|
|
if (existingUser) {
|
|
return this.addExistingUser(organizationId, existingUser.id, resolved);
|
|
}
|
|
|
|
await this.billing.assertSeatAvailable(organizationId, null);
|
|
const password = dto.password?.trim() ?? '';
|
|
if (password.length < 8) {
|
|
throw new BadRequestException(
|
|
'برای کاربر جدید رمز عبور حداقل ۸ کاراکتر لازم است',
|
|
);
|
|
}
|
|
|
|
const email = inviteEmailFromMobile(mobile);
|
|
const existingEmail = await this.prisma.user.findFirst({ where: { email } });
|
|
if (existingEmail) {
|
|
throw new ConflictException('این شماره موبایل قبلاً ثبت شده است');
|
|
}
|
|
|
|
const passwordHash = await bcrypt.hash(password, 10);
|
|
try {
|
|
const created = await this.prisma.user.create({
|
|
data: {
|
|
organizationId,
|
|
email,
|
|
mobile,
|
|
fullName: dto.fullName.trim(),
|
|
passwordHash,
|
|
role: resolved.role,
|
|
orgRoleId: resolved.orgRoleId,
|
|
isActive: true,
|
|
},
|
|
});
|
|
await this.memberships.createMembership({
|
|
userId: created.id,
|
|
organizationId,
|
|
role: resolved.role,
|
|
orgRoleId: resolved.orgRoleId,
|
|
});
|
|
return this.dtoOf(organizationId, created.id);
|
|
} catch (e) {
|
|
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') {
|
|
throw new ConflictException('این شماره موبایل قبلاً ثبت شده است');
|
|
}
|
|
throw e;
|
|
}
|
|
}
|
|
|
|
async updateProfile(
|
|
organizationId: string,
|
|
actorRole: UserRole,
|
|
targetId: string,
|
|
dto: UpdateOrgUserRequest,
|
|
): Promise<OrgUserDto> {
|
|
if (actorRole !== 'owner' && actorRole !== 'admin') {
|
|
throw new ForbiddenException('فقط مالک یا مدیر میتواند مشخصات کاربر را ویرایش کند');
|
|
}
|
|
|
|
const target = await this.memberships.findUserInOrgById(
|
|
organizationId,
|
|
targetId,
|
|
);
|
|
if (target.role === 'owner' && actorRole !== 'owner') {
|
|
throw new ForbiddenException('مدیر نمیتواند مشخصات مالک را تغییر دهد');
|
|
}
|
|
|
|
const fullName = dto.fullName.trim();
|
|
if (fullName.length < 2) {
|
|
throw new BadRequestException('نام باید حداقل دو حرف باشد');
|
|
}
|
|
const mobile = normalizeMobile(dto.mobile);
|
|
if (!mobile) {
|
|
throw new BadRequestException('شماره موبایل معتبر نیست');
|
|
}
|
|
|
|
const previousMobile = target.user.mobile ?? '';
|
|
if (mobile !== previousMobile) {
|
|
const taken = await this.prisma.user.findFirst({
|
|
where: { mobile, id: { not: targetId } },
|
|
select: { id: true },
|
|
});
|
|
if (taken) {
|
|
throw new ConflictException('این شماره موبایل قبلاً ثبت شده است');
|
|
}
|
|
}
|
|
|
|
const data: Prisma.UserUpdateInput = { fullName, mobile };
|
|
const prevEmail = target.user.email;
|
|
const wasSynthetic =
|
|
prevEmail.endsWith('@users.mento.local') ||
|
|
prevEmail === inviteEmailFromMobile(previousMobile);
|
|
if (wasSynthetic && mobile !== previousMobile) {
|
|
const nextEmail = inviteEmailFromMobile(mobile);
|
|
const emailTaken = await this.prisma.user.findFirst({
|
|
where: { email: nextEmail, id: { not: targetId } },
|
|
select: { id: true },
|
|
});
|
|
if (!emailTaken) data.email = nextEmail;
|
|
}
|
|
|
|
const password = dto.password?.trim() ?? '';
|
|
if (password) {
|
|
if (password.length < 8) {
|
|
throw new BadRequestException('رمز عبور باید حداقل ۸ کاراکتر باشد');
|
|
}
|
|
data.passwordHash = await bcrypt.hash(password, 10);
|
|
}
|
|
|
|
try {
|
|
await this.prisma.user.update({
|
|
where: { id: targetId },
|
|
data,
|
|
});
|
|
} catch (e) {
|
|
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') {
|
|
throw new ConflictException('این شماره موبایل یا ایمیل قبلاً ثبت شده است');
|
|
}
|
|
throw e;
|
|
}
|
|
return this.dtoOf(organizationId, targetId);
|
|
}
|
|
|
|
async updateCommission(
|
|
organizationId: string,
|
|
actorRole: UserRole,
|
|
targetId: string,
|
|
percent: number,
|
|
): Promise<OrgUserDto> {
|
|
if (actorRole !== 'owner' && actorRole !== 'admin') {
|
|
throw new ForbiddenException('فقط مالک یا مدیر میتواند پورسانت را تعیین کند');
|
|
}
|
|
if (percent < 0 || percent > 100) {
|
|
throw new BadRequestException('درصد پورسانت باید بین ۰ تا ۱۰۰ باشد');
|
|
}
|
|
const target = await this.memberships.findUserInOrgById(organizationId, targetId);
|
|
await this.prisma.membership.update({
|
|
where: { id: target.id },
|
|
data: { commissionPercent: new Prisma.Decimal(percent) },
|
|
});
|
|
if (target.user.organizationId === organizationId) {
|
|
await this.prisma.user.update({
|
|
where: { id: target.user.id },
|
|
data: { commissionPercent: new Prisma.Decimal(percent) },
|
|
});
|
|
}
|
|
return this.dtoOf(organizationId, targetId);
|
|
}
|
|
|
|
async updateLogoutTime(
|
|
organizationId: string,
|
|
actorRole: UserRole,
|
|
targetId: string,
|
|
logoutTime: string | null,
|
|
blockLoginAfterLogout?: boolean,
|
|
): Promise<OrgUserDto> {
|
|
if (actorRole !== 'owner') {
|
|
throw new ForbiddenException('فقط مالک میتواند ساعت خروج را تعیین کند');
|
|
}
|
|
const target = await this.memberships.findUserInOrgById(organizationId, targetId);
|
|
if (!target) throw new NotFoundException('کاربر پیدا نشد');
|
|
if (target.role === 'owner') {
|
|
throw new BadRequestException('برای مالک ساعت خروج تعیین نمیشود');
|
|
}
|
|
const value = logoutTime?.trim() || null;
|
|
if (value && !/^([01]\d|2[0-3]):[0-5]\d$/.test(value)) {
|
|
throw new BadRequestException('ساعت خروج معتبر نیست');
|
|
}
|
|
await this.prisma.membership.update({
|
|
where: { id: target.id },
|
|
data: {
|
|
logoutTime: value,
|
|
// بدون ساعت خروج، مسدودسازی معنی ندارد
|
|
...(!value
|
|
? { blockLoginAfterLogout: false }
|
|
: blockLoginAfterLogout !== undefined
|
|
? { blockLoginAfterLogout }
|
|
: {}),
|
|
},
|
|
});
|
|
return this.dtoOf(organizationId, targetId);
|
|
}
|
|
|
|
async updatePriorArchive(
|
|
organizationId: string,
|
|
actorRole: UserRole,
|
|
targetId: string,
|
|
enabled: boolean,
|
|
): Promise<OrgUserDto> {
|
|
if (actorRole !== 'owner') {
|
|
throw new ForbiddenException('فقط مالک میتواند بایگانی اطلاعات قبل را برای کاربران روشن کند');
|
|
}
|
|
const target = await this.memberships.findUserInOrgById(organizationId, targetId);
|
|
if (enabled) {
|
|
const org = await this.prisma.organization.findUnique({
|
|
where: { id: organizationId },
|
|
select: { enabledModules: true },
|
|
});
|
|
if (!org?.enabledModules?.includes('prior_data_archive')) {
|
|
throw new ForbiddenException('بایگانی اطلاعات قبل برای این محیط فعال نیست');
|
|
}
|
|
}
|
|
await this.prisma.membership.update({
|
|
where: { id: target.id },
|
|
data: { priorArchiveEnabled: enabled },
|
|
});
|
|
return this.dtoOf(organizationId, targetId);
|
|
}
|
|
|
|
async updateVisibility(
|
|
organizationId: string,
|
|
actorRole: UserRole,
|
|
targetId: string,
|
|
dto: UpdateUserVisibilityRequest,
|
|
): Promise<OrgUserDto> {
|
|
if (actorRole !== 'owner' && actorRole !== 'admin') {
|
|
throw new ForbiddenException('اجازه تغییر سطح دسترسی به پروندهها را ندارید');
|
|
}
|
|
const { dataVisibility, scopes } = dto;
|
|
if (dataVisibility === undefined && !scopes) {
|
|
throw new BadRequestException('تغییری ارسال نشده است');
|
|
}
|
|
if (dataVisibility !== undefined && !DATA_VISIBILITIES.includes(dataVisibility)) {
|
|
throw new BadRequestException('سطح دسترسی به پروندهها نامعتبر است');
|
|
}
|
|
if (scopes) {
|
|
for (const [key, value] of Object.entries(scopes)) {
|
|
if (!DATA_SCOPES.includes(key as DataScope)) {
|
|
throw new BadRequestException(`بخش نامعتبر: ${key}`);
|
|
}
|
|
if (value !== null && !DATA_VISIBILITIES.includes(value as DataVisibility)) {
|
|
throw new BadRequestException('سطح دسترسی به پروندهها نامعتبر است');
|
|
}
|
|
}
|
|
}
|
|
const target = await this.memberships.findUserInOrgById(
|
|
organizationId,
|
|
targetId,
|
|
);
|
|
if (target.role === 'owner' || target.role === 'admin') {
|
|
throw new BadRequestException(
|
|
'مالک و مدیر همیشه همه پروندهها را میبینند',
|
|
);
|
|
}
|
|
const data: Prisma.MembershipUpdateInput = {};
|
|
if (dataVisibility !== undefined) data.dataVisibility = dataVisibility;
|
|
if (scopes) {
|
|
const current = await this.prisma.membership.findUnique({
|
|
where: { id: target.id },
|
|
select: { dataVisibilityScopes: true },
|
|
});
|
|
const merged = parseVisibilityScopes(current?.dataVisibilityScopes);
|
|
for (const [key, value] of Object.entries(scopes)) {
|
|
if (value === null) delete merged[key as DataScope];
|
|
else merged[key as DataScope] = value;
|
|
}
|
|
data.dataVisibilityScopes = merged;
|
|
}
|
|
await this.prisma.membership.update({
|
|
where: { id: target.id },
|
|
data,
|
|
});
|
|
return this.dtoOf(organizationId, targetId);
|
|
}
|
|
|
|
async updateDataAccess(
|
|
organizationId: string,
|
|
actorRole: UserRole,
|
|
targetId: string,
|
|
dto: { dataAccessOverride?: Record<string, unknown> | null },
|
|
): Promise<OrgUserDto> {
|
|
if (actorRole !== 'owner' && actorRole !== 'admin') {
|
|
throw new ForbiddenException('اجازه تغییر محدوده داده را ندارید');
|
|
}
|
|
if (dto.dataAccessOverride === undefined) {
|
|
throw new BadRequestException('تغییری ارسال نشده است');
|
|
}
|
|
const target = await this.memberships.findUserInOrgById(organizationId, targetId);
|
|
if (target.role === 'owner' || target.role === 'admin') {
|
|
throw new BadRequestException('مالک و مدیر محدودیت عمق داده ندارند');
|
|
}
|
|
const next =
|
|
dto.dataAccessOverride === null ? {} : parseDataAccessPolicy(dto.dataAccessOverride);
|
|
await this.prisma.membership.update({
|
|
where: { id: target.id },
|
|
data: { dataAccessOverride: next },
|
|
});
|
|
return this.dtoOf(organizationId, targetId);
|
|
}
|
|
|
|
async updateRole(
|
|
organizationId: string,
|
|
actor: { userId: string; role: UserRole },
|
|
targetId: string,
|
|
dto: { role?: UserRole; orgRoleId?: string },
|
|
): Promise<OrgUserDto> {
|
|
if (actor.role !== 'owner' && actor.role !== 'admin') {
|
|
throw new ForbiddenException('فقط مالک یا مدیر میتواند نقش را تغییر دهد');
|
|
}
|
|
|
|
const target = await this.memberships.findUserInOrgById(organizationId, targetId);
|
|
|
|
let nextRole = dto.role ?? target.role;
|
|
let nextOrgRoleId = dto.orgRoleId ?? target.orgRoleId;
|
|
|
|
if (dto.orgRoleId) {
|
|
const orgRole = await this.prisma.orgRole.findFirst({
|
|
where: { id: dto.orgRoleId, organizationId },
|
|
});
|
|
if (!orgRole) throw new BadRequestException('نقش انتخابشده معتبر نیست');
|
|
nextOrgRoleId = orgRole.id;
|
|
nextRole = orgRole.systemKey
|
|
? (['owner', 'admin', 'sales', 'support'].includes(orgRole.systemKey)
|
|
? (orgRole.systemKey as UserRole)
|
|
: 'sales')
|
|
: 'sales';
|
|
} else if (dto.role) {
|
|
await this.orgRoles.ensureDefaultRoles(organizationId);
|
|
const orgRole = await this.prisma.orgRole.findFirst({
|
|
where: { organizationId, systemKey: dto.role },
|
|
});
|
|
nextOrgRoleId = orgRole?.id ?? nextOrgRoleId;
|
|
}
|
|
|
|
if (actor.role === 'admin') {
|
|
if (target.role === 'owner' || nextRole === 'owner') {
|
|
throw new ForbiddenException('مدیر نمیتواند نقش مالک را تغییر دهد');
|
|
}
|
|
}
|
|
|
|
if (target.role === 'owner' && nextRole !== 'owner') {
|
|
const owners = await this.memberships.countActiveOwners(
|
|
organizationId,
|
|
target.userId,
|
|
);
|
|
if (owners < 1) {
|
|
throw new BadRequestException(
|
|
'حداقل یک مالک برای سازمان لازم است؛ ابتدا مالک دیگری تعیین کنید',
|
|
);
|
|
}
|
|
}
|
|
|
|
await this.prisma.membership.update({
|
|
where: { id: target.id },
|
|
data: { role: nextRole, orgRoleId: nextOrgRoleId },
|
|
});
|
|
if (target.user.organizationId === organizationId) {
|
|
await this.memberships.syncUserLastOrg(
|
|
target.userId,
|
|
organizationId,
|
|
nextRole,
|
|
nextOrgRoleId,
|
|
);
|
|
}
|
|
return this.dtoOf(organizationId, targetId);
|
|
}
|
|
|
|
async updateLoginOtpOnly(
|
|
organizationId: string,
|
|
actor: { userId: string; role: UserRole },
|
|
targetId: string,
|
|
loginOtpOnly: boolean,
|
|
): Promise<OrgUserDto> {
|
|
if (actor.role !== 'owner' && actor.role !== 'admin') {
|
|
throw new ForbiddenException('فقط مالک یا مدیر میتواند شیوهٔ ورود را تغییر دهد');
|
|
}
|
|
const target = await this.memberships.findUserInOrgById(organizationId, targetId);
|
|
if (target.role === 'owner') {
|
|
throw new BadRequestException('برای مالک نمیتوان ورود فقط با کد را فعال کرد');
|
|
}
|
|
if (loginOtpOnly) {
|
|
const mobile = target.user.mobile?.trim();
|
|
if (!mobile) {
|
|
throw new BadRequestException('برای ورود با کد، کاربر باید موبایل داشته باشد');
|
|
}
|
|
}
|
|
await this.prisma.membership.update({
|
|
where: { id: target.id },
|
|
data: { loginOtpOnly },
|
|
});
|
|
return this.dtoOf(organizationId, targetId);
|
|
}
|
|
|
|
async updateStatus(
|
|
organizationId: string,
|
|
actor: { userId: string; role: UserRole },
|
|
targetId: string,
|
|
isActive: boolean,
|
|
): Promise<OrgUserDto> {
|
|
if (actor.role !== 'owner' && actor.role !== 'admin') {
|
|
throw new ForbiddenException('فقط مالک یا مدیر میتواند وضعیت کاربر را تغییر دهد');
|
|
}
|
|
if (actor.userId === targetId && !isActive) {
|
|
throw new BadRequestException('نمیتوانید حساب خودتان را غیرفعال کنید');
|
|
}
|
|
const target = await this.memberships.findUserInOrgById(organizationId, targetId);
|
|
if (target.role === 'owner' && !isActive && actor.role !== 'owner') {
|
|
throw new ForbiddenException('مدیر نمیتواند مالک را غیرفعال کند');
|
|
}
|
|
if (target.role === 'owner' && !isActive) {
|
|
const activeOwners = await this.memberships.countActiveOwners(
|
|
organizationId,
|
|
targetId,
|
|
);
|
|
if (activeOwners < 1) {
|
|
throw new BadRequestException('حداقل یک مالک فعال لازم است');
|
|
}
|
|
}
|
|
if (isActive && !target.isActive) {
|
|
await this.billing.assertSeatAvailable(organizationId, targetId);
|
|
}
|
|
await this.prisma.membership.update({
|
|
where: { id: target.id },
|
|
data: { isActive },
|
|
});
|
|
return this.dtoOf(organizationId, targetId);
|
|
}
|
|
|
|
async openAvatar(organizationId: string, userId: string) {
|
|
const membership = await this.memberships.findInOrg(organizationId, userId);
|
|
if (!membership) throw new NotFoundException('کاربر پیدا نشد');
|
|
const key = membership.user.avatarStorageKey;
|
|
if (!key) {
|
|
throw new NotFoundException('عکس پروفایل تنظیم نشده');
|
|
}
|
|
const ext = path.extname(key).slice(1).toLowerCase();
|
|
const mime = AVATAR_EXT[ext] ?? 'image/jpeg';
|
|
return {
|
|
stream: this.files.openReadStream(key),
|
|
mimeType: mime,
|
|
};
|
|
}
|
|
|
|
private async addExistingUser(
|
|
organizationId: string,
|
|
userId: string,
|
|
resolved: { role: UserRole; orgRoleId: string },
|
|
): Promise<OrgUserDto> {
|
|
await this.billing.assertSeatAvailable(organizationId, userId);
|
|
const existing = await this.prisma.membership.findFirst({
|
|
where: { userId, organizationId },
|
|
});
|
|
if (existing?.isActive) {
|
|
throw new ConflictException('این کاربر هماکنون عضو این محیط است');
|
|
}
|
|
if (existing) {
|
|
await this.prisma.membership.update({
|
|
where: { id: existing.id },
|
|
data: {
|
|
isActive: true,
|
|
role: resolved.role,
|
|
orgRoleId: resolved.orgRoleId,
|
|
},
|
|
});
|
|
return this.dtoOf(organizationId, userId);
|
|
}
|
|
try {
|
|
await this.memberships.createMembership({
|
|
userId,
|
|
organizationId,
|
|
role: resolved.role,
|
|
orgRoleId: resolved.orgRoleId,
|
|
});
|
|
} catch (e) {
|
|
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') {
|
|
throw new ConflictException('این کاربر هماکنون عضو این محیط است');
|
|
}
|
|
throw e;
|
|
}
|
|
return this.dtoOf(organizationId, userId);
|
|
}
|
|
|
|
private async dtoOf(organizationId: string, userId: string): Promise<OrgUserDto> {
|
|
const row = await this.memberships.findUserInOrgById(organizationId, userId);
|
|
return this.toDto(row);
|
|
}
|
|
|
|
private toDto(row: MembershipRow & { userId?: string }): OrgUserDto {
|
|
return {
|
|
id: row.user.id,
|
|
email: row.user.email,
|
|
mobile: row.user.mobile ?? null,
|
|
fullName: row.user.fullName,
|
|
role: row.role,
|
|
orgRoleId: row.orgRoleId,
|
|
orgRoleName: row.orgRole?.name,
|
|
orgRoleColor: row.orgRole?.color ?? this.orgRoles.defaultColorFor(row.role),
|
|
isActive: row.isActive,
|
|
lastLoginAt: row.user.lastLoginAt ? row.user.lastLoginAt.toISOString() : null,
|
|
hasAvatar: Boolean(row.user.avatarStorageKey),
|
|
commissionPercent: Number(row.commissionPercent ?? 0),
|
|
dataVisibility:
|
|
row.role === 'owner' || row.role === 'admin'
|
|
? 'org'
|
|
: DATA_VISIBILITIES.includes(row.dataVisibility as DataVisibility)
|
|
? (row.dataVisibility as DataVisibility)
|
|
: 'own',
|
|
dataVisibilityScopes:
|
|
row.role === 'owner' || row.role === 'admin'
|
|
? {}
|
|
: parseVisibilityScopes(row.dataVisibilityScopes),
|
|
dataAccessOverride:
|
|
row.role === 'owner' || row.role === 'admin'
|
|
? {}
|
|
: parseDataAccessPolicy(row.dataAccessOverride),
|
|
...(() => {
|
|
const effectiveDataAccess = resolveAllSectionAccess({
|
|
role: row.role,
|
|
rolePolicy: row.orgRole?.dataAccessPolicy,
|
|
override: row.dataAccessOverride,
|
|
legacyVisibility: row.dataVisibility,
|
|
legacyScopes: parseVisibilityScopes(row.dataVisibilityScopes),
|
|
});
|
|
return {
|
|
effectiveDataAccess,
|
|
effectiveCustomersAccess: effectiveDataAccess.customers,
|
|
};
|
|
})(),
|
|
priorArchiveEnabled: Boolean(row.priorArchiveEnabled),
|
|
logoutTime: row.role === 'owner' ? null : row.logoutTime ?? null,
|
|
blockLoginAfterLogout:
|
|
row.role !== 'owner' && Boolean(row.logoutTime) && Boolean(row.blockLoginAfterLogout),
|
|
loginOtpOnly: row.role === 'owner' ? false : Boolean(row.loginOtpOnly),
|
|
createdAt: row.createdAt.toISOString(),
|
|
};
|
|
}
|
|
}
|