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:
@@ -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';
|
||||
@@ -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).
|
||||
|
||||
@@ -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';
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -24,6 +24,7 @@ export class RemoteTokenGuard implements CanActivate {
|
||||
where: {
|
||||
remoteApiToken: token,
|
||||
isActive: true,
|
||||
product: { not: 'other' },
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user