fix super admin
This commit is contained in:
@@ -56,6 +56,17 @@ describe('findTenantForHost', () => {
|
|||||||
expect(scoreTenantHost(mine, api)).toBe(75);
|
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', () => {
|
it('does not pick a tenant when only a shared nodeServer matches', () => {
|
||||||
const api = 'box.example.ir';
|
const api = 'box.example.ir';
|
||||||
const left = fromDomain('a.ir', { nodeServer: `https://${api}/server` });
|
const left = fromDomain('a.ir', { nodeServer: `https://${api}/server` });
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ import {
|
|||||||
NotFoundException,
|
NotFoundException,
|
||||||
OnModuleDestroy,
|
OnModuleDestroy,
|
||||||
OnModuleInit,
|
OnModuleInit,
|
||||||
ServiceUnavailableException,
|
|
||||||
} from '@nestjs/common';
|
} from '@nestjs/common';
|
||||||
import {
|
import {
|
||||||
PluginCatalogItem,
|
PluginCatalogItem,
|
||||||
@@ -33,6 +32,11 @@ import {
|
|||||||
iranDaysUntil,
|
iranDaysUntil,
|
||||||
} from '../common/iran-time.util';
|
} from '../common/iran-time.util';
|
||||||
import { rewriteLegacyUploadUrl } from '../uploads/uploads.util';
|
import { rewriteLegacyUploadUrl } from '../uploads/uploads.util';
|
||||||
|
import {
|
||||||
|
findTenantForHost,
|
||||||
|
normalizeTenantHost,
|
||||||
|
scoreTenantHost,
|
||||||
|
} from '../auth/tenant-host.util';
|
||||||
import {
|
import {
|
||||||
daysToPlanMonths,
|
daysToPlanMonths,
|
||||||
inferSettingKeyFromName,
|
inferSettingKeyFromName,
|
||||||
@@ -493,17 +497,90 @@ export class PluginStoreService implements OnModuleInit, OnModuleDestroy {
|
|||||||
return { ok: true };
|
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) {
|
async remoteStore(tenantId: string, product: string) {
|
||||||
await this.assertRemoteKhanegi(tenantId, product);
|
await this.assertRemoteKhanegi(tenantId, product);
|
||||||
try {
|
return this.buildRemoteStore(tenantId);
|
||||||
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 || 'فروشگاه افزونه در سوپرادمین در دسترس نیست',
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private isCatalogPriceEnabled(price: Pick<PluginCatalogPrice, 'enabled'> | { enabled?: unknown }) {
|
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 { ProgramSettingsModule } from '../program-settings/program-settings.module';
|
||||||
import { PluginsController } from './plugins.controller';
|
import { PluginsController } from './plugins.controller';
|
||||||
import { PluginStoreController } from './plugin-store.controller';
|
import { PluginStoreController } from './plugin-store.controller';
|
||||||
|
import { PublicPluginStoreController } from './public-plugin-store.controller';
|
||||||
import { RemotePluginsController } from './remote-plugins.controller';
|
import { RemotePluginsController } from './remote-plugins.controller';
|
||||||
import { PluginsService } from './plugins.service';
|
import { PluginsService } from './plugins.service';
|
||||||
import { PluginStoreService } from './plugin-store.service';
|
import { PluginStoreService } from './plugin-store.service';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
imports: [NotificationsModule, AuthModule, ProgramSettingsModule],
|
imports: [NotificationsModule, AuthModule, ProgramSettingsModule],
|
||||||
controllers: [PluginsController, PluginStoreController, RemotePluginsController],
|
controllers: [
|
||||||
|
PluginsController,
|
||||||
|
PluginStoreController,
|
||||||
|
PublicPluginStoreController,
|
||||||
|
RemotePluginsController,
|
||||||
|
],
|
||||||
providers: [PluginsService, PluginStoreService],
|
providers: [PluginsService, PluginStoreService],
|
||||||
exports: [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