feat: granular hamkar/khanegi permissions (manage / settings / appearance)
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -23,8 +23,12 @@ import { PermissionsGuard } from '../auth/permissions.guard';
|
||||
import { RequirePermissions } from '../auth/permissions.decorator';
|
||||
import { CurrentUser } from '../auth/current-user.decorator';
|
||||
import {
|
||||
ANY_TENANT_READ,
|
||||
productAnyReadPermissions,
|
||||
productReadPermission,
|
||||
productUiWritePermission,
|
||||
productWritePermission,
|
||||
userHasAnyPermission,
|
||||
userHasPermission,
|
||||
} from '../auth/permissions';
|
||||
import {
|
||||
@@ -38,6 +42,15 @@ import { TenantsService } from './tenants.service';
|
||||
|
||||
const IMAGE_MIME = /^image\/(jpeg|png|gif|webp|svg\+xml)$/i;
|
||||
|
||||
/** Fields edited from the appearance pages; guarded by `<product>.ui:write`. */
|
||||
const APPEARANCE_FIELDS = [
|
||||
'appUiConfig',
|
||||
'uiVariant',
|
||||
'primaryColor',
|
||||
'secondaryColor',
|
||||
'backgroundColor',
|
||||
] as const;
|
||||
|
||||
type AuthUser = { id: string; role?: string; permissions?: unknown };
|
||||
|
||||
@Controller('tenants')
|
||||
@@ -49,18 +62,18 @@ export class TenantsController {
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@RequirePermissions('hamkar:read', 'khanegi:read', 'others:read', { mode: 'any' })
|
||||
@RequirePermissions(...ANY_TENANT_READ, { mode: 'any' })
|
||||
async findAll(
|
||||
@Query('product') product?: ProductType,
|
||||
@CurrentUser() user?: AuthUser,
|
||||
) {
|
||||
if (product) {
|
||||
this.assertProductAccess(user, product, 'read');
|
||||
this.assertAnyProductRead(user, product);
|
||||
return this.tenantsService.findAll(product);
|
||||
}
|
||||
const rows = await this.tenantsService.findAll();
|
||||
return rows.filter((row) =>
|
||||
userHasPermission(user || {}, productReadPermission(row.product)),
|
||||
userHasAnyPermission(user || {}, productAnyReadPermissions(row.product)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -103,10 +116,10 @@ export class TenantsController {
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@RequirePermissions('hamkar:read', 'khanegi:read', 'others:read', { mode: 'any' })
|
||||
@RequirePermissions(...ANY_TENANT_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');
|
||||
this.assertAnyProductRead(user, tenant.product);
|
||||
return tenant;
|
||||
}
|
||||
|
||||
@@ -118,14 +131,48 @@ export class TenantsController {
|
||||
}
|
||||
|
||||
@Patch(':id')
|
||||
@RequirePermissions('hamkar:write', 'khanegi:write', 'others:write', { mode: 'any' })
|
||||
@RequirePermissions(
|
||||
'hamkar:write',
|
||||
'hamkar.ui:write',
|
||||
'khanegi:write',
|
||||
'khanegi.ui:write',
|
||||
'others:write',
|
||||
{ mode: 'any' },
|
||||
)
|
||||
async update(
|
||||
@Param('id') id: string,
|
||||
@Body() dto: UpdateTenantDto,
|
||||
@CurrentUser() user: AuthUser,
|
||||
) {
|
||||
const tenant = await this.tenantsService.findOne(id);
|
||||
this.assertProductAccess(user, tenant.product, 'write');
|
||||
const before = tenant as unknown as Record<string, unknown>;
|
||||
const body = dto as Record<string, unknown>;
|
||||
|
||||
const appearanceChanged = APPEARANCE_FIELDS.some((key) => {
|
||||
if (body[key] === undefined) return false;
|
||||
return JSON.stringify(body[key]) !== JSON.stringify(before[key] ?? null);
|
||||
});
|
||||
const touchesIdentity = Object.keys(body).some(
|
||||
(key) =>
|
||||
body[key] !== undefined &&
|
||||
!(APPEARANCE_FIELDS as readonly string[]).includes(key),
|
||||
);
|
||||
|
||||
if (touchesIdentity) {
|
||||
this.assertProductAccess(user, tenant.product, 'write');
|
||||
}
|
||||
if (appearanceChanged) {
|
||||
const uiCode = productUiWritePermission(tenant.product);
|
||||
const allowed = uiCode
|
||||
? userHasPermission(user || {}, uiCode)
|
||||
: userHasPermission(user || {}, productWritePermission(tenant.product));
|
||||
if (!allowed) {
|
||||
throw new ForbiddenException('شما به ظاهر این محصول دسترسی ندارید');
|
||||
}
|
||||
}
|
||||
if (!touchesIdentity && !appearanceChanged) {
|
||||
this.assertAnyProductRead(user, tenant.product);
|
||||
}
|
||||
return this.tenantsService.update(id, dto, user.id);
|
||||
}
|
||||
|
||||
@@ -150,4 +197,10 @@ export class TenantsController {
|
||||
throw new ForbiddenException('شما به این محصول دسترسی ندارید');
|
||||
}
|
||||
}
|
||||
|
||||
private assertAnyProductRead(user: AuthUser | undefined, product: string) {
|
||||
if (!userHasAnyPermission(user || {}, productAnyReadPermissions(product))) {
|
||||
throw new ForbiddenException('شما به این محصول دسترسی ندارید');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user