Add production-ready SuperAdmin API features for tenants, SMS, servers, and plugins.

Includes mobile auth, get_user/get_version, SMS wallet parity endpoints, appearance backfill, and related Prisma migrations.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-07-30 18:19:53 +03:30
co-authored by Cursor
parent ddf36f8470
commit a6b3457779
54 changed files with 3770 additions and 156 deletions
+19
View File
@@ -0,0 +1,19 @@
import { Controller, Get, Headers, Query } from '@nestjs/common';
import { GetVersionService } from './get-version.service';
@Controller()
export class GetVersionController {
constructor(private readonly getVersionService: GetVersionService) {}
@Get('get_version')
getVersion(
@Headers('host') host?: string,
@Headers('x-forwarded-host') forwardedHost?: string,
@Query('domain') domain?: string,
) {
return this.getVersionService.resolveByHost(
domain || forwardedHost || host,
domain,
);
}
}
+9
View File
@@ -0,0 +1,9 @@
import { Module } from '@nestjs/common';
import { GetVersionController } from './get-version.controller';
import { GetVersionService } from './get-version.service';
@Module({
controllers: [GetVersionController],
providers: [GetVersionService],
})
export class GetVersionModule {}
+71
View File
@@ -0,0 +1,71 @@
import { Injectable, NotFoundException } from '@nestjs/common';
import { PrismaService } from '../prisma/prisma.service';
@Injectable()
export class GetVersionService {
constructor(private readonly prisma: PrismaService) {}
async resolveByHost(hostHeader?: string, domainQuery?: string) {
const host = this.normalizeHost(domainQuery || hostHeader || '');
if (!host) {
throw new NotFoundException('domain required');
}
const tenants = await this.prisma.tenant.findMany({
where: { isActive: true },
select: {
appVersion: true,
domain: true,
domains: true,
},
});
const tenant = tenants.find((item) => this.matchesHost(item, host));
if (!tenant) {
throw new NotFoundException('tenant not found for domain');
}
return { version: tenant.appVersion || '1' };
}
private matchesHost(
tenant: {
domain: string;
domains: unknown;
},
host: string,
) {
const candidates = new Set<string>();
for (const value of [tenant.domain, ...this.readDomains(tenant.domains)]) {
const normalized = this.normalizeHost(value);
if (!normalized) continue;
candidates.add(normalized);
candidates.add(normalized.replace(/^www\./, ''));
candidates.add(`www.${normalized.replace(/^www\./, '')}`);
}
return candidates.has(host) || candidates.has(host.replace(/^www\./, ''));
}
private readDomains(raw: unknown): string[] {
if (!raw) return [];
if (Array.isArray(raw)) return raw.map(String);
if (typeof raw === 'string') {
try {
const parsed = JSON.parse(raw);
return Array.isArray(parsed) ? parsed.map(String) : [];
} catch {
return [];
}
}
return [];
}
private normalizeHost(value: string) {
return value
.trim()
.toLowerCase()
.replace(/^https?:\/\//, '')
.split('/')[0]
.split(':')[0];
}
}