import { Body, Controller, Delete, ForbiddenException, Get, HttpCode, Param, Post, Put, } from '@nestjs/common'; import { AdminMiscService } from './admin-misc.service'; import { TableQueryDto } from '../admin-table/table-query.dto'; import { CurrentAdmin, RequirePermission } from '../auth/guards'; import type { AuthenticatedAdmin } from '../auth/jwt.strategy'; import { isAccountingSettingKey } from '../accounting-sync/accounting-config'; /** * Cities, SMS, notifications, newsletter, activity log and settings — the * remaining admin screens. See AdminMiscService for the legacy endpoint map. */ @Controller('admin/misc') export class AdminMiscController { constructor(private readonly misc: AdminMiscService) {} // ── cities ──────────────────────────────────────────────────────── @Post('cities/list') @HttpCode(200) @RequirePermission('cities.list') listCities(@Body() query: TableQueryDto) { return this.misc.listCities(query); } @Post('cities') @HttpCode(200) @RequirePermission('cities.create') createCity(@Body() data: { title?: string; name?: string; province?: string }) { return this.misc.createCity(data); } @Get('cities/percent/:id') @RequirePermission('cities.list') getCityPercent(@Param('id') id: string) { return this.misc.getCityPercent(id, false); } @Get('cities/percent-aux/:id') @RequirePermission('cities.list') getCityPercentAux(@Param('id') id: string) { return this.misc.getCityPercent(id, true); } @Put('cities/:id') @RequirePermission('cities.list.view.edit') updateCity(@Param('id') id: string, @Body() data: Record) { return this.misc.updateCity(id, data); } @Delete('cities/:id') @RequirePermission('cities.list.remove') removeCity(@Param('id') id: string) { return this.misc.removeCity(id); } // ── sms ─────────────────────────────────────────────────────────── @Post('sms/list') @HttpCode(200) @RequirePermission('sms.list') listSms(@Body() query: TableQueryDto) { return this.misc.listSms(query); } @Post('sms') @HttpCode(200) @RequirePermission('sms.create') sendSms( @Body() data: { type?: string | number; mobile?: string; text?: string }, @CurrentAdmin() admin: AuthenticatedAdmin, ) { return this.misc.sendSms(data, admin.id); } // ── notifications ───────────────────────────────────────────────── @Post('notify/list') @HttpCode(200) @RequirePermission('notify.me') listNotifications(@Body() query: TableQueryDto) { return this.misc.listNotifications(query); } @Post('notify') @HttpCode(200) @RequirePermission('notify.create') createNotification( @Body() data: { title?: string; text?: string; body?: string; type?: string | number }, @CurrentAdmin() admin: AuthenticatedAdmin, ) { return this.misc.createNotification(data, admin.id); } @Post('notify/customer') @HttpCode(200) @RequirePermission('notify.create') notifyCustomer( @Body() data: { customer_id?: string; title?: string; text?: string; body?: string }, @CurrentAdmin() admin: AuthenticatedAdmin, ) { return this.misc.notifyCustomer(data, admin.id); } @Get('notify/read/:id') @RequirePermission('notify.me') markRead(@Param('id') id: string) { return this.misc.markNotificationRead(id); } @Get('notify/:id') @RequirePermission('notify.me') findNotification(@Param('id') id: string) { return this.misc.findNotification(id); } // ── newsletter ──────────────────────────────────────────────────── @Post('newsletter/list') @HttpCode(200) @RequirePermission('newsletters') listNewsletter(@Body() query: TableQueryDto) { return this.misc.listNewsletter(query); } // ── activity log ────────────────────────────────────────────────── @Get('log/customers') @RequirePermission('statics') logCustomers() { return this.misc.logCustomers(); } @Get('log/customer/:id') @RequirePermission('statics') customerLog(@Param('id') id: string) { return this.misc.customerLog(id); } // ── settings + brands ("مدیریت ثابت ها") ───────────────────────── @Get('dashboard-links') @RequirePermission('config.report') dashboardLinks() { return this.misc.dashboardLinks(); } @Get('const') @RequirePermission('config.statics') listConsts() { return this.misc.listConsts(); } @Post('brands') @HttpCode(200) @RequirePermission('config.statics.brand.create') createBrand(@Body() body: { title: string }) { return this.misc.createBrand(body.title); } @Put('brands/:id') @RequirePermission('config.statics.brand.edit') updateBrand(@Param('id') id: string, @Body() body: { title: string }) { return this.misc.updateBrand(id, body.title); } @Delete('brands/:id') @RequirePermission('config.statics.brand.remove') removeBrand(@Param('id') id: string) { return this.misc.removeBrand(id); } // The mobile home slider ("اسلایدر موبایل") is stored under the same // `Setting` table as every other `const/:key` entry (key: mobile_main_slider), // but it gets its own permission (`custom.config.statics.slider`, per // acl.constants.ts / permissionConst.ts) rather than the broad `config.statics` // that guards the rest of the "مدیریت ثابت ها" screen — uploading/replacing the // public site banner is more sensitive than editing brand/tag lists, so it // should not be reachable by everyone who can merely open that screen. // Routes declared ahead of `const/:key` so Nest doesn't match `const/slider` // (the legacy REST shape) to the generic key handler instead. @Get('const/slider') @RequirePermission('custom.config.statics.slider') getSlider() { return this.misc.getConst('mobile_main_slider'); } @Put('const/slider') @RequirePermission('custom.config.statics.slider') setSlider(@Body() body: Record) { return this.misc.setConst('mobile_main_slider', body.val !== undefined ? body.val : body); } @Delete('const/slider') @RequirePermission('custom.config.statics.slider') removeSlider() { return this.misc.removeConst('mobile_main_slider'); } @Get('const/:key') @RequirePermission('config.statics') getConst(@Param('key') key: string) { this.assertNotSliderKey(key); return this.misc.getConst(key); } @Put('const/:key') @RequirePermission('config.statics') setConst(@Param('key') key: string, @Body() body: Record) { this.assertNotSliderKey(key); // Legacy posts either a bare value under `val` or a whole object. return this.misc.setConst(key, body.val !== undefined ? body.val : body); } @Delete('const/:key') @RequirePermission('config.statics') removeConst(@Param('key') key: string) { this.assertNotSliderKey(key); return this.misc.removeConst(key); } // Blocks the generic `config.statics`-gated routes from reaching the slider // setting by its raw key — otherwise anyone with `config.statics` could // bypass the dedicated `const/slider` routes (guarded by the stricter // `custom.config.statics.slider`) simply by calling `const/mobile_main_slider` // directly, since the underlying Setting row is keyed the same either way. private assertNotSliderKey(key: string) { if (key === 'mobile_main_slider' || isAccountingSettingKey(key)) { throw new ForbiddenException('این تنظیم فقط از مسیر اختصاصی خود قابل دسترسی است'); } } }