2026-09-21 08:32:31 +03:30
|
|
|
import {
|
|
|
|
|
Body,
|
|
|
|
|
Controller,
|
|
|
|
|
Delete,
|
|
|
|
|
ForbiddenException,
|
|
|
|
|
Get,
|
|
|
|
|
HttpCode,
|
|
|
|
|
Param,
|
|
|
|
|
Post,
|
|
|
|
|
Put,
|
|
|
|
|
} from '@nestjs/common';
|
2026-09-13 21:02:25 +03:30
|
|
|
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';
|
2026-10-06 16:48:36 +03:30
|
|
|
import { isAccountingSettingKey } from '../accounting-sync/accounting-config';
|
2026-09-13 21:02:25 +03:30
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 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<string, never>) {
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-21 08:32:31 +03:30
|
|
|
// 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<string, unknown>) {
|
|
|
|
|
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');
|
|
|
|
|
}
|
|
|
|
|
|
2026-09-13 21:02:25 +03:30
|
|
|
@Get('const/:key')
|
|
|
|
|
@RequirePermission('config.statics')
|
|
|
|
|
getConst(@Param('key') key: string) {
|
2026-09-21 08:32:31 +03:30
|
|
|
this.assertNotSliderKey(key);
|
2026-09-13 21:02:25 +03:30
|
|
|
return this.misc.getConst(key);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Put('const/:key')
|
|
|
|
|
@RequirePermission('config.statics')
|
|
|
|
|
setConst(@Param('key') key: string, @Body() body: Record<string, unknown>) {
|
2026-09-21 08:32:31 +03:30
|
|
|
this.assertNotSliderKey(key);
|
2026-09-13 21:02:25 +03:30
|
|
|
// 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) {
|
2026-09-21 08:32:31 +03:30
|
|
|
this.assertNotSliderKey(key);
|
2026-09-13 21:02:25 +03:30
|
|
|
return this.misc.removeConst(key);
|
|
|
|
|
}
|
2026-09-21 08:32:31 +03:30
|
|
|
|
|
|
|
|
// 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) {
|
2026-10-06 16:48:36 +03:30
|
|
|
if (key === 'mobile_main_slider' || isAccountingSettingKey(key)) {
|
2026-09-21 08:32:31 +03:30
|
|
|
throw new ForbiddenException('این تنظیم فقط از مسیر اختصاصی خود قابل دسترسی است');
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-09-13 21:02:25 +03:30
|
|
|
}
|