feat: add 'other' product type for miscellaneous sites (tracking only, no remote control)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-10-07 16:52:23 +03:30
co-authored by Cursor
parent 0c29222e9d
commit 674da4e939
13 changed files with 34 additions and 15 deletions
+1 -1
View File
@@ -89,7 +89,7 @@ function hostFromUrl(url?: string | null) {
}
function classifyProduct(row: SourceUser, domains: string[]): {
product: ProductType;
product: Exclude<ProductType, 'other'>;
confident: boolean;
reason: string;
} {
@@ -0,0 +1,2 @@
-- AlterEnum
ALTER TYPE "public"."ProductType" ADD VALUE IF NOT EXISTS 'other';
+2
View File
@@ -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).
+9 -2
View File
@@ -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';
}
+2 -2
View File
@@ -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,
+1
View File
@@ -24,6 +24,7 @@ export class RemoteTokenGuard implements CanActivate {
where: {
remoteApiToken: token,
isActive: true,
product: { not: 'other' },
},
select: {
id: true,
+1 -1
View File
@@ -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);
+1 -1
View File
@@ -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,
+1 -1
View File
@@ -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,
@@ -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;
}
+3
View File
@@ -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;
+6 -6
View File
@@ -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');
+1 -1
View File
@@ -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,