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[]): {
|
function classifyProduct(row: SourceUser, domains: string[]): {
|
||||||
product: ProductType;
|
product: Exclude<ProductType, 'other'>;
|
||||||
confident: boolean;
|
confident: boolean;
|
||||||
reason: string;
|
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")
|
url = env("DATABASE_URL")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// other = سایتهای متفرقه: فقط ثبت مشخصات و پیگیری سرور/افزونه/پشتیبانی/پیامک؛ بدون کنترل ریموت و بدون get_user.
|
||||||
enum ProductType {
|
enum ProductType {
|
||||||
hamkar
|
hamkar
|
||||||
khanegi
|
khanegi
|
||||||
|
other
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Khanegi front-end appearance family (ignored for hamkar).
|
/// Khanegi front-end appearance family (ignored for hamkar).
|
||||||
|
|||||||
@@ -12,6 +12,8 @@ export const PERMISSIONS = [
|
|||||||
'hamkar:write',
|
'hamkar:write',
|
||||||
'khanegi:read',
|
'khanegi:read',
|
||||||
'khanegi:write',
|
'khanegi:write',
|
||||||
|
'others:read',
|
||||||
|
'others:write',
|
||||||
'admins:read',
|
'admins:read',
|
||||||
'admins:write',
|
'admins:write',
|
||||||
'audit:read',
|
'audit:read',
|
||||||
@@ -36,6 +38,7 @@ export const PERMISSION_MODULES: PermissionModule[] = [
|
|||||||
{ key: 'support', label: 'پشتیبانی', read: 'support:read', write: 'support:write' },
|
{ key: 'support', label: 'پشتیبانی', read: 'support:read', write: 'support:write' },
|
||||||
{ key: 'hamkar', label: 'همکار', read: 'hamkar:read', write: 'hamkar:write' },
|
{ key: 'hamkar', label: 'همکار', read: 'hamkar:read', write: 'hamkar:write' },
|
||||||
{ key: 'khanegi', label: 'خانگی', read: 'khanegi:read', write: 'khanegi: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: 'admins', label: 'مدیران', read: 'admins:read', write: 'admins:write' },
|
||||||
{ key: 'audit', label: 'لاگ', read: 'audit:read' },
|
{ key: 'audit', label: 'لاگ', read: 'audit:read' },
|
||||||
{ key: 'settings', label: 'تنظیمات حساس', write: 'settings:write' },
|
{ key: 'settings', label: 'تنظیمات حساس', write: 'settings:write' },
|
||||||
@@ -94,9 +97,13 @@ export function userHasAllPermissions(
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function productReadPermission(product: string): PermissionCode {
|
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 {
|
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;
|
if (!token || token.length < 16) return null;
|
||||||
|
|
||||||
return this.prisma.tenant.findFirst({
|
return this.prisma.tenant.findFirst({
|
||||||
where: { remoteApiToken: token, isActive: true },
|
where: { remoteApiToken: token, isActive: true, product: { not: 'other' } },
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
nameFa: true,
|
nameFa: true,
|
||||||
@@ -146,7 +146,7 @@ export class RemoteSmsAuthGuard implements CanActivate {
|
|||||||
if (!normalizedHost) return null;
|
if (!normalizedHost) return null;
|
||||||
|
|
||||||
const tenants = await this.prisma.tenant.findMany({
|
const tenants = await this.prisma.tenant.findMany({
|
||||||
where: { isActive: true },
|
where: { isActive: true, product: { not: 'other' } },
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
nameFa: true,
|
nameFa: true,
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ export class RemoteTokenGuard implements CanActivate {
|
|||||||
where: {
|
where: {
|
||||||
remoteApiToken: token,
|
remoteApiToken: token,
|
||||||
isActive: true,
|
isActive: true,
|
||||||
|
product: { not: 'other' },
|
||||||
},
|
},
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export class GetUserService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tenants = await this.prisma.tenant.findMany({
|
const tenants = await this.prisma.tenant.findMany({
|
||||||
where: { isActive: true },
|
where: { isActive: true, product: { not: 'other' } },
|
||||||
});
|
});
|
||||||
|
|
||||||
const tenant = this.findTenantForHost(tenants, host);
|
const tenant = this.findTenantForHost(tenants, host);
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ export class GetVersionService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tenants = await this.prisma.tenant.findMany({
|
const tenants = await this.prisma.tenant.findMany({
|
||||||
where: { isActive: true },
|
where: { isActive: true, product: { not: 'other' } },
|
||||||
select: {
|
select: {
|
||||||
appVersion: true,
|
appVersion: true,
|
||||||
domain: true,
|
domain: true,
|
||||||
|
|||||||
@@ -505,7 +505,7 @@ export class PluginStoreService implements OnModuleInit, OnModuleDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const tenants = await this.prisma.tenant.findMany({
|
const tenants = await this.prisma.tenant.findMany({
|
||||||
where: { isActive: true },
|
where: { isActive: true, product: { not: ProductType.other } },
|
||||||
select: {
|
select: {
|
||||||
id: true,
|
id: true,
|
||||||
nameFa: true,
|
nameFa: true,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import {
|
import {
|
||||||
BadGatewayException,
|
BadGatewayException,
|
||||||
|
BadRequestException,
|
||||||
Injectable,
|
Injectable,
|
||||||
NotFoundException,
|
NotFoundException,
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
@@ -257,6 +258,9 @@ export class SettingsProxyService {
|
|||||||
if (!tenant) {
|
if (!tenant) {
|
||||||
throw new NotFoundException('نسخه یافت نشد');
|
throw new NotFoundException('نسخه یافت نشد');
|
||||||
}
|
}
|
||||||
|
if (tenant.product === 'other') {
|
||||||
|
throw new BadRequestException('سایتهای دیگر کنترل ریموت ندارند');
|
||||||
|
}
|
||||||
return tenant;
|
return tenant;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import {
|
|||||||
Matches,
|
Matches,
|
||||||
MaxLength,
|
MaxLength,
|
||||||
MinLength,
|
MinLength,
|
||||||
|
ValidateIf,
|
||||||
} from 'class-validator';
|
} from 'class-validator';
|
||||||
import { KhanegiUiVariant, ProductType } from '@prisma/client';
|
import { KhanegiUiVariant, ProductType } from '@prisma/client';
|
||||||
|
|
||||||
@@ -64,6 +65,8 @@ export class CreateTenantDto {
|
|||||||
@MaxLength(32)
|
@MaxLength(32)
|
||||||
backgroundColor?: string;
|
backgroundColor?: string;
|
||||||
|
|
||||||
|
/** Not required for product=other (no remote connection). */
|
||||||
|
@ValidateIf((o) => o.product !== ProductType.other || Boolean(o.nodeServer))
|
||||||
@IsString()
|
@IsString()
|
||||||
@MinLength(8)
|
@MinLength(8)
|
||||||
nodeServer!: string;
|
nodeServer!: string;
|
||||||
|
|||||||
@@ -49,7 +49,7 @@ export class TenantsController {
|
|||||||
) {}
|
) {}
|
||||||
|
|
||||||
@Get()
|
@Get()
|
||||||
@RequirePermissions('hamkar:read', 'khanegi:read', { mode: 'any' })
|
@RequirePermissions('hamkar:read', 'khanegi:read', 'others:read', { mode: 'any' })
|
||||||
async findAll(
|
async findAll(
|
||||||
@Query('product') product?: ProductType,
|
@Query('product') product?: ProductType,
|
||||||
@CurrentUser() user?: AuthUser,
|
@CurrentUser() user?: AuthUser,
|
||||||
@@ -65,7 +65,7 @@ export class TenantsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post('upload-avatar')
|
@Post('upload-avatar')
|
||||||
@RequirePermissions('hamkar:write', 'khanegi:write', { mode: 'any' })
|
@RequirePermissions('hamkar:write', 'khanegi:write', 'others:write', { mode: 'any' })
|
||||||
@UseInterceptors(
|
@UseInterceptors(
|
||||||
FileInterceptor('file', {
|
FileInterceptor('file', {
|
||||||
storage: diskStorage({
|
storage: diskStorage({
|
||||||
@@ -103,7 +103,7 @@ export class TenantsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Get(':id')
|
@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) {
|
async findOne(@Param('id') id: string, @CurrentUser() user: AuthUser) {
|
||||||
const tenant = await this.tenantsService.findOne(id);
|
const tenant = await this.tenantsService.findOne(id);
|
||||||
this.assertProductAccess(user, tenant.product, 'read');
|
this.assertProductAccess(user, tenant.product, 'read');
|
||||||
@@ -111,14 +111,14 @@ export class TenantsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Post()
|
@Post()
|
||||||
@RequirePermissions('hamkar:write', 'khanegi:write', { mode: 'any' })
|
@RequirePermissions('hamkar:write', 'khanegi:write', 'others:write', { mode: 'any' })
|
||||||
create(@Body() dto: CreateTenantDto, @CurrentUser() user: AuthUser) {
|
create(@Body() dto: CreateTenantDto, @CurrentUser() user: AuthUser) {
|
||||||
this.assertProductAccess(user, dto.product, 'write');
|
this.assertProductAccess(user, dto.product, 'write');
|
||||||
return this.tenantsService.create(dto, user.id);
|
return this.tenantsService.create(dto, user.id);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Patch(':id')
|
@Patch(':id')
|
||||||
@RequirePermissions('hamkar:write', 'khanegi:write', { mode: 'any' })
|
@RequirePermissions('hamkar:write', 'khanegi:write', 'others:write', { mode: 'any' })
|
||||||
async update(
|
async update(
|
||||||
@Param('id') id: string,
|
@Param('id') id: string,
|
||||||
@Body() dto: UpdateTenantDto,
|
@Body() dto: UpdateTenantDto,
|
||||||
@@ -130,7 +130,7 @@ export class TenantsController {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Delete(':id')
|
@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) {
|
async remove(@Param('id') id: string, @CurrentUser() user: AuthUser) {
|
||||||
const tenant = await this.tenantsService.findOne(id);
|
const tenant = await this.tenantsService.findOne(id);
|
||||||
this.assertProductAccess(user, tenant.product, 'write');
|
this.assertProductAccess(user, tenant.product, 'write');
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ export class TenantsService {
|
|||||||
primaryColor: dto.primaryColor,
|
primaryColor: dto.primaryColor,
|
||||||
secondaryColor: dto.secondaryColor,
|
secondaryColor: dto.secondaryColor,
|
||||||
backgroundColor: dto.backgroundColor,
|
backgroundColor: dto.backgroundColor,
|
||||||
nodeServer: dto.nodeServer,
|
nodeServer: dto.nodeServer ?? '',
|
||||||
nodeWs: dto.nodeWs,
|
nodeWs: dto.nodeWs,
|
||||||
appUrl: dto.appUrl,
|
appUrl: dto.appUrl,
|
||||||
adminUrl: dto.adminUrl,
|
adminUrl: dto.adminUrl,
|
||||||
|
|||||||
Reference in New Issue
Block a user