fix super admin
This commit is contained in:
@@ -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` });
|
||||
|
||||
@@ -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 }) {
|
||||
|
||||
@@ -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}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user