feat(rbac): implement full admin management, dynamic roles, permissions matrix, and workspace access control

- Add Workspace, Role, Permission, RolePermission, AdminRoleAssignment, and AdminWorkspaceAssignment models to Prisma schema\n- Create database migration and RBAC seed script\n- Implement PermissionsGuard, @RequirePermission decorator, and real-time permission resolution in JwtAdminStrategy\n- Implement AdminManagementModule with endpoints for managers CRUD, roles matrix, permissions catalog, and workspaces\n- Secure all sensitive admin controllers with granular permission requirements\n- Add useAuthStore helpers (can, canAny, fetchMe) and v-permission directive\n- Implement Desktop views: AdminListView, AdminFormDrawer (RTL right-side), RoleMatrixView, WorkspaceListView, and ForbiddenView\n- Implement Mobile views: MAdminListView, MAdminFormView (stepped mobile form), MRoleMatrixView, MWorkspaceListView, and MForbiddenView\n- Fix sticky header behavior across desktop and mobile shells\n- Update route guards and device detection logic for mobile prefixing
This commit is contained in:
s.esmaeili
2026-10-02 16:29:23 +03:30
parent afde44ab8a
commit c5a841c35f
51 changed files with 5402 additions and 48 deletions
@@ -0,0 +1,118 @@
-- CreateEnum
CREATE TYPE "AccessScopeType" AS ENUM ('ALL', 'CUSTOM_CAMPAIGNS');
-- AlterTable
ALTER TABLE "admins" ADD COLUMN "is_super_admin" BOOLEAN NOT NULL DEFAULT false,
ADD COLUMN "last_login_at" TIMESTAMP(3);
-- AlterTable
ALTER TABLE "campaigns" ADD COLUMN "workspace_id" TEXT;
-- CreateTable
CREATE TABLE "workspaces" (
"id" TEXT NOT NULL,
"name" TEXT NOT NULL,
"slug" TEXT NOT NULL,
"description" TEXT,
"status" TEXT NOT NULL DEFAULT 'ACTIVE',
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "workspaces_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "roles" (
"id" TEXT NOT NULL,
"code" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
"is_system" BOOLEAN NOT NULL DEFAULT false,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
CONSTRAINT "roles_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "permissions" (
"id" TEXT NOT NULL,
"code" TEXT NOT NULL,
"module" TEXT NOT NULL,
"name" TEXT NOT NULL,
"description" TEXT,
CONSTRAINT "permissions_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "role_permissions" (
"role_id" TEXT NOT NULL,
"permission_id" TEXT NOT NULL,
CONSTRAINT "role_permissions_pkey" PRIMARY KEY ("role_id","permission_id")
);
-- CreateTable
CREATE TABLE "admin_role_assignments" (
"id" TEXT NOT NULL,
"admin_id" TEXT NOT NULL,
"role_id" TEXT NOT NULL,
"assigned_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "admin_role_assignments_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "admin_workspace_assignments" (
"id" TEXT NOT NULL,
"admin_id" TEXT NOT NULL,
"workspace_id" TEXT NOT NULL,
"scope_type" "AccessScopeType" NOT NULL DEFAULT 'ALL',
"specific_campaign_ids" JSONB,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "admin_workspace_assignments_pkey" PRIMARY KEY ("id")
);
-- CreateIndex
CREATE UNIQUE INDEX "workspaces_slug_key" ON "workspaces"("slug");
-- CreateIndex
CREATE UNIQUE INDEX "roles_code_key" ON "roles"("code");
-- CreateIndex
CREATE UNIQUE INDEX "permissions_code_key" ON "permissions"("code");
-- CreateIndex
CREATE INDEX "permissions_module_idx" ON "permissions"("module");
-- CreateIndex
CREATE UNIQUE INDEX "admin_role_assignments_admin_id_role_id_key" ON "admin_role_assignments"("admin_id", "role_id");
-- CreateIndex
CREATE UNIQUE INDEX "admin_workspace_assignments_admin_id_workspace_id_key" ON "admin_workspace_assignments"("admin_id", "workspace_id");
-- CreateIndex
CREATE INDEX "campaigns_workspace_id_idx" ON "campaigns"("workspace_id");
-- AddForeignKey
ALTER TABLE "campaigns" ADD CONSTRAINT "campaigns_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE SET NULL ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "role_permissions" ADD CONSTRAINT "role_permissions_role_id_fkey" FOREIGN KEY ("role_id") REFERENCES "roles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "role_permissions" ADD CONSTRAINT "role_permissions_permission_id_fkey" FOREIGN KEY ("permission_id") REFERENCES "permissions"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "admin_role_assignments" ADD CONSTRAINT "admin_role_assignments_admin_id_fkey" FOREIGN KEY ("admin_id") REFERENCES "admins"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "admin_role_assignments" ADD CONSTRAINT "admin_role_assignments_role_id_fkey" FOREIGN KEY ("role_id") REFERENCES "roles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "admin_workspace_assignments" ADD CONSTRAINT "admin_workspace_assignments_admin_id_fkey" FOREIGN KEY ("admin_id") REFERENCES "admins"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "admin_workspace_assignments" ADD CONSTRAINT "admin_workspace_assignments_workspace_id_fkey" FOREIGN KEY ("workspace_id") REFERENCES "workspaces"("id") ON DELETE CASCADE ON UPDATE CASCADE;
+95 -2
View File
@@ -21,6 +21,11 @@ enum AdminRole {
SUPPORT
}
enum AccessScopeType {
ALL
CUSTOM_CAMPAIGNS
}
enum UserStatus {
ACTIVE
BLOCKED
@@ -111,11 +116,15 @@ model Admin {
passwordHash String @map("password_hash")
role AdminRole @default(OPERATOR)
status AdminStatus @default(ACTIVE)
isSuperAdmin Boolean @default(false) @map("is_super_admin")
lastLoginAt DateTime? @map("last_login_at")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
auditLogs AdminLog[]
chanceTxns ChanceTransaction[] @relation("AdminCreatedTxn")
auditLogs AdminLog[]
chanceTxns ChanceTransaction[] @relation("AdminCreatedTxn")
roles AdminRoleAssignment[]
workspaceAssignments AdminWorkspaceAssignment[]
@@map("admins")
}
@@ -171,6 +180,8 @@ model Campaign {
ctaText String? @map("cta_text")
wheelDesign WheelDesign @default(CLASSIC_CASINO) @map("wheel_design")
settings Json? // primaryColor, secondaryColor, headerText, buttonText, headerImage, celebrationEffect...
workspaceId String? @map("workspace_id")
workspace Workspace? @relation(fields: [workspaceId], references: [id])
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
@@ -181,6 +192,7 @@ model Campaign {
userPrizes UserPrize[]
@@index([status, startAt, endAt])
@@index([workspaceId])
@@map("campaigns")
}
@@ -317,3 +329,84 @@ model AdminLog {
@@index([entityType, entityId])
@@map("admin_logs")
}
// ==================== WORKSPACE & RBAC MODELS ====================
model Workspace {
id String @id @default(uuid())
name String
slug String @unique
description String?
status String @default("ACTIVE")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
campaigns Campaign[]
adminAssignments AdminWorkspaceAssignment[]
@@map("workspaces")
}
model Role {
id String @id @default(uuid())
code String @unique // e.g. "SUPER_ADMIN", "OPERATOR", "SUPPORT"
name String // نام فارسی
description String?
isSystem Boolean @default(false) @map("is_system")
createdAt DateTime @default(now()) @map("created_at")
updatedAt DateTime @updatedAt @map("updated_at")
permissions RolePermission[]
admins AdminRoleAssignment[]
@@map("roles")
}
model Permission {
id String @id @default(uuid())
code String @unique // e.g. "campaigns:read", "campaigns:create"
module String // e.g. "campaigns", "users", "prizes", "reports", "admins"
name String // نام فارسی
description String?
roles RolePermission[]
@@index([module])
@@map("permissions")
}
model RolePermission {
roleId String @map("role_id")
role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)
permissionId String @map("permission_id")
permission Permission @relation(fields: [permissionId], references: [id], onDelete: Cascade)
@@id([roleId, permissionId])
@@map("role_permissions")
}
model AdminRoleAssignment {
id String @id @default(uuid())
adminId String @map("admin_id")
admin Admin @relation(fields: [adminId], references: [id], onDelete: Cascade)
roleId String @map("role_id")
role Role @relation(fields: [roleId], references: [id], onDelete: Cascade)
assignedAt DateTime @default(now()) @map("assigned_at")
@@unique([adminId, roleId])
@@map("admin_role_assignments")
}
model AdminWorkspaceAssignment {
id String @id @default(uuid())
adminId String @map("admin_id")
admin Admin @relation(fields: [adminId], references: [id], onDelete: Cascade)
workspaceId String @map("workspace_id")
workspace Workspace @relation(fields: [workspaceId], references: [id], onDelete: Cascade)
scopeType AccessScopeType @default(ALL) @map("scope_type")
specificCampaignIds Json? @map("specific_campaign_ids")
createdAt DateTime @default(now()) @map("created_at")
@@unique([adminId, workspaceId])
@@map("admin_workspace_assignments")
}
+272
View File
@@ -0,0 +1,272 @@
import { AccessScopeType, PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
const PERMISSIONS = [
// Campaigns
{ code: 'campaigns:read', module: 'campaigns', name: 'مشاهده کمپین‌ها', description: 'مشاهده لیست و جزئیات کمپین‌ها' },
{ code: 'campaigns:create', module: 'campaigns', name: 'ایجاد کمپین', description: 'ثبت کمپین جدید' },
{ code: 'campaigns:update', module: 'campaigns', name: 'ویرایش کمپین', description: 'تغییر تنظیمات و وضعیت کمپین' },
{ code: 'campaigns:delete', module: 'campaigns', name: 'حذف کمپین', description: 'حذف کمپین از سامانه' },
// Prizes
{ code: 'prizes:read', module: 'prizes', name: 'مشاهده جوایز', description: 'مشاهده لیست جوایز کمپین‌ها' },
{ code: 'prizes:manage', module: 'prizes', name: 'مدیریت جوایز', description: 'افزودن، ویرایش و تنظیم ظرفیت جوایز' },
// Users & Chances
{ code: 'users:read', module: 'users', name: 'مشاهده کاربران', description: 'مشاهده لیست کاربران شرکت‌کننده و پروفایل' },
{ code: 'users:block', module: 'users', name: 'مسدودسازی کاربران', description: 'بلاک یا آن‌بلاک کردن کاربران' },
{ code: 'chances:adjust', module: 'chances', name: 'تنظیم دستی شانس', description: 'افزایش یا کاهش شانس با ثبت لاگ' },
// Winners & Spins
{ code: 'spins:read', module: 'spins', name: 'مشاهده لاگ چرخش‌ها', description: 'بررسی تاریخچه اسپین‌های کاربران' },
{ code: 'winners:read', module: 'winners', name: 'مشاهده برندگان', description: 'مشاهده لیست برندگان و کدهای هدیه' },
{ code: 'winners:manage', module: 'winners', name: 'مدیریت وضعیت جوایز', description: 'تغییر وضعیت کدهای جایزه به تحویل‌شده و...' },
// SMS & Logs
{ code: 'sms:read', module: 'sms', name: 'مشاهده لاگ پیامک', description: 'مشاهده پیامک‌های ارسالی OTP و برندگان' },
{ code: 'sms:resend', module: 'sms', name: 'ارسال مجدد پیامک', description: 'ارسال دوباره پیامک‌های ناموفق' },
{ code: 'audit:read', module: 'audit', name: 'مشاهده لاگ بازرسی', description: 'مشاهده ردپای عملیات و تغییرات مدیران' },
// Reports
{ code: 'reports:read', module: 'reports', name: 'مشاهده گزارش‌ها و آمار', description: 'مشاهده نمودارها، آمار زنده و داشبورد' },
{ code: 'reports:export', module: 'reports', name: 'خروجی داده‌ها', description: 'دریافت خروجی اکسل و CSV گزارش‌ها' },
// Access Control & Admins
{ code: 'admins:read', module: 'admins', name: 'مشاهده مدیران', description: 'مشاهده لیست مدیران و همکاران' },
{ code: 'admins:manage', module: 'admins', name: 'مدیریت مدیران', description: 'ایجاد، ویرایش، تغییر رمز و غیرفعال‌سازی مدیران' },
{ code: 'roles:manage', module: 'roles', name: 'مدیریت نقش‌ها و مجوزها', description: 'تعریف نقش جدید و تخصیص مجوزها' },
{ code: 'workspaces:manage', module: 'workspaces', name: 'مدیریت فضاهای کاری', description: 'تعریف و مدیریت فضاهای کاری سازمانی' },
];
async function main() {
console.log('Seeding RBAC and Workspaces...');
// 1. Ensure Default Workspace exists
let defaultWorkspace = await prisma.workspace.findUnique({
where: { slug: 'default-workspace' },
});
if (!defaultWorkspace) {
defaultWorkspace = await prisma.workspace.create({
data: {
name: 'فضای کاری اصلی',
slug: 'default-workspace',
description: 'فضای کاری پیش‌فرض و سراسری سیستم گردونه شانس',
status: 'ACTIVE',
},
});
console.log('Created default workspace:', defaultWorkspace.id);
}
// Link any unassigned campaigns to default workspace
await prisma.campaign.updateMany({
where: { workspaceId: null },
data: { workspaceId: defaultWorkspace.id },
});
// 2. Seed Permissions
const permissionMap = new Map<string, string>();
for (const perm of PERMISSIONS) {
const p = await prisma.permission.upsert({
where: { code: perm.code },
update: {
module: perm.module,
name: perm.name,
description: perm.description,
},
create: {
code: perm.code,
module: perm.module,
name: perm.name,
description: perm.description,
},
});
permissionMap.set(p.code, p.id);
}
console.log(`Seeded ${permissionMap.size} permissions.`);
// 3. Seed Roles
// SUPER_ADMIN (All permissions)
const superAdminRole = await prisma.role.upsert({
where: { code: 'SUPER_ADMIN' },
update: {
name: 'مدیر ارشد سامانه',
description: 'دسترسی کامل و بدون محدودیت به تمامی بخش‌ها و تنظیمات سامانه',
isSystem: true,
},
create: {
code: 'SUPER_ADMIN',
name: 'مدیر ارشد سامانه',
description: 'دسترسی کامل و بدون محدودیت به تمامی بخش‌ها و تنظیمات سامانه',
isSystem: true,
},
});
// OPERATOR (Operational permissions)
const operatorRole = await prisma.role.upsert({
where: { code: 'OPERATOR' },
update: {
name: 'اپراتور سامانه',
description: 'مدیریت کمپین‌ها، جوایز، شانس‌ها، برندگان و کاربران بدون دسترسی به مدیریت ادمین‌ها',
isSystem: true,
},
create: {
code: 'OPERATOR',
name: 'اپراتور سامانه',
description: 'مدیریت کمپین‌ها، جوایز، شانس‌ها، برندگان و کاربران بدون دسترسی به مدیریت ادمین‌ها',
isSystem: true,
},
});
// SUPPORT (Read-only + winner management)
const supportRole = await prisma.role.upsert({
where: { code: 'SUPPORT' },
update: {
name: 'کارشناس پشتیبانی',
description: 'مشاهده کاربران، برندگان، پیامک‌ها و تغییر وضعیت تحویل جوایز',
isSystem: true,
},
create: {
code: 'SUPPORT',
name: 'کارشناس پشتیبانی',
description: 'مشاهده کاربران، برندگان، پیامک‌ها و تغییر وضعیت تحویل جوایز',
isSystem: true,
},
});
// 4. Assign permissions to roles
// SUPER_ADMIN gets all permissions
for (const permId of permissionMap.values()) {
await prisma.rolePermission.upsert({
where: {
roleId_permissionId: {
roleId: superAdminRole.id,
permissionId: permId,
},
},
update: {},
create: {
roleId: superAdminRole.id,
permissionId: permId,
},
});
}
// OPERATOR permissions
const operatorPermCodes = [
'campaigns:read', 'campaigns:create', 'campaigns:update', 'campaigns:delete',
'prizes:read', 'prizes:manage',
'users:read', 'users:block', 'chances:adjust',
'spins:read', 'winners:read', 'winners:manage',
'sms:read', 'sms:resend',
'reports:read', 'reports:export',
'audit:read',
];
for (const code of operatorPermCodes) {
const permId = permissionMap.get(code);
if (permId) {
await prisma.rolePermission.upsert({
where: {
roleId_permissionId: {
roleId: operatorRole.id,
permissionId: permId,
},
},
update: {},
create: {
roleId: operatorRole.id,
permissionId: permId,
},
});
}
}
// SUPPORT permissions
const supportPermCodes = [
'campaigns:read',
'prizes:read',
'users:read',
'spins:read',
'winners:read', 'winners:manage',
'sms:read',
];
for (const code of supportPermCodes) {
const permId = permissionMap.get(code);
if (permId) {
await prisma.rolePermission.upsert({
where: {
roleId_permissionId: {
roleId: supportRole.id,
permissionId: permId,
},
},
update: {},
create: {
roleId: supportRole.id,
permissionId: permId,
},
});
}
}
// 5. Backfill existing admins
const admins = await prisma.admin.findMany();
for (const admin of admins) {
const isSuper = admin.role === 'SUPER_ADMIN';
await prisma.admin.update({
where: { id: admin.id },
data: {
isSuperAdmin: isSuper,
},
});
const targetRoleId = isSuper
? superAdminRole.id
: admin.role === 'OPERATOR'
? operatorRole.id
: supportRole.id;
await prisma.adminRoleAssignment.upsert({
where: {
adminId_roleId: {
adminId: admin.id,
roleId: targetRoleId,
},
},
update: {},
create: {
adminId: admin.id,
roleId: targetRoleId,
},
});
await prisma.adminWorkspaceAssignment.upsert({
where: {
adminId_workspaceId: {
adminId: admin.id,
workspaceId: defaultWorkspace.id,
},
},
update: {},
create: {
adminId: admin.id,
workspaceId: defaultWorkspace.id,
scopeType: AccessScopeType.ALL,
},
});
}
console.log(`Backfilled ${admins.length} existing admins with roles and workspace assignments.`);
console.log('RBAC seed completed successfully.');
}
main()
.catch((e) => {
console.error(e);
process.exit(1);
})
.finally(async () => {
await prisma.$disconnect();
});
+2
View File
@@ -6,6 +6,7 @@ import { ThrottlerModule } from '@nestjs/throttler';
import { validateEnv } from './config/env.validation';
import { AdminAuditModule } from './modules/admin-audit/admin-audit.module';
import { AdminAuthModule } from './modules/admin-auth/admin-auth.module';
import { AdminManagementModule } from './modules/admin-management/admin-management.module';
import { AuthModule } from './modules/auth/auth.module';
import { CampaignModule } from './modules/campaign/campaign.module';
import { ChanceModule } from './modules/chance/chance.module';
@@ -57,6 +58,7 @@ import { PrismaModule } from './prisma/prisma.module';
WinnerModule,
ReportModule,
AdminAuditModule,
AdminManagementModule,
],
})
export class AppModule {}
@@ -7,6 +7,13 @@ export interface AuthenticatedAdmin {
mobile: string;
name: string;
role: AdminRole;
isSuperAdmin?: boolean;
permissions?: string[];
workspaceAssignments?: Array<{
workspaceId: string;
scopeType: string;
specificCampaignIds?: string[] | null;
}>;
}
export const CurrentAdmin = createParamDecorator(
@@ -0,0 +1,6 @@
import { SetMetadata } from '@nestjs/common';
export const PERMISSIONS_KEY = 'permissions';
export const RequirePermission = (...permissions: string[]) =>
SetMetadata(PERMISSIONS_KEY, permissions);
@@ -0,0 +1,43 @@
import { CanActivate, ExecutionContext, ForbiddenException, Injectable } from '@nestjs/common';
import { Reflector } from '@nestjs/core';
import { PERMISSIONS_KEY } from '../decorators/require-permission.decorator';
@Injectable()
export class PermissionsGuard implements CanActivate {
constructor(private readonly reflector: Reflector) {}
canActivate(context: ExecutionContext): boolean {
const requiredPermissions = this.reflector.getAllAndOverride<string[]>(PERMISSIONS_KEY, [
context.getHandler(),
context.getClass(),
]);
// If no permission is required on the route, allow access
if (!requiredPermissions || requiredPermissions.length === 0) {
return true;
}
const request = context.switchToHttp().getRequest();
const admin = request.admin || request.user;
if (!admin) {
throw new ForbiddenException('ACCESS_DENIED');
}
// Super Admin has full unrestricted bypass access
if (admin.isSuperAdmin === true || admin.role === 'SUPER_ADMIN') {
return true;
}
const adminPermissions: string[] = admin.permissions || [];
// Check if admin has all required permissions (or at least one if multiple are given)
const hasRequired = requiredPermissions.every((perm) => adminPermissions.includes(perm));
if (!hasRequired) {
throw new ForbiddenException('INSUFFICIENT_PERMISSIONS');
}
return true;
}
}
@@ -1,16 +1,19 @@
import { Controller, Get, Query, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger';
import { JwtAdminGuard } from '../../common/guards/jwt-admin.guard';
import { PermissionsGuard } from '../../common/guards/permissions.guard';
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
import { AdminAuditService } from './admin-audit.service';
@ApiTags('Admin Audit Logs (لاگ‌های بازرسی مدیران)')
@Controller('admin/audit-logs')
@UseGuards(JwtAdminGuard)
@UseGuards(JwtAdminGuard, PermissionsGuard)
@ApiBearerAuth('AdminAuth')
export class AdminAuditController {
constructor(private readonly auditService: AdminAuditService) {}
@Get()
@RequirePermission('audit:read')
@ApiOperation({ summary: 'مشاهده تمام لاگ‌های بازرسی اقدامات ادمین با فیلتر' })
@ApiQuery({ name: 'page', required: false })
@ApiQuery({ name: 'pageSize', required: false })
@@ -1,4 +1,4 @@
import { Body, Controller, HttpCode, HttpStatus, Patch, Post, Req, UseGuards } from '@nestjs/common';
import { Body, Controller, Get, HttpCode, HttpStatus, Patch, Post, Req, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { Throttle } from '@nestjs/throttler';
import { JwtAdminGuard } from '../../common/guards/jwt-admin.guard';
@@ -31,6 +31,14 @@ export class AdminAuthController {
return this.adminAuthService.refreshToken(dto);
}
@Get('me')
@UseGuards(JwtAdminGuard)
@ApiBearerAuth('AdminAuth')
@ApiOperation({ summary: 'دریافت مشخصات کامل و مجوزهای مدیر جاری' })
async getMe(@Req() req: any) {
return this.adminAuthService.getProfile(req.user.id);
}
@Patch('profile')
@UseGuards(JwtAdminGuard)
@HttpCode(HttpStatus.OK)
@@ -56,6 +56,26 @@ export class AdminAuthService implements OnModuleInit {
const { email, password } = dto;
const admin = await this.prisma.admin.findUnique({
where: { email: email.toLowerCase() },
include: {
roles: {
include: {
role: {
include: {
permissions: {
include: {
permission: true,
},
},
},
},
},
},
workspaceAssignments: {
include: {
workspace: true,
},
},
},
});
if (!admin) {
@@ -71,6 +91,19 @@ export class AdminAuthService implements OnModuleInit {
throw new UnauthorizedException('ایمیل یا کلمه عبور اشتباه است.');
}
// Update lastLoginAt
await this.prisma.admin.update({
where: { id: admin.id },
data: { lastLoginAt: new Date() },
});
const permissions = new Set<string>();
admin.roles.forEach((ra) => {
ra.role.permissions.forEach((rp) => {
permissions.add(rp.permission.code);
});
});
const tokens = this.generateAdminTokens(admin);
return {
@@ -81,6 +114,19 @@ export class AdminAuthService implements OnModuleInit {
email: admin.email,
mobile: admin.mobile,
role: admin.role,
isSuperAdmin: admin.isSuperAdmin,
permissions: Array.from(permissions),
roles: admin.roles.map((ra) => ({
id: ra.role.id,
code: ra.role.code,
name: ra.role.name,
})),
workspaces: admin.workspaceAssignments.map((wa) => ({
workspaceId: wa.workspaceId,
workspaceName: wa.workspace.name,
scopeType: wa.scopeType,
specificCampaignIds: wa.specificCampaignIds,
})),
},
};
}
@@ -183,9 +229,68 @@ export class AdminAuthService implements OnModuleInit {
mobile: true,
role: true,
status: true,
isSuperAdmin: true,
},
});
return updated;
}
async getProfile(adminId: string) {
const admin = await this.prisma.admin.findUnique({
where: { id: adminId },
include: {
roles: {
include: {
role: {
include: {
permissions: {
include: {
permission: true,
},
},
},
},
},
},
workspaceAssignments: {
include: {
workspace: true,
},
},
},
});
if (!admin || admin.status !== AdminStatus.ACTIVE) {
throw new UnauthorizedException('حساب کاربری یافت نشد یا غیرفعال است.');
}
const permissions = new Set<string>();
admin.roles.forEach((ra) => {
ra.role.permissions.forEach((rp) => {
permissions.add(rp.permission.code);
});
});
return {
id: admin.id,
name: admin.name,
email: admin.email,
mobile: admin.mobile,
role: admin.role,
isSuperAdmin: admin.isSuperAdmin,
permissions: Array.from(permissions),
roles: admin.roles.map((ra) => ({
id: ra.role.id,
code: ra.role.code,
name: ra.role.name,
})),
workspaces: admin.workspaceAssignments.map((wa) => ({
workspaceId: wa.workspaceId,
workspaceName: wa.workspace.name,
scopeType: wa.scopeType,
specificCampaignIds: wa.specificCampaignIds,
})),
};
}
}
@@ -25,15 +25,58 @@ export class JwtAdminStrategy extends PassportStrategy(Strategy, 'jwt-admin') {
const admin = await this.prisma.admin.findUnique({
where: { id: payload.sub },
select: { id: true, email: true, name: true, mobile: true, role: true, status: true },
select: {
id: true,
email: true,
name: true,
mobile: true,
role: true,
status: true,
isSuperAdmin: true,
roles: {
include: {
role: {
include: {
permissions: {
include: {
permission: true,
},
},
},
},
},
},
workspaceAssignments: {
select: {
workspaceId: true,
scopeType: true,
specificCampaignIds: true,
},
},
},
});
if (!admin || admin.status !== AdminStatus.ACTIVE) {
throw new UnauthorizedException('ADMIN_INACTIVE_OR_NOT_FOUND');
}
const permissions = new Set<string>();
admin.roles.forEach((ra) => {
ra.role.permissions.forEach((rp) => {
permissions.add(rp.permission.code);
});
});
return {
...admin,
id: admin.id,
email: admin.email,
name: admin.name,
mobile: admin.mobile,
role: admin.role,
status: admin.status,
isSuperAdmin: admin.isSuperAdmin,
permissions: Array.from(permissions),
workspaceAssignments: admin.workspaceAssignments,
type: 'admin',
};
}
@@ -0,0 +1,103 @@
import {
Body,
Controller,
Delete,
Get,
HttpCode,
HttpStatus,
Param,
Patch,
Post,
Query,
UseGuards,
UseInterceptors,
} from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger';
import { AdminStatus } from '@prisma/client';
import { CurrentAdmin } from '../../common/decorators/current-admin.decorator';
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
import { JwtAdminGuard } from '../../common/guards/jwt-admin.guard';
import { PermissionsGuard } from '../../common/guards/permissions.guard';
import { AuditInterceptor } from '../../common/interceptors/audit.interceptor';
import { AdminManagementService } from './admin-management.service';
import { CreateAdminDto } from './dto/create-admin.dto';
import { UpdateAdminDto } from './dto/update-admin.dto';
@ApiTags('Admin Management (مدیریت مدیران و دسترسی‌ها)')
@Controller('admin/managers')
@UseGuards(JwtAdminGuard, PermissionsGuard)
@UseInterceptors(AuditInterceptor)
@ApiBearerAuth('AdminAuth')
export class AdminManagementController {
constructor(private readonly service: AdminManagementService) {}
@Get()
@RequirePermission('admins:read')
@ApiOperation({ summary: 'لیست مدیران سامانه با فیلتر جستجو، نقش و وضعیت' })
@ApiQuery({ name: 'search', required: false })
@ApiQuery({ name: 'roleCode', required: false })
@ApiQuery({ name: 'status', enum: AdminStatus, required: false })
@ApiQuery({ name: 'page', required: false })
@ApiQuery({ name: 'pageSize', required: false })
async findAll(@Query() query: any) {
return this.service.findAllAdmins(query);
}
@Get(':id')
@RequirePermission('admins:read')
@ApiOperation({ summary: 'مشاهده مشخصات کامل مدیر، نقش‌ها و فضاهای کاری' })
async findOne(@Param('id') id: string) {
return this.service.findAdminById(id);
}
@Post()
@RequirePermission('admins:manage')
@ApiOperation({ summary: 'افزودن مدیر جدید به همراه نقش‌ها و اسکوپ فضاهای کاری' })
@ApiResponse({ status: 201, description: 'مدیر با موفقیت ایجاد شد' })
async create(@Body() dto: CreateAdminDto) {
return this.service.createAdmin(dto);
}
@Patch(':id')
@RequirePermission('admins:manage')
@ApiOperation({ summary: 'ویرایش مشخصات مدیر و نقش‌ها/فضاهای کاری' })
async update(
@Param('id') id: string,
@Body() dto: UpdateAdminDto,
@CurrentAdmin('id') currentAdminId: string,
) {
return this.service.updateAdmin(id, dto, currentAdminId);
}
@Patch(':id/status')
@RequirePermission('admins:manage')
@ApiOperation({ summary: 'تغییر وضعیت فعال یا غیرفعال مدیر' })
async toggleStatus(
@Param('id') id: string,
@Body('status') status: AdminStatus,
@CurrentAdmin('id') currentAdminId: string,
) {
return this.service.toggleAdminStatus(id, status, currentAdminId);
}
@Patch(':id/password')
@RequirePermission('admins:manage')
@ApiOperation({ summary: 'بازنشانی کلمه عبور مدیر توسط سوپرادمین' })
async resetPassword(
@Param('id') id: string,
@Body('password') password: string,
) {
return this.service.updateAdmin(id, { password }, '');
}
@Delete(':id')
@RequirePermission('admins:manage')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'حذف مدیر (در صورت عدم وجود سوابق تراکنش)' })
async delete(
@Param('id') id: string,
@CurrentAdmin('id') currentAdminId: string,
) {
return this.service.deleteAdmin(id, currentAdminId);
}
}
@@ -0,0 +1,18 @@
import { Module } from '@nestjs/common';
import { PrismaModule } from '../../prisma/prisma.module';
import { AdminManagementController } from './admin-management.controller';
import { AdminManagementService } from './admin-management.service';
import { RoleManagementController } from './role-management.controller';
import { WorkspaceController } from './workspace.controller';
@Module({
imports: [PrismaModule],
controllers: [
AdminManagementController,
RoleManagementController,
WorkspaceController,
],
providers: [AdminManagementService],
exports: [AdminManagementService],
})
export class AdminManagementModule {}
@@ -0,0 +1,611 @@
import {
BadRequestException,
ConflictException,
Injectable,
NotFoundException,
} from '@nestjs/common';
import { AccessScopeType, AdminRole, AdminStatus } from '@prisma/client';
import * as bcrypt from 'bcrypt';
import { PrismaService } from '../../prisma/prisma.service';
import { CreateAdminDto } from './dto/create-admin.dto';
import { CreateRoleDto } from './dto/create-role.dto';
import { CreateWorkspaceDto } from './dto/create-workspace.dto';
import { UpdateAdminDto } from './dto/update-admin.dto';
import { UpdateRoleDto } from './dto/update-role.dto';
import { UpdateWorkspaceDto } from './dto/update-workspace.dto';
@Injectable()
export class AdminManagementService {
constructor(private readonly prisma: PrismaService) {}
// =========================================================================
// ADMINS CRUD
// =========================================================================
async findAllAdmins(params: {
search?: string;
roleCode?: string;
status?: AdminStatus;
page?: number;
pageSize?: number;
}) {
const page = Math.max(1, Number(params.page) || 1);
const pageSize = Math.min(100, Math.max(1, Number(params.pageSize) || 10));
const skip = (page - 1) * pageSize;
const where: any = {};
if (params.status) {
where.status = params.status;
}
if (params.search && params.search.trim()) {
const q = params.search.trim();
where.OR = [
{ name: { contains: q, mode: 'insensitive' } },
{ email: { contains: q, mode: 'insensitive' } },
{ mobile: { contains: q } },
];
}
if (params.roleCode) {
where.roles = {
some: {
role: {
code: params.roleCode,
},
},
};
}
const [total, admins] = await Promise.all([
this.prisma.admin.count({ where }),
this.prisma.admin.findMany({
where,
skip,
take: pageSize,
orderBy: { createdAt: 'desc' },
select: {
id: true,
name: true,
email: true,
mobile: true,
role: true,
status: true,
isSuperAdmin: true,
lastLoginAt: true,
createdAt: true,
updatedAt: true,
roles: {
select: {
role: {
select: {
id: true,
code: true,
name: true,
isSystem: true,
},
},
},
},
workspaceAssignments: {
select: {
workspace: {
select: {
id: true,
name: true,
slug: true,
},
},
scopeType: true,
specificCampaignIds: true,
},
},
},
}),
]);
return {
data: admins,
meta: {
page,
pageSize,
total,
totalPages: Math.ceil(total / pageSize),
},
};
}
async findAdminById(id: string) {
const admin = await this.prisma.admin.findUnique({
where: { id },
include: {
roles: {
include: {
role: {
include: {
permissions: {
include: {
permission: true,
},
},
},
},
},
},
workspaceAssignments: {
include: {
workspace: true,
},
},
auditLogs: {
take: 10,
orderBy: { createdAt: 'desc' },
},
},
});
if (!admin) {
throw new NotFoundException('مدیر مورد نظر یافت نشد.');
}
const { passwordHash, ...safeAdmin } = admin;
return safeAdmin;
}
async createAdmin(dto: CreateAdminDto) {
const email = dto.email.toLowerCase().trim();
const mobile = dto.mobile.trim();
const [existingEmail, existingMobile] = await Promise.all([
this.prisma.admin.findUnique({ where: { email } }),
this.prisma.admin.findUnique({ where: { mobile } }),
]);
if (existingEmail) {
throw new ConflictException('این آدرس ایمیل قبلاً در سامانه ثبت شده است.');
}
if (existingMobile) {
throw new ConflictException('این شماره موبایل قبلاً برای مدیر دیگری ثبت شده است.');
}
const passwordHash = await bcrypt.hash(dto.password, 10);
return this.prisma.$transaction(async (tx) => {
const admin = await tx.admin.create({
data: {
name: dto.name.trim(),
email,
mobile,
passwordHash,
status: dto.status || AdminStatus.ACTIVE,
isSuperAdmin: dto.isSuperAdmin || false,
role: dto.isSuperAdmin ? AdminRole.SUPER_ADMIN : AdminRole.OPERATOR,
},
});
// Role assignments
if (dto.roleIds && dto.roleIds.length > 0) {
await tx.adminRoleAssignment.createMany({
data: dto.roleIds.map((roleId) => ({
adminId: admin.id,
roleId,
})),
});
}
// Workspace assignments
if (dto.workspaceAssignments && dto.workspaceAssignments.length > 0) {
await tx.adminWorkspaceAssignment.createMany({
data: dto.workspaceAssignments.map((wa) => ({
adminId: admin.id,
workspaceId: wa.workspaceId,
scopeType: wa.scopeType || AccessScopeType.ALL,
specificCampaignIds: wa.specificCampaignIds ? wa.specificCampaignIds : undefined,
})),
});
} else {
// Assign to default workspace if none specified
const defaultWs = await tx.workspace.findUnique({ where: { slug: 'default-workspace' } });
if (defaultWs) {
await tx.adminWorkspaceAssignment.create({
data: {
adminId: admin.id,
workspaceId: defaultWs.id,
scopeType: AccessScopeType.ALL,
},
});
}
}
return tx.admin.findUnique({
where: { id: admin.id },
select: {
id: true,
name: true,
email: true,
mobile: true,
status: true,
isSuperAdmin: true,
createdAt: true,
},
});
});
}
async updateAdmin(id: string, dto: UpdateAdminDto, currentAdminId: string) {
const admin = await this.prisma.admin.findUnique({ where: { id } });
if (!admin) {
throw new NotFoundException('مدیر مورد نظر یافت نشد.');
}
// Protection rule: Self modification of status / isSuperAdmin
if (id === currentAdminId) {
if (dto.status !== undefined && dto.status !== admin.status) {
throw new BadRequestException('امکان تغییر وضعیت حساب کاربری جاری توسط خودتان وجود ندارد.');
}
if (dto.isSuperAdmin !== undefined && dto.isSuperAdmin !== admin.isSuperAdmin) {
throw new BadRequestException('امکان تغییر اختیارات ارشد حساب کاربری خودتان وجود ندارد.');
}
}
// Protection rule: Last Super Admin demotion/deactivation
if (admin.isSuperAdmin) {
const willDemoteOrDeactivate =
(dto.isSuperAdmin === false) ||
(dto.status === AdminStatus.DISABLED);
if (willDemoteOrDeactivate) {
const activeSuperAdminCount = await this.prisma.admin.count({
where: { isSuperAdmin: true, status: AdminStatus.ACTIVE },
});
if (activeSuperAdminCount <= 1) {
throw new BadRequestException('امکان غیرفعال‌سازی یا خلع اختیارات آخرین مدیر ارشد سامانه وجود ندارد.');
}
}
}
const dataToUpdate: any = {};
if (dto.name) dataToUpdate.name = dto.name.trim();
if (dto.email) {
const emailLower = dto.email.toLowerCase().trim();
const existing = await this.prisma.admin.findFirst({
where: { email: emailLower, id: { not: id } },
});
if (existing) {
throw new ConflictException('این ایمیل قبلاً برای مدیر دیگری ثبت شده است.');
}
dataToUpdate.email = emailLower;
}
if (dto.mobile) {
const mobileClean = dto.mobile.trim();
const existing = await this.prisma.admin.findFirst({
where: { mobile: mobileClean, id: { not: id } },
});
if (existing) {
throw new ConflictException('این شماره موبایل قبلاً برای مدیر دیگری ثبت شده است.');
}
dataToUpdate.mobile = mobileClean;
}
if (dto.password) {
dataToUpdate.passwordHash = await bcrypt.hash(dto.password, 10);
}
if (dto.status !== undefined) {
dataToUpdate.status = dto.status;
}
if (dto.isSuperAdmin !== undefined) {
dataToUpdate.isSuperAdmin = dto.isSuperAdmin;
if (dto.isSuperAdmin) {
dataToUpdate.role = AdminRole.SUPER_ADMIN;
}
}
return this.prisma.$transaction(async (tx) => {
await tx.admin.update({
where: { id },
data: dataToUpdate,
});
// Update role assignments if explicitly provided
if (dto.roleIds !== undefined) {
await tx.adminRoleAssignment.deleteMany({ where: { adminId: id } });
if (dto.roleIds.length > 0) {
await tx.adminRoleAssignment.createMany({
data: dto.roleIds.map((roleId) => ({
adminId: id,
roleId,
})),
});
}
}
// Update workspace assignments if explicitly provided
if (dto.workspaceAssignments !== undefined) {
await tx.adminWorkspaceAssignment.deleteMany({ where: { adminId: id } });
if (dto.workspaceAssignments.length > 0) {
await tx.adminWorkspaceAssignment.createMany({
data: dto.workspaceAssignments.map((wa) => ({
adminId: id,
workspaceId: wa.workspaceId,
scopeType: wa.scopeType || AccessScopeType.ALL,
specificCampaignIds: wa.specificCampaignIds ? wa.specificCampaignIds : undefined,
})),
});
}
}
return tx.admin.findUnique({
where: { id },
select: {
id: true,
name: true,
email: true,
mobile: true,
status: true,
isSuperAdmin: true,
updatedAt: true,
},
});
});
}
async toggleAdminStatus(id: string, status: AdminStatus, currentAdminId: string) {
if (id === currentAdminId) {
throw new BadRequestException('امکان غیرفعال‌سازی حساب کاربری جاری توسط خودتان وجود ندارد.');
}
const admin = await this.prisma.admin.findUnique({ where: { id } });
if (!admin) {
throw new NotFoundException('مدیر مورد نظر یافت نشد.');
}
if (admin.isSuperAdmin && status === AdminStatus.DISABLED) {
const activeSuperAdminCount = await this.prisma.admin.count({
where: { isSuperAdmin: true, status: AdminStatus.ACTIVE },
});
if (activeSuperAdminCount <= 1) {
throw new BadRequestException('امکان غیرفعال‌سازی آخرین مدیر ارشد سامانه وجود ندارد.');
}
}
return this.prisma.admin.update({
where: { id },
data: { status },
select: { id: true, name: true, status: true },
});
}
async deleteAdmin(id: string, currentAdminId: string) {
if (id === currentAdminId) {
throw new BadRequestException('امکان حذف حساب کاربری خودتان وجود ندارد.');
}
const admin = await this.prisma.admin.findUnique({
where: { id },
include: {
chanceTxns: { take: 1 },
},
});
if (!admin) {
throw new NotFoundException('مدیر مورد نظر یافت نشد.');
}
if (admin.isSuperAdmin) {
const activeSuperAdminCount = await this.prisma.admin.count({
where: { isSuperAdmin: true, status: AdminStatus.ACTIVE },
});
if (activeSuperAdminCount <= 1) {
throw new BadRequestException('امکان حذف آخرین مدیر ارشد سامانه وجود ندارد.');
}
}
if (admin.chanceTxns && admin.chanceTxns.length > 0) {
throw new BadRequestException(
'این مدیر دارای سوابق ثبت یا تغییر شانس است و حذف فیزیکی آن یکپارچگی داده‌ها را مخدوش می‌کند. لطفاً وضعیت حساب آن را به «غیرفعال» تغییر دهید.',
);
}
await this.prisma.admin.delete({ where: { id } });
return { success: true, message: 'مدیر با موفقیت حذف شد.' };
}
// =========================================================================
// ROLES & PERMISSIONS
// =========================================================================
async findAllRoles() {
return this.prisma.role.findMany({
orderBy: { createdAt: 'asc' },
include: {
permissions: {
include: {
permission: true,
},
},
_count: {
select: { admins: true },
},
},
});
}
async findRoleById(id: string) {
const role = await this.prisma.role.findUnique({
where: { id },
include: {
permissions: {
include: {
permission: true,
},
},
},
});
if (!role) {
throw new NotFoundException('نقش مورد نظر یافت نشد.');
}
return role;
}
async createRole(dto: CreateRoleDto) {
const existing = await this.prisma.role.findUnique({
where: { code: dto.code.toUpperCase().trim() },
});
if (existing) {
throw new ConflictException('نقشی با این کد انگلیسی قبلاً ثبت شده است.');
}
return this.prisma.$transaction(async (tx) => {
const role = await tx.role.create({
data: {
code: dto.code.toUpperCase().trim(),
name: dto.name.trim(),
description: dto.description?.trim(),
isSystem: false,
},
});
if (dto.permissionIds && dto.permissionIds.length > 0) {
await tx.rolePermission.createMany({
data: dto.permissionIds.map((permId) => ({
roleId: role.id,
permissionId: permId,
})),
});
}
return tx.role.findUnique({
where: { id: role.id },
include: {
permissions: {
include: { permission: true },
},
},
});
});
}
async updateRole(id: string, dto: UpdateRoleDto) {
const role = await this.prisma.role.findUnique({ where: { id } });
if (!role) {
throw new NotFoundException('نقش مورد نظر یافت نشد.');
}
return this.prisma.$transaction(async (tx) => {
await tx.role.update({
where: { id },
data: {
name: dto.name?.trim(),
description: dto.description?.trim(),
},
});
if (dto.permissionIds !== undefined) {
await tx.rolePermission.deleteMany({ where: { roleId: id } });
if (dto.permissionIds.length > 0) {
await tx.rolePermission.createMany({
data: dto.permissionIds.map((permId) => ({
roleId: id,
permissionId: permId,
})),
});
}
}
return tx.role.findUnique({
where: { id },
include: {
permissions: {
include: { permission: true },
},
},
});
});
}
async deleteRole(id: string) {
const role = await this.prisma.role.findUnique({
where: { id },
include: {
_count: {
select: { admins: true },
},
},
});
if (!role) {
throw new NotFoundException('نقش مورد نظر یافت نشد.');
}
if (role.isSystem) {
throw new BadRequestException('امکان حذف نقش‌های پیش‌فرض و سیستمی وجود ندارد.');
}
if (role._count.admins > 0) {
throw new BadRequestException('به دلیل وجود مدیران منتسب به این نقش، ابتدا نقش آن‌ها را تغییر داده و سپس اقدام به حذف نمایید.');
}
await this.prisma.role.delete({ where: { id } });
return { success: true, message: 'نقش با موفقیت حذف شد.' };
}
async findAllPermissions() {
return this.prisma.permission.findMany({
orderBy: [{ module: 'asc' }, { code: 'asc' }],
});
}
// =========================================================================
// WORKSPACES
// =========================================================================
async findAllWorkspaces() {
return this.prisma.workspace.findMany({
orderBy: { createdAt: 'asc' },
include: {
_count: {
select: { campaigns: true, adminAssignments: true },
},
},
});
}
async createWorkspace(dto: CreateWorkspaceDto) {
const existing = await this.prisma.workspace.findUnique({
where: { slug: dto.slug.toLowerCase().trim() },
});
if (existing) {
throw new ConflictException('فضای کاری با این اسلاگ انگلیسی قبلاً ثبت شده است.');
}
return this.prisma.workspace.create({
data: {
name: dto.name.trim(),
slug: dto.slug.toLowerCase().trim(),
description: dto.description?.trim(),
},
});
}
async updateWorkspace(id: string, dto: UpdateWorkspaceDto) {
const workspace = await this.prisma.workspace.findUnique({ where: { id } });
if (!workspace) {
throw new NotFoundException('فضای کاری یافت نشد.');
}
return this.prisma.workspace.update({
where: { id },
data: {
name: dto.name?.trim(),
description: dto.description?.trim(),
},
});
}
}
@@ -0,0 +1,79 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { AccessScopeType, AdminStatus } from '@prisma/client';
import { Type } from 'class-transformer';
import {
IsArray,
IsBoolean,
IsEmail,
IsEnum,
IsNotEmpty,
IsOptional,
IsString,
Matches,
MinLength,
ValidateNested,
} from 'class-validator';
export class AdminWorkspaceAssignmentDto {
@ApiProperty({ description: 'شناسه فضای کاری' })
@IsString()
@IsNotEmpty()
workspaceId: string;
@ApiPropertyOptional({ enum: AccessScopeType, default: AccessScopeType.ALL })
@IsEnum(AccessScopeType)
@IsOptional()
scopeType?: AccessScopeType = AccessScopeType.ALL;
@ApiPropertyOptional({ description: 'لیست شناسه کمپین‌های مجاز در صورت اسکوپ سفارشی' })
@IsArray()
@IsString({ each: true })
@IsOptional()
specificCampaignIds?: string[];
}
export class CreateAdminDto {
@ApiProperty({ description: 'نام و نام خانوادگی مدیر', example: 'علی رضایی' })
@IsString()
@IsNotEmpty({ message: 'نام مدیر الزامی است.' })
name: string;
@ApiProperty({ description: 'ایمیل سازمانی مدیر', example: 'ali@example.com' })
@IsEmail({}, { message: 'فرمت ایمیل نامعتبر است.' })
@IsNotEmpty({ message: 'ایمیل الزامی است.' })
email: string;
@ApiProperty({ description: 'شماره موبایل مدیر (۱۱ رقم با ۰۹)', example: '09123456789' })
@Matches(/^09\d{9}$/, { message: 'شماره موبایل باید ۱۱ رقمی بوده و با ۰۹ شروع شود.' })
@IsNotEmpty({ message: 'شماره موبایل الزامی است.' })
mobile: string;
@ApiProperty({ description: 'کلمه عبور ورود به سامانه (حداقل ۶ کاراکتر)', example: 'SecurePass123' })
@IsString()
@MinLength(6, { message: 'کلمه عبور باید حداقل ۶ کاراکتر باشد.' })
@IsNotEmpty({ message: 'کلمه عبور الزامی است.' })
password: string;
@ApiPropertyOptional({ enum: AdminStatus, default: AdminStatus.ACTIVE })
@IsEnum(AdminStatus)
@IsOptional()
status?: AdminStatus = AdminStatus.ACTIVE;
@ApiPropertyOptional({ description: 'آیا این کاربر مدیر ارشد تام‌الاختیار است؟', default: false })
@IsBoolean()
@IsOptional()
isSuperAdmin?: boolean = false;
@ApiPropertyOptional({ description: 'شناسه نقش‌های تخصیص‌یافته به مدیر', type: [String] })
@IsArray()
@IsString({ each: true })
@IsOptional()
roleIds?: string[];
@ApiPropertyOptional({ description: 'تخصیص‌های فضاهای کاری و اسکوپ دسترسی', type: [AdminWorkspaceAssignmentDto] })
@IsArray()
@ValidateNested({ each: true })
@Type(() => AdminWorkspaceAssignmentDto)
@IsOptional()
workspaceAssignments?: AdminWorkspaceAssignmentDto[];
}
@@ -0,0 +1,26 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsArray, IsNotEmpty, IsOptional, IsString, Matches } from 'class-validator';
export class CreateRoleDto {
@ApiProperty({ description: 'کد یکتا و انگلیسی نقش', example: 'MARKETING_LEAD' })
@IsString()
@Matches(/^[A-Z0-9_]+$/, { message: 'کد نقش باید فقط شامل حروف بزرگ انگلیسی، اعداد و خط زیر (_) باشد.' })
@IsNotEmpty({ message: 'کد نقش الزامی است.' })
code: string;
@ApiProperty({ description: 'نام نمایشی نقش به فارسی', example: 'سرپرست بازاریابی' })
@IsString()
@IsNotEmpty({ message: 'نام نقش الزامی است.' })
name: string;
@ApiPropertyOptional({ description: 'توضیحات نقش' })
@IsString()
@IsOptional()
description?: string;
@ApiPropertyOptional({ description: 'لیست شناسه‌های پرمیشن‌های منتسب به این نقش', type: [String] })
@IsArray()
@IsString({ each: true })
@IsOptional()
permissionIds?: string[];
}
@@ -0,0 +1,20 @@
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
import { IsNotEmpty, IsOptional, IsString, Matches } from 'class-validator';
export class CreateWorkspaceDto {
@ApiProperty({ description: 'نام فضای کاری', example: 'برند دیجی‌کالا' })
@IsString()
@IsNotEmpty({ message: 'نام فضای کاری الزامی است.' })
name: string;
@ApiProperty({ description: 'شناسه انگلیسی یکتا (اسلاگ)', example: 'digikala-brand' })
@IsString()
@Matches(/^[a-z0-9-]+$/, { message: 'اسلاگ باید فقط شامل حروف کوچک انگلیسی، اعداد و خط تیره (-) باشد.' })
@IsNotEmpty({ message: 'اسلاگ الزامی است.' })
slug: string;
@ApiPropertyOptional({ description: 'توضیحات فضای کاری' })
@IsString()
@IsOptional()
description?: string;
}
@@ -0,0 +1,61 @@
import { ApiPropertyOptional } from '@nestjs/swagger';
import { AdminStatus } from '@prisma/client';
import { Type } from 'class-transformer';
import {
IsArray,
IsBoolean,
IsEmail,
IsEnum,
IsOptional,
IsString,
Matches,
MinLength,
ValidateNested,
} from 'class-validator';
import { AdminWorkspaceAssignmentDto } from './create-admin.dto';
export class UpdateAdminDto {
@ApiPropertyOptional({ description: 'نام و نام خانوادگی مدیر' })
@IsString()
@IsOptional()
name?: string;
@ApiPropertyOptional({ description: 'ایمیل سازمانی مدیر' })
@IsEmail({}, { message: 'فرمت ایمیل نامعتبر است.' })
@IsOptional()
email?: string;
@ApiPropertyOptional({ description: 'شماره موبایل مدیر (۱۱ رقم با ۰۹)' })
@Matches(/^09\d{9}$/, { message: 'شماره موبایل باید ۱۱ رقمی بوده و با ۰۹ شروع شود.' })
@IsOptional()
mobile?: string;
@ApiPropertyOptional({ description: 'کلمه عبور جدید (اختیاری)' })
@IsString()
@MinLength(6, { message: 'کلمه عبور باید حداقل ۶ کاراکتر باشد.' })
@IsOptional()
password?: string;
@ApiPropertyOptional({ enum: AdminStatus })
@IsEnum(AdminStatus)
@IsOptional()
status?: AdminStatus;
@ApiPropertyOptional({ description: 'آیا این کاربر مدیر ارشد تام‌الاختیار است؟' })
@IsBoolean()
@IsOptional()
isSuperAdmin?: boolean;
@ApiPropertyOptional({ description: 'شناسه نقش‌های تخصیص‌یافته به مدیر', type: [String] })
@IsArray()
@IsString({ each: true })
@IsOptional()
roleIds?: string[];
@ApiPropertyOptional({ description: 'تخصیص‌های فضاهای کاری و اسکوپ دسترسی', type: [AdminWorkspaceAssignmentDto] })
@IsArray()
@ValidateNested({ each: true })
@Type(() => AdminWorkspaceAssignmentDto)
@IsOptional()
workspaceAssignments?: AdminWorkspaceAssignmentDto[];
}
@@ -0,0 +1,20 @@
import { ApiPropertyOptional } from '@nestjs/swagger';
import { IsArray, IsOptional, IsString } from 'class-validator';
export class UpdateRoleDto {
@ApiPropertyOptional({ description: 'نام نمایشی نقش به فارسی' })
@IsString()
@IsOptional()
name?: string;
@ApiPropertyOptional({ description: 'توضیحات نقش' })
@IsString()
@IsOptional()
description?: string;
@ApiPropertyOptional({ description: 'لیست شناسه‌های پرمیشن‌های منتسب به این نقش', type: [String] })
@IsArray()
@IsString({ each: true })
@IsOptional()
permissionIds?: string[];
}
@@ -0,0 +1,14 @@
import { ApiPropertyOptional } from '@nestjs/swagger';
import { IsOptional, IsString } from 'class-validator';
export class UpdateWorkspaceDto {
@ApiPropertyOptional({ description: 'نام فضای کاری' })
@IsString()
@IsOptional()
name?: string;
@ApiPropertyOptional({ description: 'توضیحات فضای کاری' })
@IsString()
@IsOptional()
description?: string;
}
@@ -0,0 +1,74 @@
import {
Body,
Controller,
Delete,
Get,
HttpCode,
HttpStatus,
Param,
Patch,
Post,
UseGuards,
UseInterceptors,
} from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
import { JwtAdminGuard } from '../../common/guards/jwt-admin.guard';
import { PermissionsGuard } from '../../common/guards/permissions.guard';
import { AuditInterceptor } from '../../common/interceptors/audit.interceptor';
import { AdminManagementService } from './admin-management.service';
import { CreateRoleDto } from './dto/create-role.dto';
import { UpdateRoleDto } from './dto/update-role.dto';
@ApiTags('Role & Permission Management (مدیریت نقش‌ها و مجوزها)')
@Controller('admin')
@UseGuards(JwtAdminGuard, PermissionsGuard)
@UseInterceptors(AuditInterceptor)
@ApiBearerAuth('AdminAuth')
export class RoleManagementController {
constructor(private readonly service: AdminManagementService) {}
@Get('roles')
@RequirePermission('admins:read')
@ApiOperation({ summary: 'لیست تمام نقش‌های تعریف‌شده به همراه تعداد مدیران و مجوزها' })
async findAllRoles() {
return this.service.findAllRoles();
}
@Get('roles/:id')
@RequirePermission('roles:manage')
@ApiOperation({ summary: 'مشاهده جزئیات نقش و شناسه‌های مجوزهای آن' })
async findRoleById(@Param('id') id: string) {
return this.service.findRoleById(id);
}
@Post('roles')
@RequirePermission('roles:manage')
@ApiOperation({ summary: 'ایجاد نقش جدید و تخصیص مجوزها' })
@ApiResponse({ status: 201, description: 'نقش با موفقیت ایجاد شد' })
async createRole(@Body() dto: CreateRoleDto) {
return this.service.createRole(dto);
}
@Patch('roles/:id')
@RequirePermission('roles:manage')
@ApiOperation({ summary: 'ویرایش مشخصات یا مجوزهای نقش' })
async updateRole(@Param('id') id: string, @Body() dto: UpdateRoleDto) {
return this.service.updateRole(id, dto);
}
@Delete('roles/:id')
@RequirePermission('roles:manage')
@HttpCode(HttpStatus.OK)
@ApiOperation({ summary: 'حذف نقش سفارشی' })
async deleteRole(@Param('id') id: string) {
return this.service.deleteRole(id);
}
@Get('permissions')
@RequirePermission('admins:read')
@ApiOperation({ summary: 'کاتالوگ کامل مجوزهای سیستم دسته‌بندی‌شده بر اساس ماژول' })
async findAllPermissions() {
return this.service.findAllPermissions();
}
}
@@ -0,0 +1,49 @@
import {
Body,
Controller,
Get,
Param,
Patch,
Post,
UseGuards,
UseInterceptors,
} from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
import { JwtAdminGuard } from '../../common/guards/jwt-admin.guard';
import { PermissionsGuard } from '../../common/guards/permissions.guard';
import { AuditInterceptor } from '../../common/interceptors/audit.interceptor';
import { AdminManagementService } from './admin-management.service';
import { CreateWorkspaceDto } from './dto/create-workspace.dto';
import { UpdateWorkspaceDto } from './dto/update-workspace.dto';
@ApiTags('Workspace Management (مدیریت فضاهای کاری)')
@Controller('admin/workspaces')
@UseGuards(JwtAdminGuard, PermissionsGuard)
@UseInterceptors(AuditInterceptor)
@ApiBearerAuth('AdminAuth')
export class WorkspaceController {
constructor(private readonly service: AdminManagementService) {}
@Get()
@RequirePermission('admins:read')
@ApiOperation({ summary: 'لیست فضاهای کاری به همراه آمار کمپین‌ها و مدیران' })
async findAll() {
return this.service.findAllWorkspaces();
}
@Post()
@RequirePermission('workspaces:manage')
@ApiOperation({ summary: 'تعریف فضای کاری جدید' })
@ApiResponse({ status: 201, description: 'فضای کاری با موفقیت ایجاد شد' })
async create(@Body() dto: CreateWorkspaceDto) {
return this.service.createWorkspace(dto);
}
@Patch(':id')
@RequirePermission('workspaces:manage')
@ApiOperation({ summary: 'ویرایش مشخصات فضای کاری' })
async update(@Param('id') id: string, @Body() dto: UpdateWorkspaceDto) {
return this.service.updateWorkspace(id, dto);
}
}
@@ -12,6 +12,8 @@ import {
} from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { JwtAdminGuard } from '../../common/guards/jwt-admin.guard';
import { PermissionsGuard } from '../../common/guards/permissions.guard';
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
import { AuditInterceptor } from '../../common/interceptors/audit.interceptor';
import { CampaignService } from './campaign.service';
import { CampaignFilterDto } from './dto/campaign-filter.dto';
@@ -20,13 +22,14 @@ import { UpdateCampaignDto } from './dto/update-campaign.dto';
@ApiTags('Admin Campaigns (مدیریت کمپین‌ها)')
@Controller('admin/campaigns')
@UseGuards(JwtAdminGuard)
@UseGuards(JwtAdminGuard, PermissionsGuard)
@UseInterceptors(AuditInterceptor)
@ApiBearerAuth('AdminAuth')
export class AdminCampaignController {
constructor(private readonly campaignService: CampaignService) {}
@Post()
@RequirePermission('campaigns:create')
@ApiOperation({ summary: 'ایجاد کمپین جدید' })
@ApiResponse({ status: 201, description: 'کمپین با موفقیت ایجاد شد' })
async create(@Body() dto: CreateCampaignDto) {
@@ -34,30 +37,35 @@ export class AdminCampaignController {
}
@Get()
@RequirePermission('campaigns:read')
@ApiOperation({ summary: 'لیست کمپین‌ها با فیلتر وضعیت، جستجو و صفحه‌بندی' })
async findAll(@Query() filter: CampaignFilterDto) {
return this.campaignService.findAll(filter);
}
@Get(':id')
@RequirePermission('campaigns:read')
@ApiOperation({ summary: 'دریافت جزئیات کامل کمپین به همراه جوایز و آمار' })
async findOne(@Param('id') id: string) {
return this.campaignService.findOne(id);
}
@Patch(':id')
@RequirePermission('campaigns:update')
@ApiOperation({ summary: 'ویرایش اطلاعات و وضعیت کمپین' })
async update(@Param('id') id: string, @Body() dto: UpdateCampaignDto) {
return this.campaignService.update(id, dto);
}
@Delete(':id')
@RequirePermission('campaigns:delete')
@ApiOperation({ summary: 'حذف کمپین (در صورت عدم وجود اسپین ثبت‌شده)' })
async remove(@Param('id') id: string) {
return this.campaignService.remove(id);
}
@Post(':id/duplicate')
@RequirePermission('campaigns:create')
@ApiOperation({ summary: 'کپی‌برداری سریع از یک کمپین به همراه جوایز آن' })
async duplicate(@Param('id') id: string) {
return this.campaignService.duplicate(id);
@@ -10,6 +10,8 @@ import {
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { CurrentAdmin } from '../../common/decorators/current-admin.decorator';
import { JwtAdminGuard } from '../../common/guards/jwt-admin.guard';
import { PermissionsGuard } from '../../common/guards/permissions.guard';
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
import { AuditInterceptor } from '../../common/interceptors/audit.interceptor';
import { ChanceService } from './chance.service';
import { AdjustChanceDto, AdminAdjustChancePayloadDto } from './dto/adjust-chance.dto';
@@ -17,13 +19,14 @@ import { AdjustChanceDto, AdminAdjustChancePayloadDto } from './dto/adjust-chanc
@ApiTags('Admin Chance Management (مدیریت شانس)')
@Controller('admin')
@UseGuards(JwtAdminGuard)
@UseGuards(JwtAdminGuard, PermissionsGuard)
@UseInterceptors(AuditInterceptor)
@ApiBearerAuth('AdminAuth')
export class AdminChanceController {
constructor(private readonly chanceService: ChanceService) {}
@Post('chances/adjust')
@RequirePermission('chances:adjust')
@ApiOperation({ summary: 'تغییر شانس کاربر در کمپین (اندپوئینت مستقیم پنل مدیریت)' })
@ApiResponse({
status: 200,
@@ -46,6 +49,7 @@ export class AdminChanceController {
}
@Post('users/:userId/campaigns/:campaignId/chances')
@RequirePermission('chances:adjust')
@ApiOperation({ summary: 'اضافه یا کم کردن دستی شانس کاربر در یک کمپین خاص' })
@ApiResponse({
status: 200,
@@ -12,6 +12,8 @@ import {
} from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiResponse, ApiTags } from '@nestjs/swagger';
import { JwtAdminGuard } from '../../common/guards/jwt-admin.guard';
import { PermissionsGuard } from '../../common/guards/permissions.guard';
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
import { AuditInterceptor } from '../../common/interceptors/audit.interceptor';
import { CreatePrizeDto } from './dto/create-prize.dto';
import { UpdatePrizeDto } from './dto/update-prize.dto';
@@ -20,13 +22,14 @@ import { PrizeService } from './prize.service';
@ApiTags('Admin Prizes (مدیریت جوایز)')
@Controller('admin')
@UseGuards(JwtAdminGuard)
@UseGuards(JwtAdminGuard, PermissionsGuard)
@UseInterceptors(AuditInterceptor)
@ApiBearerAuth('AdminAuth')
export class AdminPrizeController {
constructor(private readonly prizeService: PrizeService) {}
@Post('campaigns/:campaignId/prizes')
@RequirePermission('prizes:manage')
@ApiOperation({ summary: 'ایجاد جایزه جدید برای یک کمپین' })
@ApiResponse({ status: 201, description: 'جایزه با موفقیت به کمپین اضافه شد' })
async create(
@@ -37,12 +40,14 @@ export class AdminPrizeController {
}
@Get('campaigns/:campaignId/prizes')
@RequirePermission('prizes:read')
@ApiOperation({ summary: 'لیست تمام جوایز تعریف‌شده برای یک کمپین' })
async findByCampaign(@Param('campaignId') campaignId: string) {
return this.prizeService.findByCampaign(campaignId);
}
@Get('prizes/:id')
@RequirePermission('prizes:read')
@ApiOperation({ summary: 'دریافت مشخصات کامل یک جایزه' })
async findOne(@Param('id') id: string) {
return this.prizeService.findOne(id);
@@ -50,18 +55,21 @@ export class AdminPrizeController {
@Patch('prizes/:id')
@Put('prizes/:id')
@RequirePermission('prizes:manage')
@ApiOperation({ summary: 'ویرایش اطلاعات و وزن جایزه' })
async update(@Param('id') id: string, @Body() dto: UpdatePrizeDto) {
return this.prizeService.update(id, dto);
}
@Patch('prizes/:id/stock')
@RequirePermission('prizes:manage')
@ApiOperation({ summary: 'شارژ یا اصلاح دستی موجودی جایزه' })
async updateStock(@Param('id') id: string, @Body() dto: UpdateStockDto) {
return this.prizeService.updateStock(id, dto);
}
@Delete('prizes/:id')
@RequirePermission('prizes:manage')
@ApiOperation({ summary: 'حذف جایزه (در صورت عدم ثبت در نتایج قرعه‌کشی)' })
async remove(@Param('id') id: string) {
return this.prizeService.remove(id);
@@ -2,16 +2,19 @@ import { Controller, Get, Header, Query, Res, UseGuards } from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger';
import { Response } from 'express';
import { JwtAdminGuard } from '../../common/guards/jwt-admin.guard';
import { PermissionsGuard } from '../../common/guards/permissions.guard';
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
import { ReportService } from './report.service';
@ApiTags('Admin Reports (گزارش‌گیری و داشبورد)')
@Controller('admin/reports')
@UseGuards(JwtAdminGuard)
@UseGuards(JwtAdminGuard, PermissionsGuard)
@ApiBearerAuth('AdminAuth')
export class AdminReportController {
constructor(private readonly reportService: ReportService) {}
@Get('dashboard')
@RequirePermission('reports:read')
@ApiOperation({ summary: 'دریافت تمام آمارهای تجمیعی داشبورد اصلی، چارت‌های روزانه و آخرین برندگان' })
@ApiResponse({ status: 200, description: 'شاخص‌های KPI داشبورد' })
async getDashboard() {
@@ -19,6 +22,7 @@ export class AdminReportController {
}
@Get('participation')
@RequirePermission('reports:read')
@ApiOperation({ summary: 'گزارش مشارکت کاربران و تحلیل شانس‌ها با فیلتر بازه زمانی' })
@ApiQuery({ name: 'from', required: false })
@ApiQuery({ name: 'to', required: false })
@@ -28,6 +32,7 @@ export class AdminReportController {
}
@Get('export')
@RequirePermission('reports:export')
@ApiOperation({ summary: 'خروجی اکسل گزارش مشارکت' })
@Header('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
@Header('Content-Disposition', 'attachment; filename=participation-report.xlsx')
@@ -9,18 +9,21 @@ import {
} from '@nestjs/common';
import { ApiBearerAuth, ApiOperation, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger';
import { JwtAdminGuard } from '../../common/guards/jwt-admin.guard';
import { PermissionsGuard } from '../../common/guards/permissions.guard';
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
import { AuditInterceptor } from '../../common/interceptors/audit.interceptor';
import { SmsService } from './sms.service';
@ApiTags('Admin SMS Logs (مدیریت پیامک‌ها)')
@Controller('admin/sms-logs')
@UseGuards(JwtAdminGuard)
@UseGuards(JwtAdminGuard, PermissionsGuard)
@UseInterceptors(AuditInterceptor)
@ApiBearerAuth('AdminAuth')
export class AdminSmsController {
constructor(private readonly smsService: SmsService) {}
@Get()
@RequirePermission('sms:read')
@ApiOperation({ summary: 'لیست تمام پیامک‌های ارسالی با فیلتر وضعیت و موبایل' })
@ApiQuery({ name: 'page', required: false })
@ApiQuery({ name: 'pageSize', required: false })
@@ -31,6 +34,7 @@ export class AdminSmsController {
}
@Post(':id/resend')
@RequirePermission('sms:resend')
@ApiOperation({ summary: 'ارسال مجدد پیامک ناموفق' })
@ApiResponse({ status: 200, description: 'پیامک در صف ارسال مجدد قرار گرفت' })
async resend(@Param('id') id: string) {
@@ -11,18 +11,21 @@ import {
import { ApiBearerAuth, ApiOperation, ApiQuery, ApiResponse, ApiTags } from '@nestjs/swagger';
import { UserStatus } from '@prisma/client';
import { JwtAdminGuard } from '../../common/guards/jwt-admin.guard';
import { PermissionsGuard } from '../../common/guards/permissions.guard';
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
import { AuditInterceptor } from '../../common/interceptors/audit.interceptor';
import { UserService } from './user.service';
@ApiTags('Admin Users (مدیریت کاربران)')
@Controller('admin/users')
@UseGuards(JwtAdminGuard)
@UseGuards(JwtAdminGuard, PermissionsGuard)
@UseInterceptors(AuditInterceptor)
@ApiBearerAuth('AdminAuth')
export class AdminUserController {
constructor(private readonly userService: UserService) {}
@Get()
@RequirePermission('users:read')
@ApiOperation({ summary: 'لیست کاربران با جستجوی شماره موبایل/نام و فیلتر وضعیت' })
@ApiQuery({ name: 'page', required: false })
@ApiQuery({ name: 'pageSize', required: false })
@@ -33,12 +36,14 @@ export class AdminUserController {
}
@Get(':id')
@RequirePermission('users:read')
@ApiOperation({ summary: 'مشاهده پروفایل کامل، کمپین‌ها، شانس‌ها، اسپین‌ها و جوایز کاربر' })
async findOne(@Param('id') id: string) {
return this.userService.findOneForAdmin(id);
}
@Patch(':id/block')
@RequirePermission('users:block')
@ApiOperation({ summary: 'مسدودسازی یا رفع مسدودیت کاربر' })
async toggleBlock(
@Param('id') id: string,
@@ -10,18 +10,21 @@ import {
import { ApiBearerAuth, ApiOperation, ApiQuery, ApiTags } from '@nestjs/swagger';
import { Response } from 'express';
import { JwtAdminGuard } from '../../common/guards/jwt-admin.guard';
import { PermissionsGuard } from '../../common/guards/permissions.guard';
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
import { AuditInterceptor } from '../../common/interceptors/audit.interceptor';
import { WinnerService } from './winner.service';
@ApiTags('Admin Spins (تاریخچه اسپین‌ها)')
@Controller('admin/spins')
@UseGuards(JwtAdminGuard)
@UseGuards(JwtAdminGuard, PermissionsGuard)
@UseInterceptors(AuditInterceptor)
@ApiBearerAuth('AdminAuth')
export class AdminSpinController {
constructor(private readonly winnerService: WinnerService) {}
@Get()
@RequirePermission('spins:read')
@ApiOperation({ summary: 'لیست تاریخچه تمام چرخاندن‌های گردونه با فیلتر و جستجو' })
@ApiQuery({ name: 'page', required: false })
@ApiQuery({ name: 'pageSize', required: false })
@@ -33,6 +36,7 @@ export class AdminSpinController {
}
@Get('export')
@RequirePermission('reports:export')
@ApiOperation({ summary: 'خروجی فایل اکسل از تاریخچه تمام اسپین‌ها' })
@Header('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
@Header('Content-Disposition', 'attachment; filename=spins.xlsx')
@@ -15,19 +15,22 @@ import { ApiBearerAuth, ApiOperation, ApiQuery, ApiResponse, ApiTags } from '@ne
import { Response } from 'express';
import { CurrentAdmin } from '../../common/decorators/current-admin.decorator';
import { JwtAdminGuard } from '../../common/guards/jwt-admin.guard';
import { PermissionsGuard } from '../../common/guards/permissions.guard';
import { RequirePermission } from '../../common/decorators/require-permission.decorator';
import { AuditInterceptor } from '../../common/interceptors/audit.interceptor';
import { UpdatePrizeStatusDto } from './dto/update-prize-status.dto';
import { WinnerService } from './winner.service';
@ApiTags('Admin Winners (مدیریت برندگان)')
@Controller('admin/user-prizes')
@UseGuards(JwtAdminGuard)
@UseGuards(JwtAdminGuard, PermissionsGuard)
@UseInterceptors(AuditInterceptor)
@ApiBearerAuth('AdminAuth')
export class AdminWinnerController {
constructor(private readonly winnerService: WinnerService) {}
@Get()
@RequirePermission('winners:read')
@ApiOperation({ summary: 'لیست برندگان با فیلتر کمپین، وضعیت جایزه و جستجو' })
@ApiQuery({ name: 'page', required: false })
@ApiQuery({ name: 'pageSize', required: false })
@@ -39,6 +42,7 @@ export class AdminWinnerController {
}
@Patch(':id/status')
@RequirePermission('winners:manage')
@ApiOperation({ summary: 'تغییر وضعیت جایزه برنده (تحویل شد / تماس گرفته شد / منقضی شد)' })
async updateStatus(
@Param('id') id: string,
@@ -51,6 +55,7 @@ export class AdminWinnerController {
}
@Get('export')
@RequirePermission('reports:export')
@ApiOperation({ summary: 'خروجی فایل اکسل از لیست برندگان' })
@Header('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet')
@Header('Content-Disposition', 'attachment; filename=winners.xlsx')