chore: commit pending admin-panel, api auth and parity doc changes
Co-Authored-By: Claude Haiku 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Haiku 5.5
parent
55856cac89
commit
cf014e1f6c
@@ -1,7 +1,7 @@
|
||||
import { Body, Controller, Get, HttpCode, Param, Post, Put } from '@nestjs/common';
|
||||
import { AdminBranchesService } from './admin-branches.service';
|
||||
import { TableQueryDto } from '../admin-table/table-query.dto';
|
||||
import { RequirePermission } from '../auth/guards';
|
||||
import { RequirePermission, SuperAdminOnly } from '../auth/guards';
|
||||
|
||||
/**
|
||||
* Legacy endpoints (BranchController + WorkshopController):
|
||||
@@ -12,6 +12,7 @@ import { RequirePermission } from '../auth/guards';
|
||||
* GET user/list -> the branch-manager picker
|
||||
*/
|
||||
@Controller('admin/branches')
|
||||
@SuperAdminOnly()
|
||||
export class AdminBranchesController {
|
||||
constructor(private readonly branches: AdminBranchesService) {}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Body, Controller, Delete, Get, HttpCode, Param, Post, Put, Query } from '@nestjs/common';
|
||||
import { AdminCategoriesService } from './admin-categories.service';
|
||||
import { TableQueryDto } from '../admin-table/table-query.dto';
|
||||
import { RequirePermission } from '../auth/guards';
|
||||
import { RequirePermission, SuperAdminOnly } from '../auth/guards';
|
||||
|
||||
/**
|
||||
* Legacy endpoints (CategoryController + the collection screens):
|
||||
@@ -37,6 +37,7 @@ export class AdminCategoriesController {
|
||||
// ── collections ("سینی") ─────────────────────────────────────────
|
||||
@Post('collections/list')
|
||||
@HttpCode(200)
|
||||
@SuperAdminOnly()
|
||||
@RequirePermission('collection.list')
|
||||
listCollections(@Body() query: TableQueryDto) {
|
||||
return this.categories.listCollections(query);
|
||||
@@ -44,12 +45,14 @@ export class AdminCategoriesController {
|
||||
|
||||
@Post('collections')
|
||||
@HttpCode(200)
|
||||
@SuperAdminOnly()
|
||||
@RequirePermission('collection.create')
|
||||
createCollection(@Body() body: { title: string; description?: string; is_active?: boolean }) {
|
||||
return this.categories.createCollection(body);
|
||||
}
|
||||
|
||||
@Put('collections/:id')
|
||||
@SuperAdminOnly()
|
||||
@RequirePermission('collection.list.edit')
|
||||
updateCollection(
|
||||
@Param('id') id: string,
|
||||
@@ -59,6 +62,7 @@ export class AdminCategoriesController {
|
||||
}
|
||||
|
||||
@Delete('collections/:id')
|
||||
@SuperAdminOnly()
|
||||
@RequirePermission('collection.list.edit')
|
||||
removeCollection(@Param('id') id: string) {
|
||||
return this.categories.removeCollection(id);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Body, Controller, Get, HttpCode, Param, Post, Put } from '@nestjs/common';
|
||||
import { AdminCustomersService, type OmdeScope } from './admin-customers.service';
|
||||
import { TableQueryDto } from '../admin-table/table-query.dto';
|
||||
import { CurrentAdmin, RequirePermission } from '../auth/guards';
|
||||
import { CurrentAdmin, RequirePermission, SuperAdminOnly } from '../auth/guards';
|
||||
import type { AuthenticatedAdmin } from '../auth/jwt.strategy';
|
||||
|
||||
/**
|
||||
@@ -31,6 +31,7 @@ export class AdminCustomersController {
|
||||
|
||||
@Post('banker/list')
|
||||
@HttpCode(200)
|
||||
@SuperAdminOnly()
|
||||
@RequirePermission('persons.list.client.omde')
|
||||
listBanker(@Body() query: TableQueryDto) {
|
||||
return this.list(query, 'banker');
|
||||
@@ -38,6 +39,7 @@ export class AdminCustomersController {
|
||||
|
||||
@Post('shopkeeper/list')
|
||||
@HttpCode(200)
|
||||
@SuperAdminOnly()
|
||||
@RequirePermission('persons.list.client.omde')
|
||||
listShopkeeper(@Body() query: TableQueryDto) {
|
||||
return this.list(query, 'shopkeeper');
|
||||
@@ -60,6 +62,7 @@ export class AdminCustomersController {
|
||||
/** Legacy POST single-customer — the retail customer list. */
|
||||
@Post('single/list')
|
||||
@HttpCode(200)
|
||||
@SuperAdminOnly({ allowBranch: true })
|
||||
@RequirePermission('persons.list.client.single')
|
||||
listSingle(@Body() query: TableQueryDto, @CurrentAdmin() admin: AuthenticatedAdmin) {
|
||||
return this.customers.listSingle(query, admin.branchId ?? undefined);
|
||||
@@ -68,6 +71,7 @@ export class AdminCustomersController {
|
||||
/** Legacy `POST single` — singleCustomer.vue, the retail customer form. */
|
||||
@Post('single')
|
||||
@HttpCode(200)
|
||||
@SuperAdminOnly({ allowBranch: true })
|
||||
@RequirePermission('persons.create')
|
||||
createSingle(@Body() data: Record<string, unknown>) {
|
||||
return this.customers.createSingle(data);
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Body, Controller, Delete, Get, HttpCode, Param, Post, Put } from '@nestjs/common';
|
||||
import { AdminDocumentsService } from './admin-documents.service';
|
||||
import { TableQueryDto } from '../admin-table/table-query.dto';
|
||||
import { CurrentAdmin, RequirePermission } from '../auth/guards';
|
||||
import { CurrentAdmin, RequirePermission, SuperAdminOnly } from '../auth/guards';
|
||||
import type { AuthenticatedAdmin } from '../auth/jwt.strategy';
|
||||
|
||||
/**
|
||||
@@ -46,6 +46,7 @@ export class AdminDocumentsController {
|
||||
|
||||
// ── wallet settings (views/manage/wallet.vue) ────────────────────
|
||||
@Get('wallet/config')
|
||||
@SuperAdminOnly()
|
||||
@RequirePermission('config.wallet')
|
||||
getWalletConfig() {
|
||||
return this.documents.getSetting('wallet.config');
|
||||
@@ -53,12 +54,14 @@ export class AdminDocumentsController {
|
||||
|
||||
@Post('wallet/config')
|
||||
@HttpCode(200)
|
||||
@SuperAdminOnly()
|
||||
@RequirePermission('config.wallet')
|
||||
setWalletConfig(@Body() body: Record<string, unknown>) {
|
||||
return this.documents.setSetting('wallet.config', body);
|
||||
}
|
||||
|
||||
@Get('wallet/birthday/config')
|
||||
@SuperAdminOnly()
|
||||
@RequirePermission('config.wallet')
|
||||
getBirthdayConfig() {
|
||||
return this.documents.getSetting('wallet.birthday');
|
||||
@@ -66,6 +69,7 @@ export class AdminDocumentsController {
|
||||
|
||||
@Post('wallet/birthday/config')
|
||||
@HttpCode(200)
|
||||
@SuperAdminOnly()
|
||||
@RequirePermission('config.wallet')
|
||||
setBirthdayConfig(@Body() body: Record<string, unknown>) {
|
||||
return this.documents.setSetting('wallet.birthday', body);
|
||||
|
||||
@@ -2,7 +2,7 @@ import { Body, Controller, Get, HttpCode, Param, Post } from '@nestjs/common';
|
||||
import { AdminOrdersService } from './admin-orders.service';
|
||||
import { TableQueryDto } from '../admin-table/table-query.dto';
|
||||
import { UpdateOrderStatusDto } from './update-status.dto';
|
||||
import { CurrentAdmin, RequirePermission } from '../auth/guards';
|
||||
import { CurrentAdmin, RequirePermission, SuperAdminOnly } from '../auth/guards';
|
||||
import type { AuthenticatedAdmin } from '../auth/jwt.strategy';
|
||||
|
||||
/**
|
||||
@@ -20,6 +20,7 @@ export class AdminOrdersController {
|
||||
/** Legacy POST single-order. POST, not GET, because XTable posts its filters. */
|
||||
@Post('list')
|
||||
@HttpCode(200)
|
||||
@SuperAdminOnly({ allowBranch: true })
|
||||
@RequirePermission('order.list')
|
||||
list(@Body() query: TableQueryDto, @CurrentAdmin() admin: AuthenticatedAdmin) {
|
||||
/**
|
||||
@@ -66,6 +67,7 @@ export class AdminOrdersController {
|
||||
/** Legacy POST /order from the admin order builder (views/order/create.vue). */
|
||||
@Post('create')
|
||||
@HttpCode(200)
|
||||
@SuperAdminOnly({ allowBranch: true })
|
||||
@RequirePermission('order.create')
|
||||
createFromAdmin(
|
||||
@Body()
|
||||
@@ -82,13 +84,17 @@ export class AdminOrdersController {
|
||||
|
||||
/** Legacy GET order/type. */
|
||||
@Get('type-options')
|
||||
@SuperAdminOnly()
|
||||
@RequirePermission('order.create')
|
||||
typeOptions() {
|
||||
return this.orders.typeOptions();
|
||||
}
|
||||
|
||||
// The three routes below also serve the wholesale and sourcing lists, whose
|
||||
// staff hold order.omde / order.tamin rather than the super-admin-only
|
||||
// order.list.
|
||||
@Get('status-options')
|
||||
@RequirePermission('order.list')
|
||||
@RequirePermission('order.list', 'order.omde', 'order.tamin')
|
||||
statusOptions() {
|
||||
return this.orders.statusOptions();
|
||||
}
|
||||
@@ -96,13 +102,13 @@ export class AdminOrdersController {
|
||||
/** Legacy POST single/order-sts/edit. */
|
||||
@Post('status')
|
||||
@HttpCode(200)
|
||||
@RequirePermission('order.list')
|
||||
@RequirePermission('order.list', 'order.omde', 'order.tamin')
|
||||
setStatus(@Body() dto: UpdateOrderStatusDto, @CurrentAdmin() admin: AuthenticatedAdmin) {
|
||||
return this.orders.setStatus(dto, admin.id);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@RequirePermission('order.list')
|
||||
@RequirePermission('order.list', 'order.omde', 'order.tamin')
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.orders.findOne(id);
|
||||
}
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { Body, Controller, Get, HttpCode, Param, Post, Put } from '@nestjs/common';
|
||||
import { AdminProductsService } from './admin-products.service';
|
||||
import { TableQueryDto } from '../admin-table/table-query.dto';
|
||||
import { RequirePermission } from '../auth/guards';
|
||||
import { CurrentAdmin, RequirePermission, SuperAdminOnly } from '../auth/guards';
|
||||
import type { AuthenticatedAdmin } from '../auth/jwt.strategy';
|
||||
import { isSuperAdmin } from '../auth/super-admin';
|
||||
|
||||
/**
|
||||
* Legacy endpoints (ProductController):
|
||||
@@ -51,6 +53,7 @@ export class AdminProductsController {
|
||||
*/
|
||||
@Post('variants/create')
|
||||
@HttpCode(200)
|
||||
@SuperAdminOnly()
|
||||
@RequirePermission('order.create')
|
||||
createVariant(@Body() body: { product_id: string; title: string; grams: number }) {
|
||||
return this.products.createVariant(body);
|
||||
@@ -78,6 +81,7 @@ export class AdminProductsController {
|
||||
|
||||
|
||||
@Get('collections')
|
||||
@SuperAdminOnly()
|
||||
@RequirePermission('product.list')
|
||||
collections() {
|
||||
return this.products.collections();
|
||||
@@ -100,7 +104,9 @@ export class AdminProductsController {
|
||||
@Post('create')
|
||||
@HttpCode(200)
|
||||
@RequirePermission('product.create')
|
||||
create(@Body() data: Record<string, unknown>) {
|
||||
create(@Body() data: Record<string, unknown>, @CurrentAdmin() admin: AuthenticatedAdmin) {
|
||||
// Trays are super-admin-only; anyone else's product is created without them.
|
||||
if (!isSuperAdmin(admin.mobile)) delete data.ciny_ids;
|
||||
return this.products.create(data);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Body, Controller, Get, HttpCode, Param, Post, Put } from '@nestjs/common';
|
||||
import { AdminBranchesService } from '../admin-branches/admin-branches.service';
|
||||
import { TableQueryDto } from '../admin-table/table-query.dto';
|
||||
import { RequirePermission } from '../auth/guards';
|
||||
import { RequirePermission, SuperAdminOnly } from '../auth/guards';
|
||||
|
||||
/**
|
||||
* Workshops — legacy views/workshop/*, served by WorkshopController:
|
||||
@@ -14,6 +14,9 @@ import { RequirePermission } from '../auth/guards';
|
||||
* screen's workshop picker); the CRUD arrived with Batch 11. Both live here so
|
||||
* there is one owner of /admin/workshops, and the logic sits in
|
||||
* AdminBranchesService alongside the branch code it mirrors.
|
||||
*
|
||||
* Managing workshops is super-admin-only; the list stays open because the
|
||||
* made-to-order screen picks a workshop from it.
|
||||
*/
|
||||
@Controller('admin/workshops')
|
||||
export class AdminWorkshopsController {
|
||||
@@ -27,24 +30,28 @@ export class AdminWorkshopsController {
|
||||
|
||||
@Post()
|
||||
@HttpCode(200)
|
||||
@SuperAdminOnly()
|
||||
@RequirePermission('workshop.create')
|
||||
create(@Body() data: Record<string, unknown>) {
|
||||
return this.branches.createWorkshop(data);
|
||||
}
|
||||
|
||||
@Get('active/:id')
|
||||
@SuperAdminOnly()
|
||||
@RequirePermission('workshop.list')
|
||||
toggleActive(@Param('id') id: string) {
|
||||
return this.branches.toggleWorkshopActive(id);
|
||||
}
|
||||
|
||||
@Get(':id')
|
||||
@SuperAdminOnly()
|
||||
@RequirePermission('workshop.list')
|
||||
findOne(@Param('id') id: string) {
|
||||
return this.branches.findWorkshop(id);
|
||||
}
|
||||
|
||||
@Put(':id')
|
||||
@SuperAdminOnly()
|
||||
@RequirePermission('workshop.list')
|
||||
update(@Param('id') id: string, @Body() data: Record<string, unknown>) {
|
||||
return this.branches.updateWorkshop(id, data);
|
||||
|
||||
@@ -14,11 +14,12 @@ import { AdminSessionService } from '../sessions/admin-session.service';
|
||||
import { envList, envOr, envOrNull, pickExactUri } from '../../common/env';
|
||||
import type { LoginDto } from './dto/login.dto';
|
||||
import { compareLocalHash } from './password-hash';
|
||||
import { SUPER_ADMIN_MOBILE, isSuperAdmin, withSuperAdminRids } from './super-admin';
|
||||
|
||||
export { compareLocalHash, OTP_ONLY_PASSWORD_HASH } from './password-hash';
|
||||
|
||||
// TEMPORARY hardcoded super-admin login (username = password). Remove before going live.
|
||||
const STATIC_ADMIN_MOBILE = '09128081392';
|
||||
const STATIC_ADMIN_MOBILE = SUPER_ADMIN_MOBILE;
|
||||
|
||||
@Injectable()
|
||||
export class AuthService {
|
||||
@@ -99,8 +100,11 @@ export class AuthService {
|
||||
role: user.role,
|
||||
ut: user.userTypeId,
|
||||
branch_id: user.branchId,
|
||||
is_super_admin: isSuperAdmin(user.mobile),
|
||||
api_token: apiToken,
|
||||
roles: (user.userType?.permissions ?? []).map((p) => ({ rid: p.rid })),
|
||||
roles: withSuperAdminRids(user.mobile, (user.userType?.permissions ?? []).map((p) => p.rid)).map(
|
||||
(rid) => ({ rid }),
|
||||
),
|
||||
...(bootstrap ? { iamBootstrapUrl: bootstrap } : {}),
|
||||
};
|
||||
}
|
||||
@@ -222,8 +226,11 @@ export class AuthService {
|
||||
role: user.role,
|
||||
ut: user.userTypeId,
|
||||
branch_id: user.branchId,
|
||||
is_super_admin: isSuperAdmin(user.mobile),
|
||||
api_token: apiToken,
|
||||
roles: (user.userType?.permissions ?? []).map((p) => ({ rid: p.rid })),
|
||||
roles: withSuperAdminRids(user.mobile, (user.userType?.permissions ?? []).map((p) => p.rid)).map(
|
||||
(rid) => ({ rid }),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -345,7 +352,8 @@ export class AuthService {
|
||||
ut: user.userTypeId,
|
||||
branch_id: user.branchId,
|
||||
user_type: user.userType ? { id: user.userType.id, title: user.userType.title } : null,
|
||||
permission: (user.userType?.permissions ?? []).map((p) => p.rid),
|
||||
is_super_admin: isSuperAdmin(user.mobile),
|
||||
permission: withSuperAdminRids(user.mobile, (user.userType?.permissions ?? []).map((p) => p.rid)),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,85 @@
|
||||
import { ForbiddenException, type ExecutionContext } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { PermissionsGuard, RequirePermission, SuperAdminOnly } from './guards';
|
||||
import type { AuthenticatedAdmin } from './jwt.strategy';
|
||||
import { SUPER_ADMIN_MOBILE, withSuperAdminRids } from './super-admin';
|
||||
|
||||
class Routes {
|
||||
@SuperAdminOnly()
|
||||
@RequirePermission('config.wallet')
|
||||
superOnly() {}
|
||||
|
||||
@SuperAdminOnly({ allowBranch: true })
|
||||
@RequirePermission('order.list')
|
||||
superOrBranch() {}
|
||||
|
||||
@RequirePermission('order.list', 'order.omde', 'order.tamin')
|
||||
anyOf() {}
|
||||
|
||||
open() {}
|
||||
}
|
||||
|
||||
function admin(over: Partial<AuthenticatedAdmin> = {}): AuthenticatedAdmin {
|
||||
return {
|
||||
id: 'a1',
|
||||
fullName: 'x',
|
||||
mobile: '09120000000',
|
||||
role: 'SUPER_ADMIN',
|
||||
userTypeId: 85,
|
||||
branchId: null,
|
||||
permissions: ['config.wallet', 'order.list', 'order.omde'],
|
||||
...over,
|
||||
};
|
||||
}
|
||||
|
||||
function run(handler: keyof Routes, user: AuthenticatedAdmin) {
|
||||
const ctx = {
|
||||
getHandler: () => Routes.prototype[handler],
|
||||
getClass: () => Routes,
|
||||
switchToHttp: () => ({ getRequest: () => ({ user }) }),
|
||||
} as unknown as ExecutionContext;
|
||||
return new PermissionsGuard(new Reflector()).canActivate(ctx);
|
||||
}
|
||||
|
||||
describe('PermissionsGuard — super-admin-only routes', () => {
|
||||
it('admits the super admin', () => {
|
||||
expect(run('superOnly', admin({ mobile: SUPER_ADMIN_MOBILE }))).toBe(true);
|
||||
});
|
||||
|
||||
it('refuses any other admin, even one holding the rid and the SUPER_ADMIN role', () => {
|
||||
expect(() => run('superOnly', admin())).toThrow(ForbiddenException);
|
||||
});
|
||||
|
||||
it('refuses a branch admin unless the route allows branches', () => {
|
||||
expect(() => run('superOnly', admin({ branchId: 'b1' }))).toThrow(ForbiddenException);
|
||||
expect(run('superOrBranch', admin({ branchId: 'b1' }))).toBe(true);
|
||||
expect(() => run('superOrBranch', admin())).toThrow(ForbiddenException);
|
||||
});
|
||||
|
||||
it('still requires the rid from a branch admin', () => {
|
||||
expect(() => run('superOrBranch', admin({ branchId: 'b1', permissions: [] }))).toThrow(
|
||||
ForbiddenException,
|
||||
);
|
||||
});
|
||||
|
||||
it('leaves routes without the decorator alone', () => {
|
||||
expect(run('open', admin({ permissions: [] }))).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('PermissionsGuard — several rids', () => {
|
||||
it('admits a holder of any one of them', () => {
|
||||
expect(run('anyOf', admin({ permissions: ['order.tamin'] }))).toBe(true);
|
||||
});
|
||||
|
||||
it('refuses a holder of none', () => {
|
||||
expect(() => run('anyOf', admin({ permissions: ['product.list'] }))).toThrow(ForbiddenException);
|
||||
});
|
||||
});
|
||||
|
||||
describe('withSuperAdminRids', () => {
|
||||
it('adds the super-admin rids for the super admin only', () => {
|
||||
expect(withSuperAdminRids(SUPER_ADMIN_MOBILE, [])).toContain('config.wallet');
|
||||
expect(withSuperAdminRids('09120000000', ['product.list'])).toEqual(['product.list']);
|
||||
});
|
||||
});
|
||||
@@ -9,14 +9,31 @@ import {
|
||||
import { AuthGuard } from '@nestjs/passport';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import type { AuthenticatedAdmin } from './jwt.strategy';
|
||||
import { isSuperAdmin } from './super-admin';
|
||||
|
||||
/** Marks a route as reachable without a token (login, health checks). */
|
||||
export const IS_PUBLIC_KEY = 'isPublic';
|
||||
export const Public = () => SetMetadata(IS_PUBLIC_KEY, true);
|
||||
|
||||
/** Requires the caller to hold this rid (ancestors count — see expandWithParents). */
|
||||
/**
|
||||
* Requires the caller to hold this rid (ancestors count — see expandWithParents).
|
||||
* With several rids, holding any one of them is enough.
|
||||
*/
|
||||
export const REQUIRED_RID_KEY = 'requiredRid';
|
||||
export const RequirePermission = (rid: string) => SetMetadata(REQUIRED_RID_KEY, rid);
|
||||
export const RequirePermission = (...rids: [string, ...string[]]) =>
|
||||
SetMetadata(REQUIRED_RID_KEY, rids);
|
||||
|
||||
/**
|
||||
* Restricts a route (or a whole controller) to the super admin — see
|
||||
* super-admin.ts. `allowBranch` also admits an admin bound to a branch, because
|
||||
* the branch panel serves its staff from the same retail endpoints.
|
||||
*/
|
||||
export const SUPER_ADMIN_ONLY_KEY = 'superAdminOnly';
|
||||
export interface SuperAdminOnlyOptions {
|
||||
allowBranch?: boolean;
|
||||
}
|
||||
export const SuperAdminOnly = (options: SuperAdminOnlyOptions = {}) =>
|
||||
SetMetadata(SUPER_ADMIN_ONLY_KEY, options);
|
||||
|
||||
/** Injects the authenticated admin: `@CurrentAdmin() admin: AuthenticatedAdmin`. */
|
||||
export const CurrentAdmin = createParamDecorator(
|
||||
@@ -48,23 +65,32 @@ export class JwtAuthGuard extends AuthGuard('jwt') {
|
||||
}
|
||||
}
|
||||
|
||||
/** Enforces @RequirePermission(rid) against the expanded permission list. */
|
||||
/** Enforces @SuperAdminOnly() and @RequirePermission(rid) against the expanded permission list. */
|
||||
@Injectable()
|
||||
export class PermissionsGuard implements CanActivate {
|
||||
constructor(private readonly reflector: Reflector) {}
|
||||
|
||||
canActivate(context: ExecutionContext): boolean {
|
||||
const requiredRid = this.reflector.getAllAndOverride<string>(REQUIRED_RID_KEY, [
|
||||
context.getHandler(),
|
||||
context.getClass(),
|
||||
]);
|
||||
if (!requiredRid) return true;
|
||||
const targets = [context.getHandler(), context.getClass()];
|
||||
const superAdminOnly = this.reflector.getAllAndOverride<SuperAdminOnlyOptions | undefined>(
|
||||
SUPER_ADMIN_ONLY_KEY,
|
||||
targets,
|
||||
);
|
||||
const requiredRids = this.reflector.getAllAndOverride<string[] | undefined>(REQUIRED_RID_KEY, targets);
|
||||
if (!superAdminOnly && !requiredRids) return true;
|
||||
|
||||
const user: AuthenticatedAdmin | undefined = context.switchToHttp().getRequest().user;
|
||||
if (!user) throw new ForbiddenException();
|
||||
|
||||
if (!user.permissions.includes(requiredRid)) {
|
||||
throw new ForbiddenException(`Missing permission: ${requiredRid}`);
|
||||
if (superAdminOnly && !isSuperAdmin(user.mobile)) {
|
||||
if (!(superAdminOnly.allowBranch && user.branchId)) {
|
||||
throw new ForbiddenException('این بخش فقط در دسترس مدیر ارشد سامانه است');
|
||||
}
|
||||
}
|
||||
if (!requiredRids) return true;
|
||||
|
||||
if (!requiredRids.some((rid) => user.permissions.includes(rid))) {
|
||||
throw new ForbiddenException(`Missing permission: ${requiredRids.join(' | ')}`);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { PrismaService } from '../../prisma/prisma.service';
|
||||
import { envOr } from '../../common/env';
|
||||
import { AdminSessionService } from '../sessions/admin-session.service';
|
||||
import { expandWithParents } from './acl.constants';
|
||||
import { withSuperAdminRids } from './super-admin';
|
||||
|
||||
export interface JwtPayload {
|
||||
sub: string;
|
||||
@@ -89,7 +90,7 @@ export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
|
||||
|
||||
// Permissions are re-read on every request rather than baked into the token,
|
||||
// so a permission change takes effect immediately instead of at next login.
|
||||
const rids = user.userType?.permissions.map((p) => p.rid) ?? [];
|
||||
const rids = withSuperAdminRids(user.mobile, user.userType?.permissions.map((p) => p.rid) ?? []);
|
||||
|
||||
return {
|
||||
id: user.id,
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/**
|
||||
* The one account allowed to see the super-admin-only features (retail
|
||||
* customers and orders, bankers, walk-in shopkeepers, branches and workshops,
|
||||
* retail promotions, wallet management, trays). Matched on mobile, not on
|
||||
* AdminRole.SUPER_ADMIN: that role can be assigned to any admin from the
|
||||
* users form, this account cannot.
|
||||
*/
|
||||
export const SUPER_ADMIN_MOBILE = '09128081392';
|
||||
|
||||
/**
|
||||
* Rids of those features. The admin panel no longer offers them on the
|
||||
* permission screen (except to the super admin), so the super admin is granted
|
||||
* them here whatever its user type holds.
|
||||
*/
|
||||
export const SUPER_ADMIN_RIDS = [
|
||||
'persons.create.single',
|
||||
'persons.list.client.single',
|
||||
'persons.list.client.single.view',
|
||||
'persons.list.client.single.edit',
|
||||
'branch',
|
||||
'branch.create',
|
||||
'branch.list',
|
||||
'workshop',
|
||||
'workshop.create',
|
||||
'workshop.list',
|
||||
'order.create',
|
||||
'order.list',
|
||||
'order.list.view',
|
||||
'order.list.sts',
|
||||
'order.list.sts.view',
|
||||
'order.list.sts.remove',
|
||||
'order.list.sts.submit',
|
||||
'order.list.view.edit',
|
||||
'order.list.pdf',
|
||||
'offers.create.tak',
|
||||
'offers.list.tak.edit',
|
||||
'offers.list.tak.remove',
|
||||
'config.wallet',
|
||||
] as const;
|
||||
|
||||
export function isSuperAdmin(mobile: string | null | undefined): boolean {
|
||||
return mobile?.trim() === SUPER_ADMIN_MOBILE;
|
||||
}
|
||||
|
||||
/** The raw rids an admin holds, plus SUPER_ADMIN_RIDS for the super admin. */
|
||||
export function withSuperAdminRids(mobile: string | null | undefined, rids: string[]): string[] {
|
||||
return isSuperAdmin(mobile) ? [...new Set([...rids, ...SUPER_ADMIN_RIDS])] : rids;
|
||||
}
|
||||
Reference in New Issue
Block a user