feat(auth): add module read/write admin RBAC
Introduce permission catalog, PermissionsGuard, JWT/me payload with role+permissions, and gate all module controllers. Admins CRUD accepts role/permissions with safety rules; migrate existing users to super_admin. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -9,47 +9,57 @@ import {
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||
import { PermissionsGuard } from '../auth/permissions.guard';
|
||||
import { RequirePermissions } from '../auth/permissions.decorator';
|
||||
import { CurrentUser } from '../auth/current-user.decorator';
|
||||
import { AdminsService } from './admins.service';
|
||||
import { CreateAdminDto, TestSmsDto, UpdateAdminDto } from './dto/admin.dto';
|
||||
|
||||
@Controller('admins')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@UseGuards(JwtAuthGuard, PermissionsGuard)
|
||||
export class AdminsController {
|
||||
constructor(private readonly adminsService: AdminsService) {}
|
||||
|
||||
@Get()
|
||||
@RequirePermissions('admins:read')
|
||||
list() {
|
||||
return this.adminsService.list();
|
||||
}
|
||||
|
||||
@Post()
|
||||
@RequirePermissions('admins:write')
|
||||
create(
|
||||
@Body() dto: CreateAdminDto,
|
||||
@CurrentUser() user: { id: string },
|
||||
@CurrentUser()
|
||||
user: { id: string; role?: string; permissions?: unknown },
|
||||
) {
|
||||
return this.adminsService.create(dto, user.id);
|
||||
return this.adminsService.create(dto, user);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@RequirePermissions('admins:write')
|
||||
update(
|
||||
@Param('id') id: string,
|
||||
@Body() dto: UpdateAdminDto,
|
||||
@CurrentUser() user: { id: string },
|
||||
@CurrentUser()
|
||||
user: { id: string; role?: string; permissions?: unknown },
|
||||
) {
|
||||
return this.adminsService.update(id, dto, user.id);
|
||||
return this.adminsService.update(id, dto, user);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
remove(@Param('id') id: string, @CurrentUser() user: { id: string }) {
|
||||
return this.adminsService.remove(id, user.id);
|
||||
@RequirePermissions('admins:write')
|
||||
remove(
|
||||
@Param('id') id: string,
|
||||
@CurrentUser()
|
||||
user: { id: string; role?: string; permissions?: unknown },
|
||||
) {
|
||||
return this.adminsService.remove(id, user);
|
||||
}
|
||||
|
||||
@Post(':id/test-sms')
|
||||
testSms(
|
||||
@Param('id') id: string,
|
||||
@Body() dto: TestSmsDto,
|
||||
) {
|
||||
@RequirePermissions('admins:write')
|
||||
testSms(@Param('id') id: string, @Body() dto: TestSmsDto) {
|
||||
return this.adminsService.sendTestSms(id, dto.message);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { NotificationsModule } from '../notifications/notifications.module';
|
||||
import { AdminsController } from './admins.controller';
|
||||
import { AdminsService } from './admins.service';
|
||||
|
||||
@Module({
|
||||
imports: [NotificationsModule],
|
||||
imports: [NotificationsModule, AuthModule],
|
||||
controllers: [AdminsController],
|
||||
providers: [AdminsService],
|
||||
exports: [AdminsService],
|
||||
|
||||
+129
-35
@@ -1,18 +1,31 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
ConflictException,
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { Prisma } from '@prisma/client';
|
||||
import { Prisma, UserRole } from '@prisma/client';
|
||||
import * as bcrypt from 'bcrypt';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { AuditService } from '../audit/audit.service';
|
||||
import { AdminNotifyService } from '../notifications/admin-notify.service';
|
||||
import { CentralSmsService } from '../notifications/central-sms.service';
|
||||
import { displayName, normalizeMobile } from '../common/mobile.util';
|
||||
import {
|
||||
isFullAccessRole,
|
||||
normalizePermissionList,
|
||||
parsePermissions,
|
||||
userHasAllPermissions,
|
||||
} from '../auth/permissions';
|
||||
import { CreateAdminDto, UpdateAdminDto } from './dto/admin.dto';
|
||||
|
||||
type Actor = {
|
||||
id: string;
|
||||
role?: string;
|
||||
permissions?: unknown;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class AdminsService {
|
||||
constructor(
|
||||
@@ -23,33 +36,46 @@ export class AdminsService {
|
||||
) {}
|
||||
|
||||
list() {
|
||||
return this.prisma.user.findMany({
|
||||
orderBy: [{ isActive: 'desc' }, { createdAt: 'asc' }],
|
||||
select: {
|
||||
id: true,
|
||||
mobile: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
email: true,
|
||||
role: true,
|
||||
isActive: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
}).then((rows) =>
|
||||
rows.map((row) => ({
|
||||
...row,
|
||||
name: displayName(row.firstName, row.lastName),
|
||||
})),
|
||||
);
|
||||
return this.prisma.user
|
||||
.findMany({
|
||||
orderBy: [{ isActive: 'desc' }, { createdAt: 'asc' }],
|
||||
select: {
|
||||
id: true,
|
||||
mobile: true,
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
email: true,
|
||||
role: true,
|
||||
permissions: true,
|
||||
isActive: true,
|
||||
createdAt: true,
|
||||
updatedAt: true,
|
||||
},
|
||||
})
|
||||
.then((rows) =>
|
||||
rows.map((row) => ({
|
||||
...row,
|
||||
permissions: parsePermissions(row.permissions),
|
||||
isFullAccess: isFullAccessRole(row.role),
|
||||
name: displayName(row.firstName, row.lastName),
|
||||
})),
|
||||
);
|
||||
}
|
||||
|
||||
async create(dto: CreateAdminDto, actorId?: string) {
|
||||
async create(dto: CreateAdminDto, actor: Actor) {
|
||||
const mobile = normalizeMobile(dto.mobile);
|
||||
if (!mobile) {
|
||||
throw new BadRequestException('شماره موبایل نامعتبر است');
|
||||
}
|
||||
|
||||
const role = dto.role ?? UserRole.admin;
|
||||
const permissions =
|
||||
role === UserRole.super_admin
|
||||
? []
|
||||
: normalizePermissionList(dto.permissions ?? []);
|
||||
|
||||
this.assertCanAssign(actor, role, permissions);
|
||||
|
||||
try {
|
||||
const passwordHash = await bcrypt.hash(dto.password, 10);
|
||||
const user = await this.prisma.user.create({
|
||||
@@ -58,7 +84,8 @@ export class AdminsService {
|
||||
firstName: dto.firstName.trim(),
|
||||
lastName: dto.lastName.trim(),
|
||||
passwordHash,
|
||||
role: 'super_admin',
|
||||
role,
|
||||
permissions,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
@@ -67,7 +94,7 @@ export class AdminsService {
|
||||
action: 'admin.create',
|
||||
entityType: 'user',
|
||||
entityId: user.id,
|
||||
userId: actorId,
|
||||
userId: actor.id,
|
||||
after: this.publicUser(user) as unknown as Prisma.InputJsonValue,
|
||||
});
|
||||
|
||||
@@ -82,7 +109,7 @@ export class AdminsService {
|
||||
}
|
||||
}
|
||||
|
||||
async update(id: string, dto: UpdateAdminDto, actorId?: string) {
|
||||
async update(id: string, dto: UpdateAdminDto, actor: Actor) {
|
||||
const before = await this.prisma.user.findUnique({ where: { id } });
|
||||
if (!before) throw new NotFoundException('مدیر یافت نشد');
|
||||
|
||||
@@ -96,20 +123,48 @@ export class AdminsService {
|
||||
await this.ensureNotLastActive(id);
|
||||
}
|
||||
|
||||
const nextRole = dto.role ?? before.role;
|
||||
const nextPermissions =
|
||||
dto.permissions !== undefined
|
||||
? nextRole === UserRole.super_admin
|
||||
? []
|
||||
: normalizePermissionList(dto.permissions)
|
||||
: nextRole === UserRole.super_admin
|
||||
? []
|
||||
: parsePermissions(before.permissions);
|
||||
|
||||
if (dto.role !== undefined || dto.permissions !== undefined) {
|
||||
const roleChanged = nextRole !== before.role;
|
||||
const beforePerms = parsePermissions(before.permissions);
|
||||
const permsChanged =
|
||||
nextRole === UserRole.super_admin
|
||||
? before.role !== UserRole.super_admin
|
||||
: JSON.stringify([...beforePerms].sort()) !==
|
||||
JSON.stringify([...nextPermissions].sort());
|
||||
|
||||
if (roleChanged || permsChanged) {
|
||||
this.assertCanAssign(actor, nextRole, nextPermissions);
|
||||
}
|
||||
if (
|
||||
before.role === UserRole.super_admin &&
|
||||
nextRole !== UserRole.super_admin
|
||||
) {
|
||||
await this.ensureNotLastSuperAdmin(id);
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const data: {
|
||||
firstName?: string;
|
||||
lastName?: string;
|
||||
mobile?: string;
|
||||
isActive?: boolean;
|
||||
passwordHash?: string;
|
||||
} = {};
|
||||
const data: Prisma.UserUpdateInput = {};
|
||||
|
||||
if (dto.firstName !== undefined) data.firstName = dto.firstName.trim();
|
||||
if (dto.lastName !== undefined) data.lastName = dto.lastName.trim();
|
||||
if (mobile) data.mobile = mobile;
|
||||
if (dto.isActive !== undefined) data.isActive = dto.isActive;
|
||||
if (dto.password) data.passwordHash = await bcrypt.hash(dto.password, 10);
|
||||
if (dto.role !== undefined) data.role = nextRole;
|
||||
if (dto.role !== undefined || dto.permissions !== undefined) {
|
||||
data.permissions = nextPermissions;
|
||||
}
|
||||
|
||||
const user = await this.prisma.user.update({
|
||||
where: { id },
|
||||
@@ -120,7 +175,7 @@ export class AdminsService {
|
||||
action: 'admin.update',
|
||||
entityType: 'user',
|
||||
entityId: user.id,
|
||||
userId: actorId,
|
||||
userId: actor.id,
|
||||
before: this.publicUser(before) as unknown as Prisma.InputJsonValue,
|
||||
after: this.publicUser(user) as unknown as Prisma.InputJsonValue,
|
||||
});
|
||||
@@ -136,13 +191,16 @@ export class AdminsService {
|
||||
}
|
||||
}
|
||||
|
||||
async remove(id: string, actorId?: string) {
|
||||
async remove(id: string, actor: Actor) {
|
||||
const before = await this.prisma.user.findUnique({ where: { id } });
|
||||
if (!before) throw new NotFoundException('مدیر یافت نشد');
|
||||
if (actorId && actorId === id) {
|
||||
if (actor.id === id) {
|
||||
throw new BadRequestException('نمیتوانید خودتان را حذف کنید');
|
||||
}
|
||||
await this.ensureNotLastActive(id);
|
||||
if (before.role === UserRole.super_admin) {
|
||||
await this.ensureNotLastSuperAdmin(id);
|
||||
}
|
||||
|
||||
await this.prisma.user.delete({ where: { id } });
|
||||
|
||||
@@ -150,7 +208,7 @@ export class AdminsService {
|
||||
action: 'admin.delete',
|
||||
entityType: 'user',
|
||||
entityId: id,
|
||||
userId: actorId,
|
||||
userId: actor.id,
|
||||
before: this.publicUser(before) as unknown as Prisma.InputJsonValue,
|
||||
});
|
||||
|
||||
@@ -176,6 +234,26 @@ export class AdminsService {
|
||||
return { ok: true, mobile: user.mobile };
|
||||
}
|
||||
|
||||
private assertCanAssign(
|
||||
actor: Actor,
|
||||
role: UserRole | string,
|
||||
permissions: string[],
|
||||
) {
|
||||
if (isFullAccessRole(actor.role)) return;
|
||||
|
||||
if (role === UserRole.super_admin) {
|
||||
throw new ForbiddenException(
|
||||
'فقط مدیر کامل میتواند دسترسی کامل بدهد',
|
||||
);
|
||||
}
|
||||
|
||||
if (!userHasAllPermissions(actor, permissions)) {
|
||||
throw new ForbiddenException(
|
||||
'نمیتوانید مجوزی بالاتر از دسترسی خودتان بدهید',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private async ensureNotLastActive(id: string) {
|
||||
const activeCount = await this.prisma.user.count({
|
||||
where: { isActive: true },
|
||||
@@ -186,6 +264,18 @@ export class AdminsService {
|
||||
}
|
||||
}
|
||||
|
||||
private async ensureNotLastSuperAdmin(id: string) {
|
||||
const count = await this.prisma.user.count({
|
||||
where: { role: UserRole.super_admin, isActive: true },
|
||||
});
|
||||
const target = await this.prisma.user.findUnique({ where: { id } });
|
||||
if (target?.role === UserRole.super_admin && count <= 1) {
|
||||
throw new BadRequestException(
|
||||
'حداقل یک مدیر با دسترسی کامل باید باقی بماند',
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private publicUser(user: {
|
||||
id: string;
|
||||
mobile: string;
|
||||
@@ -193,10 +283,12 @@ export class AdminsService {
|
||||
lastName: string;
|
||||
email: string | null;
|
||||
role: string;
|
||||
permissions?: unknown;
|
||||
isActive: boolean;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
}) {
|
||||
const permissions = parsePermissions(user.permissions);
|
||||
return {
|
||||
id: user.id,
|
||||
mobile: user.mobile,
|
||||
@@ -205,6 +297,8 @@ export class AdminsService {
|
||||
name: displayName(user.firstName, user.lastName),
|
||||
email: user.email,
|
||||
role: user.role,
|
||||
permissions,
|
||||
isFullAccess: isFullAccessRole(user.role),
|
||||
isActive: user.isActive,
|
||||
createdAt: user.createdAt,
|
||||
updatedAt: user.updatedAt,
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import {
|
||||
ArrayUnique,
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
IsEnum,
|
||||
IsIn,
|
||||
IsOptional,
|
||||
IsString,
|
||||
MaxLength,
|
||||
MinLength,
|
||||
} from 'class-validator';
|
||||
import { UserRole } from '@prisma/client';
|
||||
import { PERMISSIONS } from '../../auth/permissions';
|
||||
|
||||
export class CreateAdminDto {
|
||||
@IsString()
|
||||
@@ -26,6 +32,16 @@ export class CreateAdminDto {
|
||||
@MinLength(6)
|
||||
@MaxLength(72)
|
||||
password!: string;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(UserRole)
|
||||
role?: UserRole;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ArrayUnique()
|
||||
@IsIn([...PERMISSIONS], { each: true })
|
||||
permissions?: string[];
|
||||
}
|
||||
|
||||
export class UpdateAdminDto {
|
||||
@@ -56,6 +72,16 @@ export class UpdateAdminDto {
|
||||
@IsOptional()
|
||||
@IsBoolean()
|
||||
isActive?: boolean;
|
||||
|
||||
@IsOptional()
|
||||
@IsEnum(UserRole)
|
||||
role?: UserRole;
|
||||
|
||||
@IsOptional()
|
||||
@IsArray()
|
||||
@ArrayUnique()
|
||||
@IsIn([...PERMISSIONS], { each: true })
|
||||
permissions?: string[];
|
||||
}
|
||||
|
||||
export class TestSmsDto {
|
||||
|
||||
@@ -1,18 +1,22 @@
|
||||
import { Controller, Get, Param, Query, UseGuards } from '@nestjs/common';
|
||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||
import { PermissionsGuard } from '../auth/permissions.guard';
|
||||
import { RequirePermissions } from '../auth/permissions.decorator';
|
||||
import { AuditService } from './audit.service';
|
||||
|
||||
@Controller('audit')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@UseGuards(JwtAuthGuard, PermissionsGuard)
|
||||
export class AuditController {
|
||||
constructor(private readonly auditService: AuditService) {}
|
||||
|
||||
@Get()
|
||||
@RequirePermissions('audit:read')
|
||||
findAll(@Query('limit') limit?: string) {
|
||||
return this.auditService.findAll(limit ? Number(limit) : 50);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@RequirePermissions('audit:read')
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.auditService.findOne(id);
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { Global, Module } from '@nestjs/common';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { AuditController } from './audit.controller';
|
||||
import { AuditService } from './audit.service';
|
||||
|
||||
@Global()
|
||||
@Module({
|
||||
imports: [AuthModule],
|
||||
controllers: [AuditController],
|
||||
providers: [AuditService],
|
||||
exports: [AuditService],
|
||||
|
||||
@@ -5,6 +5,7 @@ import { PassportModule } from '@nestjs/passport';
|
||||
import { AuthController } from './auth.controller';
|
||||
import { AuthService } from './auth.service';
|
||||
import { JwtStrategy } from './jwt.strategy';
|
||||
import { PermissionsGuard } from './permissions.guard';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
@@ -22,7 +23,7 @@ import { JwtStrategy } from './jwt.strategy';
|
||||
}),
|
||||
],
|
||||
controllers: [AuthController],
|
||||
providers: [AuthService, JwtStrategy],
|
||||
exports: [AuthService, JwtModule],
|
||||
providers: [AuthService, JwtStrategy, PermissionsGuard],
|
||||
exports: [AuthService, JwtModule, PermissionsGuard],
|
||||
})
|
||||
export class AuthModule {}
|
||||
|
||||
@@ -4,6 +4,7 @@ import * as bcrypt from 'bcrypt';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
import { displayName, normalizeMobile } from '../common/mobile.util';
|
||||
import { LoginDto } from './dto/login.dto';
|
||||
import { isFullAccessRole, parsePermissions } from './permissions';
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
@@ -32,6 +33,7 @@ export class AuthService {
|
||||
}
|
||||
|
||||
const name = displayName(user.firstName, user.lastName);
|
||||
const permissions = parsePermissions(user.permissions);
|
||||
const payload = {
|
||||
sub: user.id,
|
||||
mobile: user.mobile,
|
||||
@@ -47,6 +49,8 @@ export class AuthService {
|
||||
lastName: user.lastName,
|
||||
name,
|
||||
role: user.role,
|
||||
permissions,
|
||||
isFullAccess: isFullAccessRole(user.role),
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -61,6 +65,7 @@ export class AuthService {
|
||||
lastName: true,
|
||||
email: true,
|
||||
role: true,
|
||||
permissions: true,
|
||||
isActive: true,
|
||||
createdAt: true,
|
||||
},
|
||||
@@ -70,8 +75,11 @@ export class AuthService {
|
||||
throw new UnauthorizedException();
|
||||
}
|
||||
|
||||
const permissions = parsePermissions(user.permissions);
|
||||
return {
|
||||
...user,
|
||||
permissions,
|
||||
isFullAccess: isFullAccessRole(user.role),
|
||||
name: displayName(user.firstName, user.lastName),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ export class JwtStrategy extends PassportStrategy(Strategy) {
|
||||
firstName: true,
|
||||
lastName: true,
|
||||
role: true,
|
||||
permissions: true,
|
||||
isActive: true,
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
import { SetMetadata } from '@nestjs/common';
|
||||
import type { PermissionCode } from './permissions';
|
||||
|
||||
export const PERMISSIONS_KEY = 'required_permissions';
|
||||
|
||||
export type PermissionsMeta = {
|
||||
codes: PermissionCode[];
|
||||
mode: 'all' | 'any';
|
||||
};
|
||||
|
||||
export const RequirePermissions = (
|
||||
...args: Array<PermissionCode | { mode?: 'all' | 'any' }>
|
||||
) => {
|
||||
const codes: PermissionCode[] = [];
|
||||
let mode: 'all' | 'any' = 'all';
|
||||
for (const arg of args) {
|
||||
if (typeof arg === 'string') codes.push(arg);
|
||||
else if (arg && typeof arg === 'object' && arg.mode) mode = arg.mode;
|
||||
}
|
||||
return SetMetadata(PERMISSIONS_KEY, { codes, mode } satisfies PermissionsMeta);
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
import {
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
ForbiddenException,
|
||||
Injectable,
|
||||
} from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import {
|
||||
PERMISSIONS_KEY,
|
||||
type PermissionsMeta,
|
||||
} from './permissions.decorator';
|
||||
import {
|
||||
userHasAllPermissions,
|
||||
userHasAnyPermission,
|
||||
} from './permissions';
|
||||
|
||||
@Injectable()
|
||||
export class PermissionsGuard implements CanActivate {
|
||||
constructor(private readonly reflector: Reflector) {}
|
||||
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
const meta = this.reflector.getAllAndOverride<PermissionsMeta | undefined>(
|
||||
PERMISSIONS_KEY,
|
||||
[context.getHandler(), context.getClass()],
|
||||
);
|
||||
if (!meta?.codes?.length) return true;
|
||||
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const user = request.user as
|
||||
| { role?: string; permissions?: unknown }
|
||||
| undefined;
|
||||
if (!user) {
|
||||
throw new ForbiddenException('دسترسی غیرمجاز');
|
||||
}
|
||||
|
||||
const ok =
|
||||
meta.mode === 'any'
|
||||
? userHasAnyPermission(user, meta.codes)
|
||||
: userHasAllPermissions(user, meta.codes);
|
||||
|
||||
if (!ok) {
|
||||
throw new ForbiddenException('شما به این بخش دسترسی ندارید');
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
/** Permission codes for SuperAdmin panel RBAC */
|
||||
export const PERMISSIONS = [
|
||||
'sms:read',
|
||||
'sms:write',
|
||||
'servers:read',
|
||||
'servers:write',
|
||||
'plugins:read',
|
||||
'plugins:write',
|
||||
'support:read',
|
||||
'support:write',
|
||||
'hamkar:read',
|
||||
'hamkar:write',
|
||||
'khanegi:read',
|
||||
'khanegi:write',
|
||||
'admins:read',
|
||||
'admins:write',
|
||||
'audit:read',
|
||||
'settings:write',
|
||||
] as const;
|
||||
|
||||
export type PermissionCode = (typeof PERMISSIONS)[number];
|
||||
|
||||
export const PERMISSION_SET = new Set<string>(PERMISSIONS);
|
||||
|
||||
export type PermissionModule = {
|
||||
key: string;
|
||||
label: string;
|
||||
read?: PermissionCode;
|
||||
write?: PermissionCode;
|
||||
};
|
||||
|
||||
export const PERMISSION_MODULES: PermissionModule[] = [
|
||||
{ key: 'sms', label: 'پیامک', read: 'sms:read', write: 'sms:write' },
|
||||
{ key: 'servers', label: 'سرور', read: 'servers:read', write: 'servers:write' },
|
||||
{ key: 'plugins', label: 'افزونه', read: 'plugins:read', write: 'plugins:write' },
|
||||
{ key: 'support', label: 'پشتیبانی', read: 'support:read', write: 'support:write' },
|
||||
{ key: 'hamkar', label: 'همکار', read: 'hamkar:read', write: 'hamkar:write' },
|
||||
{ key: 'khanegi', label: 'خانگی', read: 'khanegi:read', write: 'khanegi:write' },
|
||||
{ key: 'admins', label: 'مدیران', read: 'admins:read', write: 'admins:write' },
|
||||
{ key: 'audit', label: 'لاگ', read: 'audit:read' },
|
||||
{ key: 'settings', label: 'تنظیمات حساس', write: 'settings:write' },
|
||||
];
|
||||
|
||||
export function parsePermissions(raw: unknown): PermissionCode[] {
|
||||
if (!Array.isArray(raw)) return [];
|
||||
const out: PermissionCode[] = [];
|
||||
for (const item of raw) {
|
||||
const code = String(item || '').trim();
|
||||
if (PERMISSION_SET.has(code)) out.push(code as PermissionCode);
|
||||
}
|
||||
return [...new Set(out)];
|
||||
}
|
||||
|
||||
/** write implies matching read for the same module */
|
||||
export function normalizePermissionList(codes: string[]): PermissionCode[] {
|
||||
const set = new Set<PermissionCode>();
|
||||
for (const code of parsePermissions(codes)) {
|
||||
set.add(code);
|
||||
if (code.endsWith(':write')) {
|
||||
const read = code.replace(/:write$/, ':read');
|
||||
if (PERMISSION_SET.has(read)) set.add(read as PermissionCode);
|
||||
}
|
||||
}
|
||||
return [...set];
|
||||
}
|
||||
|
||||
export function isFullAccessRole(role: string | null | undefined) {
|
||||
return role === 'super_admin';
|
||||
}
|
||||
|
||||
export function userHasPermission(
|
||||
user: { role?: string | null; permissions?: unknown },
|
||||
code: PermissionCode | string,
|
||||
): boolean {
|
||||
if (isFullAccessRole(user.role)) return true;
|
||||
const list = parsePermissions(user.permissions);
|
||||
return list.includes(code as PermissionCode);
|
||||
}
|
||||
|
||||
export function userHasAnyPermission(
|
||||
user: { role?: string | null; permissions?: unknown },
|
||||
codes: Array<PermissionCode | string>,
|
||||
): boolean {
|
||||
if (isFullAccessRole(user.role)) return true;
|
||||
return codes.some((code) => userHasPermission(user, code));
|
||||
}
|
||||
|
||||
export function userHasAllPermissions(
|
||||
user: { role?: string | null; permissions?: unknown },
|
||||
codes: Array<PermissionCode | string>,
|
||||
): boolean {
|
||||
if (isFullAccessRole(user.role)) return true;
|
||||
return codes.every((code) => userHasPermission(user, code));
|
||||
}
|
||||
|
||||
export function productReadPermission(product: string): PermissionCode {
|
||||
return product === 'hamkar' ? 'hamkar:read' : 'khanegi:read';
|
||||
}
|
||||
|
||||
export function productWritePermission(product: string): PermissionCode {
|
||||
return product === 'hamkar' ? 'hamkar:write' : 'khanegi:write';
|
||||
}
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { AdminNotifyService } from './admin-notify.service';
|
||||
import { CentralSmsService } from './central-sms.service';
|
||||
import { RuntimeSettingsController } from './runtime-settings.controller';
|
||||
import { RuntimeSettingsService } from './runtime-settings.service';
|
||||
|
||||
@Module({
|
||||
imports: [AuthModule],
|
||||
controllers: [RuntimeSettingsController],
|
||||
providers: [CentralSmsService, AdminNotifyService, RuntimeSettingsService],
|
||||
exports: [CentralSmsService, AdminNotifyService, RuntimeSettingsService],
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
import { Body, Controller, Get, Patch, UseGuards } from '@nestjs/common';
|
||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||
import { PermissionsGuard } from '../auth/permissions.guard';
|
||||
import { RequirePermissions } from '../auth/permissions.decorator';
|
||||
import { UpdateAdminSmsDto } from './dto/update-admin-sms.dto';
|
||||
import { RuntimeSettingsService } from './runtime-settings.service';
|
||||
|
||||
@Controller('settings')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@UseGuards(JwtAuthGuard, PermissionsGuard)
|
||||
export class RuntimeSettingsController {
|
||||
constructor(private readonly settings: RuntimeSettingsService) {}
|
||||
|
||||
@Get('admin-sms')
|
||||
@RequirePermissions('settings:write')
|
||||
getAdminSms() {
|
||||
return this.settings.getPublic();
|
||||
}
|
||||
|
||||
@Patch('admin-sms')
|
||||
@RequirePermissions('settings:write')
|
||||
setAdminSms(@Body() dto: UpdateAdminSmsDto) {
|
||||
this.settings.setAdminSmsEnabled(dto.enabled);
|
||||
return this.settings.getPublic();
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||
import { PermissionsGuard } from '../auth/permissions.guard';
|
||||
import { RequirePermissions } from '../auth/permissions.decorator';
|
||||
import { CurrentUser } from '../auth/current-user.decorator';
|
||||
import {
|
||||
CreatePluginDto,
|
||||
@@ -19,26 +21,30 @@ import {
|
||||
import { PluginsService } from './plugins.service';
|
||||
|
||||
@Controller('plugins')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@UseGuards(JwtAuthGuard, PermissionsGuard)
|
||||
export class PluginsController {
|
||||
constructor(private readonly pluginsService: PluginsService) {}
|
||||
|
||||
@Get('tenants')
|
||||
@RequirePermissions('plugins:read')
|
||||
listTenants() {
|
||||
return this.pluginsService.listTenants();
|
||||
}
|
||||
|
||||
@Get('expiring')
|
||||
@RequirePermissions('plugins:read')
|
||||
expiring(@Query('limit') limit?: string) {
|
||||
return this.pluginsService.getExpiringSoon(Number(limit) || 5);
|
||||
}
|
||||
|
||||
@Get('tenants/:tenantId')
|
||||
@RequirePermissions('plugins:read')
|
||||
getTenantPlugins(@Param('tenantId') tenantId: string) {
|
||||
return this.pluginsService.getTenantPlugins(tenantId);
|
||||
}
|
||||
|
||||
@Post('tenants/:tenantId')
|
||||
@RequirePermissions('plugins:write')
|
||||
create(
|
||||
@Param('tenantId') tenantId: string,
|
||||
@Body() dto: CreatePluginDto,
|
||||
@@ -48,11 +54,13 @@ export class PluginsController {
|
||||
}
|
||||
|
||||
@Get('items/:id')
|
||||
@RequirePermissions('plugins:read')
|
||||
getOne(@Param('id') id: string) {
|
||||
return this.pluginsService.getPlugin(id);
|
||||
}
|
||||
|
||||
@Patch('items/:id')
|
||||
@RequirePermissions('plugins:write')
|
||||
update(
|
||||
@Param('id') id: string,
|
||||
@Body() dto: UpdatePluginDto,
|
||||
@@ -62,6 +70,7 @@ export class PluginsController {
|
||||
}
|
||||
|
||||
@Post('items/:id/renew')
|
||||
@RequirePermissions('plugins:write')
|
||||
renew(
|
||||
@Param('id') id: string,
|
||||
@Body() dto: RenewPluginDto,
|
||||
@@ -71,6 +80,7 @@ export class PluginsController {
|
||||
}
|
||||
|
||||
@Delete('items/:id')
|
||||
@RequirePermissions('plugins:write')
|
||||
remove(@Param('id') id: string, @CurrentUser() user: { id: string }) {
|
||||
return this.pluginsService.remove(id, user.id);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { NotificationsModule } from '../notifications/notifications.module';
|
||||
import { PluginsController } from './plugins.controller';
|
||||
import { PluginsService } from './plugins.service';
|
||||
|
||||
@Module({
|
||||
imports: [NotificationsModule],
|
||||
imports: [NotificationsModule, AuthModule],
|
||||
controllers: [PluginsController],
|
||||
providers: [PluginsService],
|
||||
exports: [PluginsService],
|
||||
|
||||
@@ -10,36 +10,43 @@ import {
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||
import { PermissionsGuard } from '../auth/permissions.guard';
|
||||
import { RequirePermissions } from '../auth/permissions.decorator';
|
||||
import { CurrentUser } from '../auth/current-user.decorator';
|
||||
import { RenewServerDto, UpdateServerAccountDto } from './dto/server-account.dto';
|
||||
import { ServersService } from './servers.service';
|
||||
|
||||
@Controller('servers')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@UseGuards(JwtAuthGuard, PermissionsGuard)
|
||||
export class ServersController {
|
||||
constructor(private readonly serversService: ServersService) {}
|
||||
|
||||
@Get('tenants')
|
||||
@RequirePermissions('servers:read')
|
||||
listTenants() {
|
||||
return this.serversService.listTenants();
|
||||
}
|
||||
|
||||
@Get('expiring')
|
||||
@RequirePermissions('servers:read')
|
||||
expiring(@Query('limit') limit?: string) {
|
||||
return this.serversService.getExpiringSoon(Number(limit) || 5);
|
||||
}
|
||||
|
||||
@Post('check-alerts')
|
||||
@RequirePermissions('servers:write')
|
||||
checkAlerts() {
|
||||
return this.serversService.runExpiryAlerts().then(() => ({ ok: true }));
|
||||
}
|
||||
|
||||
@Get('tenants/:id')
|
||||
@RequirePermissions('servers:read')
|
||||
getTenantServers(@Param('id') id: string) {
|
||||
return this.serversService.getTenantServers(id);
|
||||
}
|
||||
|
||||
@Post('tenants/:id/servers')
|
||||
@RequirePermissions('servers:write')
|
||||
createServer(
|
||||
@Param('id') id: string,
|
||||
@Body() dto: UpdateServerAccountDto,
|
||||
@@ -50,6 +57,7 @@ export class ServersController {
|
||||
|
||||
/** @deprecated prefer PATCH /servers/accounts/:accountId */
|
||||
@Patch('tenants/:id')
|
||||
@RequirePermissions('servers:write')
|
||||
updateAccountLegacy(
|
||||
@Param('id') id: string,
|
||||
@Body() dto: UpdateServerAccountDto,
|
||||
@@ -60,6 +68,7 @@ export class ServersController {
|
||||
|
||||
/** @deprecated prefer POST /servers/accounts/:accountId/renew */
|
||||
@Post('tenants/:id/renew')
|
||||
@RequirePermissions('servers:write')
|
||||
renewLegacy(
|
||||
@Param('id') id: string,
|
||||
@Body() dto: RenewServerDto,
|
||||
@@ -69,11 +78,13 @@ export class ServersController {
|
||||
}
|
||||
|
||||
@Get('accounts/:accountId')
|
||||
@RequirePermissions('servers:read')
|
||||
getServerAccount(@Param('accountId') accountId: string) {
|
||||
return this.serversService.getServerAccount(accountId);
|
||||
}
|
||||
|
||||
@Patch('accounts/:accountId')
|
||||
@RequirePermissions('servers:write')
|
||||
updateServerAccount(
|
||||
@Param('accountId') accountId: string,
|
||||
@Body() dto: UpdateServerAccountDto,
|
||||
@@ -83,6 +94,7 @@ export class ServersController {
|
||||
}
|
||||
|
||||
@Delete('accounts/:accountId')
|
||||
@RequirePermissions('servers:write')
|
||||
deleteServerAccount(
|
||||
@Param('accountId') accountId: string,
|
||||
@CurrentUser() user: { id: string },
|
||||
@@ -91,6 +103,7 @@ export class ServersController {
|
||||
}
|
||||
|
||||
@Post('accounts/:accountId/renew')
|
||||
@RequirePermissions('servers:write')
|
||||
renewAccount(
|
||||
@Param('accountId') accountId: string,
|
||||
@Body() dto: RenewServerDto,
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { NotificationsModule } from '../notifications/notifications.module';
|
||||
import { ServersController } from './servers.controller';
|
||||
import { ServersService } from './servers.service';
|
||||
|
||||
@Module({
|
||||
imports: [NotificationsModule],
|
||||
imports: [NotificationsModule, AuthModule],
|
||||
controllers: [ServersController],
|
||||
providers: [ServersService],
|
||||
exports: [ServersService],
|
||||
|
||||
@@ -1,19 +1,23 @@
|
||||
import { Body, Controller, Get, Param, Post, UseGuards } from '@nestjs/common';
|
||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||
import { PermissionsGuard } from '../auth/permissions.guard';
|
||||
import { RequirePermissions } from '../auth/permissions.decorator';
|
||||
import { CurrentUser } from '../auth/current-user.decorator';
|
||||
import { SettingsProxyService } from './settings-proxy.service';
|
||||
|
||||
@Controller('tenants/:tenantId/settings')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@UseGuards(JwtAuthGuard, PermissionsGuard)
|
||||
export class SettingsProxyController {
|
||||
constructor(private readonly settingsProxy: SettingsProxyService) {}
|
||||
|
||||
@Get()
|
||||
@RequirePermissions('hamkar:read', 'khanegi:read', { mode: 'any' })
|
||||
getSettings(@Param('tenantId') tenantId: string) {
|
||||
return this.settingsProxy.getSettings(tenantId);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@RequirePermissions('hamkar:write', 'khanegi:write', { mode: 'any' })
|
||||
updateSettings(
|
||||
@Param('tenantId') tenantId: string,
|
||||
@Body() body: Record<string, unknown>,
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { SettingsProxyController } from './settings-proxy.controller';
|
||||
import { SettingsProxyService } from './settings-proxy.service';
|
||||
|
||||
@Module({
|
||||
imports: [AuthModule],
|
||||
controllers: [SettingsProxyController],
|
||||
providers: [SettingsProxyService],
|
||||
})
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||
import { PermissionsGuard } from '../auth/permissions.guard';
|
||||
import { RequirePermissions } from '../auth/permissions.decorator';
|
||||
import { CurrentUser } from '../auth/current-user.decorator';
|
||||
import {
|
||||
ChargeSmsWalletDto,
|
||||
@@ -20,31 +22,36 @@ import {
|
||||
import { SmsService } from './sms.service';
|
||||
|
||||
@Controller('sms')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@UseGuards(JwtAuthGuard, PermissionsGuard)
|
||||
export class SmsController {
|
||||
constructor(private readonly smsService: SmsService) {}
|
||||
|
||||
@Get('tenants')
|
||||
@RequirePermissions('sms:read')
|
||||
listTenants() {
|
||||
return this.smsService.listTenants();
|
||||
}
|
||||
|
||||
@Get('lowest')
|
||||
@RequirePermissions('sms:read')
|
||||
lowest(@Query('limit') limit?: string) {
|
||||
return this.smsService.getLowestBalances(Number(limit) || 5);
|
||||
}
|
||||
|
||||
@Get('tenants/:id')
|
||||
@RequirePermissions('sms:read')
|
||||
getAccount(@Param('id') id: string) {
|
||||
return this.smsService.getAccount(id);
|
||||
}
|
||||
|
||||
@Get('tenants/:id/charge-info')
|
||||
@RequirePermissions('sms:read')
|
||||
getChargeInfo(@Param('id') id: string) {
|
||||
return this.smsService.getChargeInfo(id);
|
||||
}
|
||||
|
||||
@Put('tenants/:id/charge-info')
|
||||
@RequirePermissions('sms:write')
|
||||
updateChargeInfo(
|
||||
@Param('id') id: string,
|
||||
@Body() dto: UpdateSmsChargeInfoDto,
|
||||
@@ -54,6 +61,7 @@ export class SmsController {
|
||||
}
|
||||
|
||||
@Get('tenants/:id/report')
|
||||
@RequirePermissions('sms:read')
|
||||
report(
|
||||
@Param('id') id: string,
|
||||
@Query('fromDate') fromDate?: string,
|
||||
@@ -63,6 +71,7 @@ export class SmsController {
|
||||
}
|
||||
|
||||
@Get('tenants/:id/report/charges')
|
||||
@RequirePermissions('sms:read')
|
||||
chargeReport(
|
||||
@Param('id') id: string,
|
||||
@Query('fromDate') fromDate?: string,
|
||||
@@ -72,16 +81,19 @@ export class SmsController {
|
||||
}
|
||||
|
||||
@Post('tenants/:id/logs/table')
|
||||
@RequirePermissions('sms:read')
|
||||
logsTable(@Param('id') id: string, @Body() dto: SmsTableQueryDto) {
|
||||
return this.smsService.getLogsTable(id, dto);
|
||||
}
|
||||
|
||||
@Post('tenants/:id/ledger/table')
|
||||
@RequirePermissions('sms:read')
|
||||
ledgerTable(@Param('id') id: string, @Body() dto: SmsTableQueryDto) {
|
||||
return this.smsService.getLedgerTable(id, dto);
|
||||
}
|
||||
|
||||
@Patch('tenants/:id')
|
||||
@RequirePermissions('sms:write')
|
||||
updateAccount(
|
||||
@Param('id') id: string,
|
||||
@Body() dto: UpdateSmsAccountDto,
|
||||
@@ -91,6 +103,7 @@ export class SmsController {
|
||||
}
|
||||
|
||||
@Post('tenants/:id/charge')
|
||||
@RequirePermissions('sms:write')
|
||||
charge(
|
||||
@Param('id') id: string,
|
||||
@Body() dto: ChargeSmsWalletDto,
|
||||
@@ -100,11 +113,13 @@ export class SmsController {
|
||||
}
|
||||
|
||||
@Post('tenants/:id/block')
|
||||
@RequirePermissions('sms:write')
|
||||
block(@Param('id') id: string, @CurrentUser() user: { id: string }) {
|
||||
return this.smsService.setBlocked(id, true, user.id);
|
||||
}
|
||||
|
||||
@Post('tenants/:id/unblock')
|
||||
@RequirePermissions('sms:write')
|
||||
unblock(@Param('id') id: string, @CurrentUser() user: { id: string }) {
|
||||
return this.smsService.setBlocked(id, false, user.id);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { NotificationsModule } from '../notifications/notifications.module';
|
||||
import { RemoteSmsAuthGuard } from '../auth/remote-sms-auth.guard';
|
||||
import { RemoteSmsController } from './remote-sms.controller';
|
||||
@@ -6,7 +7,7 @@ import { SmsController } from './sms.controller';
|
||||
import { SmsService } from './sms.service';
|
||||
|
||||
@Module({
|
||||
imports: [NotificationsModule],
|
||||
imports: [NotificationsModule, AuthModule],
|
||||
controllers: [SmsController, RemoteSmsController],
|
||||
providers: [SmsService, RemoteSmsAuthGuard],
|
||||
exports: [SmsService],
|
||||
|
||||
@@ -10,6 +10,8 @@ import {
|
||||
UseGuards,
|
||||
} from '@nestjs/common';
|
||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||
import { PermissionsGuard } from '../auth/permissions.guard';
|
||||
import { RequirePermissions } from '../auth/permissions.decorator';
|
||||
import { CurrentUser } from '../auth/current-user.decorator';
|
||||
import {
|
||||
CreateSupportDto,
|
||||
@@ -19,26 +21,30 @@ import {
|
||||
import { SupportService } from './support.service';
|
||||
|
||||
@Controller('support')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@UseGuards(JwtAuthGuard, PermissionsGuard)
|
||||
export class SupportController {
|
||||
constructor(private readonly supportService: SupportService) {}
|
||||
|
||||
@Get('tenants')
|
||||
@RequirePermissions('support:read')
|
||||
listTenants() {
|
||||
return this.supportService.listTenants();
|
||||
}
|
||||
|
||||
@Get('expiring')
|
||||
@RequirePermissions('support:read')
|
||||
expiring(@Query('limit') limit?: string) {
|
||||
return this.supportService.getExpiringSoon(Number(limit) || 5);
|
||||
}
|
||||
|
||||
@Get('tenants/:tenantId')
|
||||
@RequirePermissions('support:read')
|
||||
getTenantSupports(@Param('tenantId') tenantId: string) {
|
||||
return this.supportService.getTenantSupports(tenantId);
|
||||
}
|
||||
|
||||
@Post('tenants/:tenantId')
|
||||
@RequirePermissions('support:write')
|
||||
create(
|
||||
@Param('tenantId') tenantId: string,
|
||||
@Body() dto: CreateSupportDto,
|
||||
@@ -48,11 +54,13 @@ export class SupportController {
|
||||
}
|
||||
|
||||
@Get('items/:id')
|
||||
@RequirePermissions('support:read')
|
||||
getOne(@Param('id') id: string) {
|
||||
return this.supportService.getSupport(id);
|
||||
}
|
||||
|
||||
@Patch('items/:id')
|
||||
@RequirePermissions('support:write')
|
||||
update(
|
||||
@Param('id') id: string,
|
||||
@Body() dto: UpdateSupportDto,
|
||||
@@ -62,6 +70,7 @@ export class SupportController {
|
||||
}
|
||||
|
||||
@Post('items/:id/renew')
|
||||
@RequirePermissions('support:write')
|
||||
renew(
|
||||
@Param('id') id: string,
|
||||
@Body() dto: RenewSupportDto,
|
||||
@@ -71,6 +80,7 @@ export class SupportController {
|
||||
}
|
||||
|
||||
@Delete('items/:id')
|
||||
@RequirePermissions('support:write')
|
||||
remove(@Param('id') id: string, @CurrentUser() user: { id: string }) {
|
||||
return this.supportService.remove(id, user.id);
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { NotificationsModule } from '../notifications/notifications.module';
|
||||
import { SupportController } from './support.controller';
|
||||
import { SupportService } from './support.service';
|
||||
|
||||
@Module({
|
||||
imports: [NotificationsModule],
|
||||
imports: [NotificationsModule, AuthModule],
|
||||
controllers: [SupportController],
|
||||
providers: [SupportService],
|
||||
exports: [SupportService],
|
||||
|
||||
@@ -3,6 +3,7 @@ import {
|
||||
Body,
|
||||
Controller,
|
||||
Delete,
|
||||
ForbiddenException,
|
||||
Get,
|
||||
Param,
|
||||
Patch,
|
||||
@@ -18,7 +19,14 @@ import { ProductType } from '@prisma/client';
|
||||
import { diskStorage } from 'multer';
|
||||
import { extname } from 'path';
|
||||
import { JwtAuthGuard } from '../auth/jwt-auth.guard';
|
||||
import { PermissionsGuard } from '../auth/permissions.guard';
|
||||
import { RequirePermissions } from '../auth/permissions.decorator';
|
||||
import { CurrentUser } from '../auth/current-user.decorator';
|
||||
import {
|
||||
productReadPermission,
|
||||
productWritePermission,
|
||||
userHasPermission,
|
||||
} from '../auth/permissions';
|
||||
import {
|
||||
AVATARS_DIR,
|
||||
ensureUploadsDirs,
|
||||
@@ -30,8 +38,10 @@ import { TenantsService } from './tenants.service';
|
||||
|
||||
const IMAGE_MIME = /^image\/(jpeg|png|gif|webp|svg\+xml)$/i;
|
||||
|
||||
type AuthUser = { id: string; role?: string; permissions?: unknown };
|
||||
|
||||
@Controller('tenants')
|
||||
@UseGuards(JwtAuthGuard)
|
||||
@UseGuards(JwtAuthGuard, PermissionsGuard)
|
||||
export class TenantsController {
|
||||
constructor(
|
||||
private readonly tenantsService: TenantsService,
|
||||
@@ -39,11 +49,23 @@ export class TenantsController {
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
findAll(@Query('product') product?: ProductType) {
|
||||
return this.tenantsService.findAll(product);
|
||||
@RequirePermissions('hamkar:read', 'khanegi:read', { mode: 'any' })
|
||||
async findAll(
|
||||
@Query('product') product?: ProductType,
|
||||
@CurrentUser() user?: AuthUser,
|
||||
) {
|
||||
if (product) {
|
||||
this.assertProductAccess(user, product, 'read');
|
||||
return this.tenantsService.findAll(product);
|
||||
}
|
||||
const rows = await this.tenantsService.findAll();
|
||||
return rows.filter((row) =>
|
||||
userHasPermission(user || {}, productReadPermission(row.product)),
|
||||
);
|
||||
}
|
||||
|
||||
@Post('upload-avatar')
|
||||
@RequirePermissions('hamkar:write', 'khanegi:write', { mode: 'any' })
|
||||
@UseInterceptors(
|
||||
FileInterceptor('file', {
|
||||
storage: diskStorage({
|
||||
@@ -60,7 +82,12 @@ export class TenantsController {
|
||||
limits: { fileSize: 2 * 1024 * 1024 },
|
||||
fileFilter: (_req, file, cb) => {
|
||||
if (!IMAGE_MIME.test(file.mimetype)) {
|
||||
cb(new BadRequestException('فقط تصویر (jpg/png/gif/webp/svg) مجاز است') as any, false);
|
||||
cb(
|
||||
new BadRequestException(
|
||||
'فقط تصویر (jpg/png/gif/webp/svg) مجاز است',
|
||||
) as any,
|
||||
false,
|
||||
);
|
||||
return;
|
||||
}
|
||||
cb(null, true);
|
||||
@@ -76,29 +103,51 @@ export class TenantsController {
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.tenantsService.findOne(id);
|
||||
@RequirePermissions('hamkar:read', 'khanegi:read', { mode: 'any' })
|
||||
async findOne(@Param('id') id: string, @CurrentUser() user: AuthUser) {
|
||||
const tenant = await this.tenantsService.findOne(id);
|
||||
this.assertProductAccess(user, tenant.product, 'read');
|
||||
return tenant;
|
||||
}
|
||||
|
||||
@Post()
|
||||
create(
|
||||
@Body() dto: CreateTenantDto,
|
||||
@CurrentUser() user: { id: string },
|
||||
) {
|
||||
@RequirePermissions('hamkar:write', 'khanegi:write', { mode: 'any' })
|
||||
create(@Body() dto: CreateTenantDto, @CurrentUser() user: AuthUser) {
|
||||
this.assertProductAccess(user, dto.product, 'write');
|
||||
return this.tenantsService.create(dto, user.id);
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
update(
|
||||
@RequirePermissions('hamkar:write', 'khanegi:write', { mode: 'any' })
|
||||
async update(
|
||||
@Param('id') id: string,
|
||||
@Body() dto: UpdateTenantDto,
|
||||
@CurrentUser() user: { id: string },
|
||||
@CurrentUser() user: AuthUser,
|
||||
) {
|
||||
const tenant = await this.tenantsService.findOne(id);
|
||||
this.assertProductAccess(user, tenant.product, 'write');
|
||||
return this.tenantsService.update(id, dto, user.id);
|
||||
}
|
||||
|
||||
@Delete(':id')
|
||||
remove(@Param('id') id: string, @CurrentUser() user: { id: string }) {
|
||||
@RequirePermissions('hamkar:write', 'khanegi:write', { mode: 'any' })
|
||||
async remove(@Param('id') id: string, @CurrentUser() user: AuthUser) {
|
||||
const tenant = await this.tenantsService.findOne(id);
|
||||
this.assertProductAccess(user, tenant.product, 'write');
|
||||
return this.tenantsService.remove(id, user.id);
|
||||
}
|
||||
|
||||
private assertProductAccess(
|
||||
user: AuthUser | undefined,
|
||||
product: string,
|
||||
mode: 'read' | 'write',
|
||||
) {
|
||||
const code =
|
||||
mode === 'read'
|
||||
? productReadPermission(product)
|
||||
: productWritePermission(product);
|
||||
if (!userHasPermission(user || {}, code)) {
|
||||
throw new ForbiddenException('شما به این محصول دسترسی ندارید');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { AuthModule } from '../auth/auth.module';
|
||||
import { NotificationsModule } from '../notifications/notifications.module';
|
||||
import { TenantsController } from './tenants.controller';
|
||||
import { TenantsService } from './tenants.service';
|
||||
|
||||
@Module({
|
||||
imports: [NotificationsModule],
|
||||
imports: [NotificationsModule, AuthModule],
|
||||
controllers: [TenantsController],
|
||||
providers: [TenantsService],
|
||||
exports: [TenantsService],
|
||||
|
||||
Reference in New Issue
Block a user