From 674da4e9391ff2d38f5448269d1efcb204c11135 Mon Sep 17 00:00:00 2001 From: Mahdi Date: Wed, 7 Oct 2026 16:52:23 +0330 Subject: [PATCH] feat: add 'other' product type for miscellaneous sites (tracking only, no remote control) Co-authored-by: Cursor --- prisma/import-users.ts | 2 +- .../20261007120000_product_type_other/migration.sql | 2 ++ prisma/schema.prisma | 2 ++ src/auth/permissions.ts | 11 +++++++++-- src/auth/remote-sms-auth.guard.ts | 4 ++-- src/auth/remote-token.guard.ts | 1 + src/get-user/get-user.service.ts | 2 +- src/get-version/get-version.service.ts | 2 +- src/plugins/plugin-store.service.ts | 2 +- src/settings-proxy/settings-proxy.service.ts | 4 ++++ src/tenants/dto/create-tenant.dto.ts | 3 +++ src/tenants/tenants.controller.ts | 12 ++++++------ src/tenants/tenants.service.ts | 2 +- 13 files changed, 34 insertions(+), 15 deletions(-) create mode 100644 prisma/migrations/20261007120000_product_type_other/migration.sql diff --git a/prisma/import-users.ts b/prisma/import-users.ts index 5051c3f..32f11b6 100644 --- a/prisma/import-users.ts +++ b/prisma/import-users.ts @@ -89,7 +89,7 @@ function hostFromUrl(url?: string | null) { } function classifyProduct(row: SourceUser, domains: string[]): { - product: ProductType; + product: Exclude; confident: boolean; reason: string; } { diff --git a/prisma/migrations/20261007120000_product_type_other/migration.sql b/prisma/migrations/20261007120000_product_type_other/migration.sql new file mode 100644 index 0000000..49b58cc --- /dev/null +++ b/prisma/migrations/20261007120000_product_type_other/migration.sql @@ -0,0 +1,2 @@ +-- AlterEnum +ALTER TYPE "public"."ProductType" ADD VALUE IF NOT EXISTS 'other'; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 4f3817c..8fb9d0c 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -8,9 +8,11 @@ datasource db { url = env("DATABASE_URL") } +/// other = سایت‌های متفرقه: فقط ثبت مشخصات و پیگیری سرور/افزونه/پشتیبانی/پیامک؛ بدون کنترل ریموت و بدون get_user. enum ProductType { hamkar khanegi + other } /// Khanegi front-end appearance family (ignored for hamkar). diff --git a/src/auth/permissions.ts b/src/auth/permissions.ts index 8cfd60d..c24265c 100644 --- a/src/auth/permissions.ts +++ b/src/auth/permissions.ts @@ -12,6 +12,8 @@ export const PERMISSIONS = [ 'hamkar:write', 'khanegi:read', 'khanegi:write', + 'others:read', + 'others:write', 'admins:read', 'admins:write', 'audit:read', @@ -36,6 +38,7 @@ export const PERMISSION_MODULES: PermissionModule[] = [ { key: 'support', label: 'پشتیبانی', read: 'support:read', write: 'support:write' }, { key: 'hamkar', label: 'همکار', read: 'hamkar:read', write: 'hamkar:write' }, { key: 'khanegi', label: 'خانگی', read: 'khanegi:read', write: 'khanegi:write' }, + { key: 'others', label: 'سایت‌های دیگر', read: 'others:read', write: 'others:write' }, { key: 'admins', label: 'مدیران', read: 'admins:read', write: 'admins:write' }, { key: 'audit', label: 'لاگ', read: 'audit:read' }, { key: 'settings', label: 'تنظیمات حساس', write: 'settings:write' }, @@ -94,9 +97,13 @@ export function userHasAllPermissions( } export function productReadPermission(product: string): PermissionCode { - return product === 'hamkar' ? 'hamkar:read' : 'khanegi:read'; + if (product === 'hamkar') return 'hamkar:read'; + if (product === 'other') return 'others:read'; + return 'khanegi:read'; } export function productWritePermission(product: string): PermissionCode { - return product === 'hamkar' ? 'hamkar:write' : 'khanegi:write'; + if (product === 'hamkar') return 'hamkar:write'; + if (product === 'other') return 'others:write'; + return 'khanegi:write'; } diff --git a/src/auth/remote-sms-auth.guard.ts b/src/auth/remote-sms-auth.guard.ts index 30e7683..a590519 100644 --- a/src/auth/remote-sms-auth.guard.ts +++ b/src/auth/remote-sms-auth.guard.ts @@ -130,7 +130,7 @@ export class RemoteSmsAuthGuard implements CanActivate { if (!token || token.length < 16) return null; return this.prisma.tenant.findFirst({ - where: { remoteApiToken: token, isActive: true }, + where: { remoteApiToken: token, isActive: true, product: { not: 'other' } }, select: { id: true, nameFa: true, @@ -146,7 +146,7 @@ export class RemoteSmsAuthGuard implements CanActivate { if (!normalizedHost) return null; const tenants = await this.prisma.tenant.findMany({ - where: { isActive: true }, + where: { isActive: true, product: { not: 'other' } }, select: { id: true, nameFa: true, diff --git a/src/auth/remote-token.guard.ts b/src/auth/remote-token.guard.ts index 1fd4020..8d2d4cc 100644 --- a/src/auth/remote-token.guard.ts +++ b/src/auth/remote-token.guard.ts @@ -24,6 +24,7 @@ export class RemoteTokenGuard implements CanActivate { where: { remoteApiToken: token, isActive: true, + product: { not: 'other' }, }, select: { id: true, diff --git a/src/get-user/get-user.service.ts b/src/get-user/get-user.service.ts index ab07a2c..27f84cd 100644 --- a/src/get-user/get-user.service.ts +++ b/src/get-user/get-user.service.ts @@ -42,7 +42,7 @@ export class GetUserService { } const tenants = await this.prisma.tenant.findMany({ - where: { isActive: true }, + where: { isActive: true, product: { not: 'other' } }, }); const tenant = this.findTenantForHost(tenants, host); diff --git a/src/get-version/get-version.service.ts b/src/get-version/get-version.service.ts index a4b5f4e..b35c0dd 100644 --- a/src/get-version/get-version.service.ts +++ b/src/get-version/get-version.service.ts @@ -12,7 +12,7 @@ export class GetVersionService { } const tenants = await this.prisma.tenant.findMany({ - where: { isActive: true }, + where: { isActive: true, product: { not: 'other' } }, select: { appVersion: true, domain: true, diff --git a/src/plugins/plugin-store.service.ts b/src/plugins/plugin-store.service.ts index 332ba3a..6aaaf5d 100644 --- a/src/plugins/plugin-store.service.ts +++ b/src/plugins/plugin-store.service.ts @@ -505,7 +505,7 @@ export class PluginStoreService implements OnModuleInit, OnModuleDestroy { } const tenants = await this.prisma.tenant.findMany({ - where: { isActive: true }, + where: { isActive: true, product: { not: ProductType.other } }, select: { id: true, nameFa: true, diff --git a/src/settings-proxy/settings-proxy.service.ts b/src/settings-proxy/settings-proxy.service.ts index cabd692..21ed47a 100644 --- a/src/settings-proxy/settings-proxy.service.ts +++ b/src/settings-proxy/settings-proxy.service.ts @@ -1,5 +1,6 @@ import { BadGatewayException, + BadRequestException, Injectable, NotFoundException, } from '@nestjs/common'; @@ -257,6 +258,9 @@ export class SettingsProxyService { if (!tenant) { throw new NotFoundException('نسخه یافت نشد'); } + if (tenant.product === 'other') { + throw new BadRequestException('سایت‌های دیگر کنترل ریموت ندارند'); + } return tenant; } diff --git a/src/tenants/dto/create-tenant.dto.ts b/src/tenants/dto/create-tenant.dto.ts index e948a41..510fa99 100644 --- a/src/tenants/dto/create-tenant.dto.ts +++ b/src/tenants/dto/create-tenant.dto.ts @@ -8,6 +8,7 @@ import { Matches, MaxLength, MinLength, + ValidateIf, } from 'class-validator'; import { KhanegiUiVariant, ProductType } from '@prisma/client'; @@ -64,6 +65,8 @@ export class CreateTenantDto { @MaxLength(32) backgroundColor?: string; + /** Not required for product=other (no remote connection). */ + @ValidateIf((o) => o.product !== ProductType.other || Boolean(o.nodeServer)) @IsString() @MinLength(8) nodeServer!: string; diff --git a/src/tenants/tenants.controller.ts b/src/tenants/tenants.controller.ts index f34815c..59320d7 100644 --- a/src/tenants/tenants.controller.ts +++ b/src/tenants/tenants.controller.ts @@ -49,7 +49,7 @@ export class TenantsController { ) {} @Get() - @RequirePermissions('hamkar:read', 'khanegi:read', { mode: 'any' }) + @RequirePermissions('hamkar:read', 'khanegi:read', 'others:read', { mode: 'any' }) async findAll( @Query('product') product?: ProductType, @CurrentUser() user?: AuthUser, @@ -65,7 +65,7 @@ export class TenantsController { } @Post('upload-avatar') - @RequirePermissions('hamkar:write', 'khanegi:write', { mode: 'any' }) + @RequirePermissions('hamkar:write', 'khanegi:write', 'others:write', { mode: 'any' }) @UseInterceptors( FileInterceptor('file', { storage: diskStorage({ @@ -103,7 +103,7 @@ export class TenantsController { } @Get(':id') - @RequirePermissions('hamkar:read', 'khanegi:read', { mode: 'any' }) + @RequirePermissions('hamkar:read', 'khanegi:read', 'others: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'); @@ -111,14 +111,14 @@ export class TenantsController { } @Post() - @RequirePermissions('hamkar:write', 'khanegi:write', { mode: 'any' }) + @RequirePermissions('hamkar:write', 'khanegi:write', 'others:write', { mode: 'any' }) create(@Body() dto: CreateTenantDto, @CurrentUser() user: AuthUser) { this.assertProductAccess(user, dto.product, 'write'); return this.tenantsService.create(dto, user.id); } @Patch(':id') - @RequirePermissions('hamkar:write', 'khanegi:write', { mode: 'any' }) + @RequirePermissions('hamkar:write', 'khanegi:write', 'others:write', { mode: 'any' }) async update( @Param('id') id: string, @Body() dto: UpdateTenantDto, @@ -130,7 +130,7 @@ export class TenantsController { } @Delete(':id') - @RequirePermissions('hamkar:write', 'khanegi:write', { mode: 'any' }) + @RequirePermissions('hamkar:write', 'khanegi:write', 'others:write', { mode: 'any' }) async remove(@Param('id') id: string, @CurrentUser() user: AuthUser) { const tenant = await this.tenantsService.findOne(id); this.assertProductAccess(user, tenant.product, 'write'); diff --git a/src/tenants/tenants.service.ts b/src/tenants/tenants.service.ts index 404c53a..444c4a9 100644 --- a/src/tenants/tenants.service.ts +++ b/src/tenants/tenants.service.ts @@ -93,7 +93,7 @@ export class TenantsService { primaryColor: dto.primaryColor, secondaryColor: dto.secondaryColor, backgroundColor: dto.backgroundColor, - nodeServer: dto.nodeServer, + nodeServer: dto.nodeServer ?? '', nodeWs: dto.nodeWs, appUrl: dto.appUrl, adminUrl: dto.adminUrl,