admin user and ui panel

This commit is contained in:
s.esmaeili
2026-09-21 08:32:31 +03:30
parent 79226c9f13
commit ef1d4638cc
341 changed files with 2270 additions and 657 deletions
@@ -1,4 +1,14 @@
import { Body, Controller, Delete, Get, HttpCode, Param, Post, Put } from '@nestjs/common';
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';
@@ -162,15 +172,44 @@ export class AdminMiscController {
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<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');
}
@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<string, unknown>) {
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);
}
@@ -178,6 +217,18 @@ export class AdminMiscController {
@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') {
throw new ForbiddenException('این تنظیم فقط از مسیر اختصاصی خود قابل دسترسی است');
}
}
}
@@ -1,11 +1,12 @@
import { Module } from '@nestjs/common';
import { PrismaModule } from '../../prisma/prisma.module';
import { AdminMiscController } from './admin-misc.controller';
import { PublicSliderController } from './public-slider.controller';
import { AdminMiscService } from './admin-misc.service';
@Module({
imports: [PrismaModule],
controllers: [AdminMiscController],
controllers: [AdminMiscController, PublicSliderController],
providers: [AdminMiscService],
})
export class AdminMiscModule {}
@@ -0,0 +1,26 @@
import { Controller, Get } from '@nestjs/common';
import { AdminMiscService } from './admin-misc.service';
import { Public } from '../auth/guards';
/**
* Public read of the admin-managed home slider ("اسلایدر موبایل" in
* manage/Const.vue), served at GET /api/home/slider.
*
* The admin write path (const/slider, guarded by the dedicated
* `custom.config.statics.slider` permission — see admin-misc.controller.ts)
* stores it under the same `Setting` row (key: mobile_main_slider) that this
* reads. Split into its own controller/prefix, rather than exposing it under
* `admin/misc`, so the shopper-facing site does not call an `/admin/...` path
* unauthenticated.
*/
@Public()
@Controller('home')
export class PublicSliderController {
constructor(private readonly misc: AdminMiscService) {}
@Get('slider')
async getSlider() {
const value = await this.misc.getConst('mobile_main_slider');
return Array.isArray(value) ? value : [];
}
}