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:
@@ -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('شما به این محصول دسترسی ندارید');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user