Add named teams and per-user data visibility (own/team/org) with settings UI and org module.
This commit is contained in:
@@ -38,6 +38,7 @@ import { AuditLogsModule } from './audit-logs/audit-logs.module';
|
||||
import { SavedViewsModule } from './saved-views/saved-views.module';
|
||||
import { PublicApiModule } from './public-api/public-api.module';
|
||||
import { SmsCampaignsModule } from './sms-campaigns/sms-campaigns.module';
|
||||
import { TeamsModule } from './teams/teams.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -83,6 +84,7 @@ import { SmsCampaignsModule } from './sms-campaigns/sms-campaigns.module';
|
||||
SavedViewsModule,
|
||||
PublicApiModule,
|
||||
SmsCampaignsModule,
|
||||
TeamsModule,
|
||||
],
|
||||
})
|
||||
export class AppModule {}
|
||||
|
||||
@@ -2,8 +2,11 @@ import { Injectable, UnauthorizedException } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { PassportStrategy } from '@nestjs/passport';
|
||||
import { ExtractJwt, Strategy } from 'passport-jwt';
|
||||
import type { DataVisibility } from '@mento/shared';
|
||||
import { BillingService } from '../billing/billing.service';
|
||||
import { normalizeDataVisibility } from '../common/record-scope';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { canViewAllRecords } from '@mento/shared';
|
||||
|
||||
export type JwtPayload = {
|
||||
sub: string;
|
||||
@@ -50,16 +53,58 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
isActive: true,
|
||||
organization: { isActive: true },
|
||||
},
|
||||
select: { organizationId: true, role: true },
|
||||
select: { organizationId: true, role: true, dataVisibility: true },
|
||||
});
|
||||
if (!membership) {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
await this.billing.requireActiveForOrganization(membership.organizationId);
|
||||
|
||||
const dataVisibility = normalizeDataVisibility(membership.dataVisibility);
|
||||
const visibleUserIds = await this.resolveVisibleUserIds(
|
||||
membership.organizationId,
|
||||
user.id,
|
||||
membership.role,
|
||||
dataVisibility,
|
||||
);
|
||||
|
||||
return {
|
||||
userId: user.id,
|
||||
organizationId: membership.organizationId,
|
||||
role: membership.role,
|
||||
dataVisibility,
|
||||
visibleUserIds,
|
||||
};
|
||||
}
|
||||
|
||||
private async resolveVisibleUserIds(
|
||||
organizationId: string,
|
||||
userId: string,
|
||||
role: string,
|
||||
dataVisibility: DataVisibility,
|
||||
): Promise<string[] | null> {
|
||||
if (canViewAllRecords(role) || dataVisibility === 'org') {
|
||||
return null;
|
||||
}
|
||||
if (dataVisibility === 'own') {
|
||||
return [userId];
|
||||
}
|
||||
|
||||
const memberships = await this.prisma.teamMember.findMany({
|
||||
where: {
|
||||
userId,
|
||||
team: { organizationId },
|
||||
},
|
||||
select: { teamId: true },
|
||||
});
|
||||
if (!memberships.length) return [userId];
|
||||
|
||||
const peers = await this.prisma.teamMember.findMany({
|
||||
where: {
|
||||
teamId: { in: memberships.map((m) => m.teamId) },
|
||||
},
|
||||
select: { userId: true },
|
||||
});
|
||||
return [...new Set([userId, ...peers.map((p) => p.userId)])];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { UserRole } from '@mento/shared';
|
||||
import type { DataVisibility, UserRole } from '@mento/shared';
|
||||
import { canViewAllRecords } from '@mento/shared';
|
||||
import type { Prisma } from '@prisma/client';
|
||||
|
||||
@@ -6,39 +6,73 @@ export type Actor = {
|
||||
userId: string;
|
||||
role: string;
|
||||
organizationId: string;
|
||||
/** own | team | org — از Membership */
|
||||
dataVisibility?: DataVisibility;
|
||||
/**
|
||||
* شناسه کاربرانی که پروندهشان برای این Actor قابلمشاهده است.
|
||||
* اگر undefined باشد فقط خود کاربر؛ اگر null یعنی همه (elevated/org).
|
||||
*/
|
||||
visibleUserIds?: string[] | null;
|
||||
};
|
||||
|
||||
export function elevated(role: string | null | undefined): boolean {
|
||||
return canViewAllRecords(role);
|
||||
}
|
||||
|
||||
export function seesAllRecords(
|
||||
actor: Pick<Actor, 'role' | 'dataVisibility' | 'visibleUserIds'>,
|
||||
): boolean {
|
||||
if (elevated(actor.role)) return true;
|
||||
if (actor.dataVisibility === 'org') return true;
|
||||
if (actor.visibleUserIds === null) return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
function scopedOwnerIds(
|
||||
actor: Pick<Actor, 'userId' | 'role' | 'dataVisibility' | 'visibleUserIds'>,
|
||||
): string[] | null {
|
||||
if (seesAllRecords(actor)) return null;
|
||||
const ids = actor.visibleUserIds?.length
|
||||
? actor.visibleUserIds
|
||||
: [actor.userId];
|
||||
return [...new Set(ids.filter(Boolean))];
|
||||
}
|
||||
|
||||
/** فیلتر مالکیت برای Deal / Quote / Invoice / Person / Company */
|
||||
export function ownerWhere(
|
||||
actor: Pick<Actor, 'userId' | 'role'>,
|
||||
): { ownerId: string } | Record<string, never> {
|
||||
if (elevated(actor.role)) return {};
|
||||
return { ownerId: actor.userId };
|
||||
actor: Pick<Actor, 'userId' | 'role' | 'dataVisibility' | 'visibleUserIds'>,
|
||||
): { ownerId: string } | { ownerId: { in: string[] } } | Record<string, never> {
|
||||
const ids = scopedOwnerIds(actor);
|
||||
if (!ids) return {};
|
||||
if (ids.length === 1) return { ownerId: ids[0]! };
|
||||
return { ownerId: { in: ids } };
|
||||
}
|
||||
|
||||
/** فعالیتها بر اساس مسئول */
|
||||
export function assigneeWhere(
|
||||
actor: Pick<Actor, 'userId' | 'role'>,
|
||||
): { assigneeId: string } | Record<string, never> {
|
||||
if (elevated(actor.role)) return {};
|
||||
return { assigneeId: actor.userId };
|
||||
actor: Pick<Actor, 'userId' | 'role' | 'dataVisibility' | 'visibleUserIds'>,
|
||||
):
|
||||
| { assigneeId: string }
|
||||
| { assigneeId: { in: string[] } }
|
||||
| Record<string, never> {
|
||||
const ids = scopedOwnerIds(actor);
|
||||
if (!ids) return {};
|
||||
if (ids.length === 1) return { assigneeId: ids[0]! };
|
||||
return { assigneeId: { in: ids } };
|
||||
}
|
||||
|
||||
/** یادداشت: نویسنده یا پروندههای تحت مالکیت */
|
||||
/** یادداشت: نویسنده یا پروندههای تحت مالکیت در محدوده دید */
|
||||
export function noteAccessWhere(
|
||||
actor: Pick<Actor, 'userId' | 'role'>,
|
||||
actor: Pick<Actor, 'userId' | 'role' | 'dataVisibility' | 'visibleUserIds'>,
|
||||
): Prisma.NoteWhereInput {
|
||||
if (elevated(actor.role)) return {};
|
||||
const ids = scopedOwnerIds(actor);
|
||||
if (!ids) return {};
|
||||
return {
|
||||
OR: [
|
||||
{ authorId: actor.userId },
|
||||
{ deal: { ownerId: actor.userId } },
|
||||
{ person: { ownerId: actor.userId } },
|
||||
{ company: { ownerId: actor.userId } },
|
||||
{ authorId: { in: ids } },
|
||||
{ deal: { ownerId: { in: ids } } },
|
||||
{ person: { ownerId: { in: ids } } },
|
||||
{ company: { ownerId: { in: ids } } },
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -53,13 +87,14 @@ export function resolveOwnerId(
|
||||
return actor.userId;
|
||||
}
|
||||
|
||||
/** تیکت: مسئول یا ثبتکننده */
|
||||
/** تیکت: مسئول یا ثبتکننده در محدوده دید */
|
||||
export function ticketAccessWhere(
|
||||
actor: Pick<Actor, 'userId' | 'role'>,
|
||||
actor: Pick<Actor, 'userId' | 'role' | 'dataVisibility' | 'visibleUserIds'>,
|
||||
): Prisma.TicketWhereInput {
|
||||
if (elevated(actor.role)) return {};
|
||||
const ids = scopedOwnerIds(actor);
|
||||
if (!ids) return {};
|
||||
return {
|
||||
OR: [{ assigneeId: actor.userId }, { reporterId: actor.userId }],
|
||||
OR: [{ assigneeId: { in: ids } }, { reporterId: { in: ids } }],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -68,3 +103,10 @@ export function canReassignOwner(role: string | null | undefined): boolean {
|
||||
}
|
||||
|
||||
export type OrgRole = UserRole;
|
||||
|
||||
export function normalizeDataVisibility(
|
||||
value: string | null | undefined,
|
||||
): DataVisibility {
|
||||
if (value === 'team' || value === 'org' || value === 'own') return value;
|
||||
return 'own';
|
||||
}
|
||||
|
||||
@@ -403,13 +403,16 @@ export class SalesService {
|
||||
actor: Actor,
|
||||
filterUserId?: string,
|
||||
): Prisma.SalesGoalWhereInput {
|
||||
if (elevated(actor.role)) {
|
||||
if (elevated(actor.role) || actor.dataVisibility === 'org' || actor.visibleUserIds === null) {
|
||||
if (filterUserId === 'org') return { userId: null };
|
||||
if (filterUserId) return { userId: filterUserId };
|
||||
return {};
|
||||
}
|
||||
const ids = actor.visibleUserIds?.length
|
||||
? actor.visibleUserIds
|
||||
: [actor.userId];
|
||||
return {
|
||||
OR: [{ userId: actor.userId }, { userId: null }],
|
||||
OR: [{ userId: { in: ids } }, { userId: null }],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import {
|
||||
IsArray,
|
||||
IsIn,
|
||||
IsOptional,
|
||||
IsString,
|
||||
MaxLength,
|
||||
MinLength,
|
||||
ValidateNested,
|
||||
} from 'class-validator';
|
||||
import { Type } from 'class-transformer';
|
||||
|
||||
export class CreateTeamDto {
|
||||
@IsString()
|
||||
@MinLength(2)
|
||||
@MaxLength(80)
|
||||
name!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(300)
|
||||
description?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
memberIds?: string[];
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@IsString({ each: true })
|
||||
managerIds?: string[];
|
||||
}
|
||||
|
||||
export class UpdateTeamDto {
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MinLength(2)
|
||||
@MaxLength(80)
|
||||
name?: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsString()
|
||||
@MaxLength(300)
|
||||
description?: string;
|
||||
}
|
||||
|
||||
class TeamMemberInputDto {
|
||||
@IsString()
|
||||
userId!: string;
|
||||
|
||||
@IsIn(['member', 'manager'])
|
||||
role!: 'member' | 'manager';
|
||||
}
|
||||
|
||||
export class SetTeamMembersDto {
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => TeamMemberInputDto)
|
||||
members!: TeamMemberInputDto[];
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
Get,
|
||||
Param,
|
||||
Patch,
|
||||
Post,
|
||||
Put,
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||
import { CurrentUser } from '../auth/current-user.decorator';
|
||||
import { Roles } from '../auth/roles.decorator';
|
||||
import { RolesGuard } from '../auth/roles.guard';
|
||||
import { OrgModuleGuard } from '../billing/org-module.guard';
|
||||
import { RequireOrgModule } from '../billing/org-module.decorator';
|
||||
import { CreateTeamDto, SetTeamMembersDto, UpdateTeamDto } from './dto';
|
||||
import { TeamsService } from './teams.service';
|
||||
|
||||
@Controller('teams')
|
||||
@UseGuards(JwtAuthGuard, RolesGuard, OrgModuleGuard)
|
||||
@RequireOrgModule('teams')
|
||||
export class TeamsController {
|
||||
constructor(private readonly teams: TeamsService) {}
|
||||
|
||||
@Get()
|
||||
list(@CurrentUser() user: { organizationId: string }) {
|
||||
return this.teams.list(user.organizationId);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@Roles('owner', 'admin')
|
||||
create(
|
||||
@CurrentUser() user: { organizationId: string },
|
||||
@Body() dto: CreateTeamDto,
|
||||
) {
|
||||
return this.teams.create(user.organizationId, dto);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@Roles('owner', 'admin')
|
||||
update(
|
||||
@CurrentUser() user: { organizationId: string },
|
||||
@Param('id') id: string,
|
||||
@Body() dto: UpdateTeamDto,
|
||||
) {
|
||||
return this.teams.update(user.organizationId, id, dto);
|
||||
}
|
||||
|
||||
@Put(':id/members')
|
||||
@Roles('owner', 'admin')
|
||||
setMembers(
|
||||
@CurrentUser() user: { organizationId: string },
|
||||
@Param('id') id: string,
|
||||
@Body() dto: SetTeamMembersDto,
|
||||
) {
|
||||
return this.teams.setMembers(user.organizationId, id, dto);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
@Roles('owner', 'admin')
|
||||
async remove(
|
||||
@CurrentUser() user: { organizationId: string },
|
||||
@Param('id') id: string,
|
||||
) {
|
||||
await this.teams.remove(user.organizationId, id);
|
||||
return { ok: true };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { BillingModule } from '../billing/billing.module';
|
||||
import { TeamsController } from './teams.controller';
|
||||
import { TeamsService } from './teams.service';
|
||||
|
||||
@Module({
|
||||
imports: [AuthModule, BillingModule],
|
||||
controllers: [TeamsController],
|
||||
providers: [TeamsService],
|
||||
exports: [TeamsService],
|
||||
})
|
||||
export class TeamsModule {}
|
||||
@@ -0,0 +1,244 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import type {
|
||||
CreateTeamRequest,
|
||||
SetTeamMembersRequest,
|
||||
TeamDto,
|
||||
TeamMemberDto,
|
||||
TeamMemberRole,
|
||||
UpdateTeamRequest,
|
||||
} from '@mento/shared';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
|
||||
@Injectable()
|
||||
export class TeamsService {
|
||||
constructor(private readonly prisma: PrismaService) {}
|
||||
|
||||
async list(organizationId: string): Promise<TeamDto[]> {
|
||||
const rows = await this.prisma.team.findMany({
|
||||
where: { organizationId },
|
||||
include: {
|
||||
members: {
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
fullName: true,
|
||||
avatarStorageKey: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: [{ role: 'asc' }, { user: { fullName: 'asc' } }],
|
||||
},
|
||||
},
|
||||
orderBy: { name: 'asc' },
|
||||
});
|
||||
return rows.map((row) => this.toDto(row));
|
||||
}
|
||||
|
||||
async create(
|
||||
organizationId: string,
|
||||
dto: CreateTeamRequest,
|
||||
): Promise<TeamDto> {
|
||||
const name = dto.name?.trim();
|
||||
if (!name) throw new BadRequestException('نام تیم الزامی است');
|
||||
|
||||
const managerIds = new Set((dto.managerIds ?? []).filter(Boolean));
|
||||
const memberIds = [
|
||||
...new Set([
|
||||
...(dto.memberIds ?? []).filter(Boolean),
|
||||
...managerIds,
|
||||
]),
|
||||
];
|
||||
|
||||
if (memberIds.length) {
|
||||
await this.assertMembersInOrg(organizationId, memberIds);
|
||||
}
|
||||
|
||||
try {
|
||||
const row = await this.prisma.team.create({
|
||||
data: {
|
||||
organizationId,
|
||||
name,
|
||||
description: dto.description?.trim() ?? '',
|
||||
members: memberIds.length
|
||||
? {
|
||||
create: memberIds.map((userId) => ({
|
||||
userId,
|
||||
role: managerIds.has(userId) ? 'manager' : 'member',
|
||||
})),
|
||||
}
|
||||
: undefined,
|
||||
},
|
||||
include: {
|
||||
members: {
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
fullName: true,
|
||||
avatarStorageKey: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
return this.toDto(row);
|
||||
} catch (e) {
|
||||
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') {
|
||||
throw new BadRequestException('تیمی با این نام از قبل وجود دارد');
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
async update(
|
||||
organizationId: string,
|
||||
id: string,
|
||||
dto: UpdateTeamRequest,
|
||||
): Promise<TeamDto> {
|
||||
await this.ensureTeam(organizationId, id);
|
||||
const data: Prisma.TeamUpdateInput = {};
|
||||
if (dto.name !== undefined) {
|
||||
const name = dto.name.trim();
|
||||
if (!name) throw new BadRequestException('نام تیم الزامی است');
|
||||
data.name = name;
|
||||
}
|
||||
if (dto.description !== undefined) {
|
||||
data.description = dto.description.trim();
|
||||
}
|
||||
try {
|
||||
await this.prisma.team.update({ where: { id }, data });
|
||||
} catch (e) {
|
||||
if (e instanceof Prisma.PrismaClientKnownRequestError && e.code === 'P2002') {
|
||||
throw new BadRequestException('تیمی با این نام از قبل وجود دارد');
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
return this.get(organizationId, id);
|
||||
}
|
||||
|
||||
async remove(organizationId: string, id: string): Promise<void> {
|
||||
await this.ensureTeam(organizationId, id);
|
||||
await this.prisma.team.delete({ where: { id } });
|
||||
}
|
||||
|
||||
async setMembers(
|
||||
organizationId: string,
|
||||
id: string,
|
||||
dto: SetTeamMembersRequest,
|
||||
): Promise<TeamDto> {
|
||||
await this.ensureTeam(organizationId, id);
|
||||
const members = dto.members ?? [];
|
||||
const userIds = [...new Set(members.map((m) => m.userId).filter(Boolean))];
|
||||
if (userIds.length) {
|
||||
await this.assertMembersInOrg(organizationId, userIds);
|
||||
}
|
||||
|
||||
const byUser = new Map<string, TeamMemberRole>();
|
||||
for (const m of members) {
|
||||
if (!m.userId) continue;
|
||||
byUser.set(m.userId, m.role === 'manager' ? 'manager' : 'member');
|
||||
}
|
||||
|
||||
await this.prisma.$transaction([
|
||||
this.prisma.teamMember.deleteMany({ where: { teamId: id } }),
|
||||
...(byUser.size
|
||||
? [
|
||||
this.prisma.teamMember.createMany({
|
||||
data: [...byUser.entries()].map(([userId, role]) => ({
|
||||
teamId: id,
|
||||
userId,
|
||||
role,
|
||||
})),
|
||||
}),
|
||||
]
|
||||
: []),
|
||||
]);
|
||||
|
||||
return this.get(organizationId, id);
|
||||
}
|
||||
|
||||
private async get(organizationId: string, id: string): Promise<TeamDto> {
|
||||
const row = await this.prisma.team.findFirst({
|
||||
where: { id, organizationId },
|
||||
include: {
|
||||
members: {
|
||||
include: {
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
fullName: true,
|
||||
avatarStorageKey: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: [{ role: 'asc' }, { user: { fullName: 'asc' } }],
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!row) throw new NotFoundException('تیم پیدا نشد');
|
||||
return this.toDto(row);
|
||||
}
|
||||
|
||||
private async ensureTeam(organizationId: string, id: string) {
|
||||
const row = await this.prisma.team.findFirst({
|
||||
where: { id, organizationId },
|
||||
select: { id: true },
|
||||
});
|
||||
if (!row) throw new NotFoundException('تیم پیدا نشد');
|
||||
}
|
||||
|
||||
private async assertMembersInOrg(
|
||||
organizationId: string,
|
||||
userIds: string[],
|
||||
) {
|
||||
const count = await this.prisma.membership.count({
|
||||
where: {
|
||||
organizationId,
|
||||
userId: { in: userIds },
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
if (count !== userIds.length) {
|
||||
throw new BadRequestException('برخی کاربران عضو فعال این محیط نیستند');
|
||||
}
|
||||
}
|
||||
|
||||
private toDto(row: {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
members: Array<{
|
||||
role: string;
|
||||
user: {
|
||||
id: string;
|
||||
fullName: string;
|
||||
avatarStorageKey: string | null;
|
||||
};
|
||||
}>;
|
||||
}): TeamDto {
|
||||
const members: TeamMemberDto[] = row.members.map((m) => ({
|
||||
userId: m.user.id,
|
||||
fullName: m.user.fullName,
|
||||
role: m.role === 'manager' ? 'manager' : 'member',
|
||||
hasAvatar: Boolean(m.user.avatarStorageKey),
|
||||
}));
|
||||
return {
|
||||
id: row.id,
|
||||
name: row.name,
|
||||
description: row.description,
|
||||
memberCount: members.length,
|
||||
members,
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
updatedAt: row.updatedAt.toISOString(),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -67,3 +67,8 @@ export class UpdateUserCommissionDto {
|
||||
@Max(100)
|
||||
commissionPercent!: number;
|
||||
}
|
||||
|
||||
export class UpdateUserVisibilityDto {
|
||||
@IsIn(['own', 'team', 'org'])
|
||||
dataVisibility!: 'own' | 'team' | 'org';
|
||||
}
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
UpdateUserCommissionDto,
|
||||
UpdateUserRoleDto,
|
||||
UpdateUserStatusDto,
|
||||
UpdateUserVisibilityDto,
|
||||
} from './dto';
|
||||
import { UsersService } from './users.service';
|
||||
|
||||
@@ -79,6 +80,21 @@ export class UsersController {
|
||||
);
|
||||
}
|
||||
|
||||
@Patch(':id/visibility')
|
||||
@Roles('owner', 'admin')
|
||||
updateVisibility(
|
||||
@CurrentUser() user: { organizationId: string; role: UserRole },
|
||||
@Param('id') id: string,
|
||||
@Body() dto: UpdateUserVisibilityDto,
|
||||
) {
|
||||
return this.usersService.updateVisibility(
|
||||
user.organizationId,
|
||||
user.role,
|
||||
id,
|
||||
dto.dataVisibility,
|
||||
);
|
||||
}
|
||||
|
||||
@Patch(':id/role')
|
||||
@Roles('owner', 'admin')
|
||||
updateRole(
|
||||
|
||||
@@ -5,7 +5,13 @@ import {
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import type { InviteUserRequest, OrgUserDto, UserRole } from '@mento/shared';
|
||||
import type {
|
||||
DataVisibility,
|
||||
InviteUserRequest,
|
||||
OrgUserDto,
|
||||
UserRole,
|
||||
} from '@mento/shared';
|
||||
import { DATA_VISIBILITIES } from '@mento/shared';
|
||||
import * as bcrypt from 'bcryptjs';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { normalizeMobile } from '../auth/auth.service';
|
||||
@@ -32,6 +38,7 @@ type MembershipRow = {
|
||||
orgRoleId: string | null;
|
||||
isActive: boolean;
|
||||
commissionPercent: { toString(): string } | number | null;
|
||||
dataVisibility?: string | null;
|
||||
createdAt: Date;
|
||||
orgRole: { id: string; name: string; color: string } | null;
|
||||
user: {
|
||||
@@ -155,6 +162,34 @@ export class UsersService {
|
||||
return this.dtoOf(organizationId, targetId);
|
||||
}
|
||||
|
||||
async updateVisibility(
|
||||
organizationId: string,
|
||||
actorRole: UserRole,
|
||||
targetId: string,
|
||||
dataVisibility: DataVisibility,
|
||||
): Promise<OrgUserDto> {
|
||||
if (actorRole !== 'owner' && actorRole !== 'admin') {
|
||||
throw new ForbiddenException('اجازه تغییر سطح دید را ندارید');
|
||||
}
|
||||
if (!DATA_VISIBILITIES.includes(dataVisibility)) {
|
||||
throw new BadRequestException('سطح دید نامعتبر است');
|
||||
}
|
||||
const target = await this.memberships.findUserInOrgById(
|
||||
organizationId,
|
||||
targetId,
|
||||
);
|
||||
if (target.role === 'owner' || target.role === 'admin') {
|
||||
throw new BadRequestException(
|
||||
'مالک و مدیر همیشه همه پروندهها را میبینند',
|
||||
);
|
||||
}
|
||||
await this.prisma.membership.update({
|
||||
where: { id: target.id },
|
||||
data: { dataVisibility },
|
||||
});
|
||||
return this.dtoOf(organizationId, targetId);
|
||||
}
|
||||
|
||||
async updateRole(
|
||||
organizationId: string,
|
||||
actor: { userId: string; role: UserRole },
|
||||
@@ -326,6 +361,12 @@ export class UsersService {
|
||||
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',
|
||||
createdAt: row.createdAt.toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user