fix super admin

This commit is contained in:
2026-09-10 10:04:55 +03:30
parent bcf4724ac4
commit a78636d32e
4 changed files with 214 additions and 11 deletions
+11
View File
@@ -56,6 +56,17 @@ describe('findTenantForHost', () => {
expect(scoreTenantHost(mine, api)).toBe(75);
});
it('full match uses extra IP aliases like get_user', () => {
const tenant = fromDomain('k.montzar.ir', {
domains: ['192.168.70.191', '192.168.70.184', '192.168.70.158'],
});
expect(findTenantForHost([tenant], '192.168.70.191')?.domain).toBe(
'k.montzar.ir',
);
expect(scoreTenantHost(tenant, '192.168.70.191')).toBe(90);
});
it('does not pick a tenant when only a shared nodeServer matches', () => {
const api = 'box.example.ir';
const left = fromDomain('a.ir', { nodeServer: `https://${api}/server` });
+87 -10
View File
@@ -7,7 +7,6 @@ import {
NotFoundException,
OnModuleDestroy,
OnModuleInit,
ServiceUnavailableException,
} from '@nestjs/common';
import {
PluginCatalogItem,
@@ -33,6 +32,11 @@ import {
iranDaysUntil,
} from '../common/iran-time.util';
import { rewriteLegacyUploadUrl } from '../uploads/uploads.util';
import {
findTenantForHost,
normalizeTenantHost,
scoreTenantHost,
} from '../auth/tenant-host.util';
import {
daysToPlanMonths,
inferSettingKeyFromName,
@@ -493,17 +497,90 @@ export class PluginStoreService implements OnModuleInit, OnModuleDestroy {
return { ok: true };
}
async resolvePublicTenant(domainRaw?: string, originRaw?: string) {
const host = normalizeTenantHost(domainRaw || '');
if (!host) {
throw new BadRequestException('دامنه لازم است');
}
const tenants = await this.prisma.tenant.findMany({
where: { isActive: true },
select: {
id: true,
nameFa: true,
domain: true,
domains: true,
adminUrl: true,
appUrl: true,
nodeServer: true,
product: true,
isActive: true,
},
});
const tenant = findTenantForHost(tenants, host);
if (!tenant) {
throw new NotFoundException('نسخه‌ای با این دامنه یافت نشد');
}
if (tenant.product !== ProductType.khanegi) {
throw new ForbiddenException('فروشگاه افزونه فقط برای خانگی است');
}
if (!(await this.isStoreEnabled(tenant.id))) {
throw new ForbiddenException('فروشگاه افزونه برای این نسخه خاموش است');
}
const originHost = normalizeTenantHost(originRaw || '');
if (originHost && scoreTenantHost(tenant, originHost) <= 0) {
throw new ForbiddenException('دامنه مبدأ با این نسخه هم‌خوان نیست');
}
return tenant;
}
async publicStore(domain?: string, origin?: string) {
const tenant = await this.resolvePublicTenant(domain, origin);
return this.buildRemoteStore(tenant.id);
}
async publicListRequests(domain?: string, origin?: string) {
const tenant = await this.resolvePublicTenant(domain, origin);
return {
requests: await this.buildRemoteRequestRows(tenant.id),
deposit: this.runtime.pluginStoreDeposit(),
};
}
async publicCreateRequest(
domain: string | undefined,
origin: string | undefined,
dto: CreatePurchaseRequestDto,
) {
const tenant = await this.resolvePublicTenant(domain, origin);
return this.remoteCreateRequest(tenant.id, tenant.product, dto);
}
async publicCancelRequest(
domain: string | undefined,
origin: string | undefined,
requestId: string,
) {
const tenant = await this.resolvePublicTenant(domain, origin);
return this.remoteCancelRequest(tenant.id, tenant.product, requestId);
}
async publicAttachReceipt(
domain: string | undefined,
origin: string | undefined,
requestId: string,
receiptUrl: string,
) {
const tenant = await this.resolvePublicTenant(domain, origin);
return this.remoteAttachReceipt(tenant.id, tenant.product, requestId, receiptUrl);
}
async remoteStore(tenantId: string, product: string) {
await this.assertRemoteKhanegi(tenantId, product);
try {
return await this.buildRemoteStore(tenantId);
} catch (error: any) {
if (error instanceof ForbiddenException) throw error;
this.logger.warn(`remoteStore failed: ${error?.message || error}`);
throw new ServiceUnavailableException(
error?.message || 'فروشگاه افزونه در سوپرادمین در دسترس نیست',
);
}
return this.buildRemoteStore(tenantId);
}
private isCatalogPriceEnabled(price: Pick<PluginCatalogPrice, 'enabled'> | { enabled?: unknown }) {
+7 -1
View File
@@ -4,13 +4,19 @@ import { NotificationsModule } from '../notifications/notifications.module';
import { ProgramSettingsModule } from '../program-settings/program-settings.module';
import { PluginsController } from './plugins.controller';
import { PluginStoreController } from './plugin-store.controller';
import { PublicPluginStoreController } from './public-plugin-store.controller';
import { RemotePluginsController } from './remote-plugins.controller';
import { PluginsService } from './plugins.service';
import { PluginStoreService } from './plugin-store.service';
@Module({
imports: [NotificationsModule, AuthModule, ProgramSettingsModule],
controllers: [PluginsController, PluginStoreController, RemotePluginsController],
controllers: [
PluginsController,
PluginStoreController,
PublicPluginStoreController,
RemotePluginsController,
],
providers: [PluginsService, PluginStoreService],
exports: [PluginsService, PluginStoreService],
})
@@ -0,0 +1,109 @@
import {
BadRequestException,
Body,
Controller,
Get,
Header,
Headers,
Param,
Post,
Query,
UploadedFile,
UseInterceptors,
} from '@nestjs/common';
import { ConfigService } from '@nestjs/config';
import { FileInterceptor } from '@nestjs/platform-express';
import { diskStorage } from 'multer';
import { extname } from 'path';
import {
IMAGE_MIME,
PLUGIN_RECEIPTS_DIR,
ensureUploadsDirs,
publicApiBase,
} from '../uploads/uploads.util';
import { CreatePurchaseRequestDto } from './dto/plugin-store.dto';
import { PluginStoreService } from './plugin-store.service';
@Controller('public/plugins')
export class PublicPluginStoreController {
constructor(
private readonly store: PluginStoreService,
private readonly config: ConfigService,
) {}
@Get('store')
@Header('Cache-Control', 'no-store')
storeFront(
@Query('domain') domain?: string,
@Headers('origin') origin?: string,
) {
return this.store.publicStore(domain, origin);
}
@Get('requests')
@Header('Cache-Control', 'no-store')
list(
@Query('domain') domain?: string,
@Headers('origin') origin?: string,
) {
return this.store.publicListRequests(domain, origin);
}
@Post('requests')
create(
@Query('domain') domain?: string,
@Headers('origin') origin?: string,
@Body() dto: CreatePurchaseRequestDto,
) {
return this.store.publicCreateRequest(domain, origin, dto);
}
@Post('requests/:id/cancel')
cancel(
@Param('id') id: string,
@Query('domain') domain?: string,
@Headers('origin') origin?: string,
) {
return this.store.publicCancelRequest(domain, origin, id);
}
@Post('requests/:id/receipt')
@UseInterceptors(
FileInterceptor('file', {
storage: diskStorage({
destination: (_req, _file, cb) => {
ensureUploadsDirs();
cb(null, PLUGIN_RECEIPTS_DIR);
},
filename: (_req, file, cb) => {
const safeExt = extname(file.originalname).toLowerCase() || '.png';
const name = `receipt_${Date.now()}_${Math.random().toString(36).slice(2, 8)}${safeExt}`;
cb(null, name);
},
}),
limits: { fileSize: 2 * 1024 * 1024 },
fileFilter: (_req, file, cb) => {
if (!IMAGE_MIME.test(file.mimetype)) {
cb(new BadRequestException('فقط تصویر (jpg/png/gif/webp/svg) مجاز است') as any, false);
return;
}
cb(null, true);
},
}),
)
receipt(
@Param('id') id: string,
@Query('domain') domain?: string,
@Headers('origin') origin?: string,
@UploadedFile() file: Express.Multer.File,
) {
if (!file) throw new BadRequestException('فایل ارسال نشده است');
const base = publicApiBase(this.config);
return this.store.publicAttachReceipt(
domain,
origin,
id,
`${base}/uploads/plugin-receipts/${file.filename}`,
);
}
}