2026-07-23 10:25:32 +03:30
|
|
|
import { Injectable, NotFoundException } from '@nestjs/common';
|
|
|
|
|
import { PrismaService } from '../prisma/prisma.service';
|
|
|
|
|
|
2026-07-30 18:19:53 +03:30
|
|
|
type UiSetting = { key: string; value: string };
|
|
|
|
|
|
2026-07-23 10:25:32 +03:30
|
|
|
@Injectable()
|
|
|
|
|
export class GetUserService {
|
|
|
|
|
constructor(private readonly prisma: PrismaService) {}
|
|
|
|
|
|
|
|
|
|
async resolveByHost(hostHeader?: string, domainQuery?: string) {
|
|
|
|
|
const host = this.normalizeHost(domainQuery || hostHeader || '');
|
|
|
|
|
if (!host) {
|
|
|
|
|
throw new NotFoundException('domain required');
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-23 19:41:21 +03:30
|
|
|
const tenants = await this.prisma.tenant.findMany({
|
|
|
|
|
where: { isActive: true },
|
2026-07-23 10:25:32 +03:30
|
|
|
});
|
|
|
|
|
|
2026-07-23 19:41:21 +03:30
|
|
|
const tenant = tenants.find((item) => this.matchesHost(item, host));
|
2026-07-23 10:25:32 +03:30
|
|
|
if (!tenant) {
|
|
|
|
|
throw new NotFoundException('tenant not found for domain');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const ui = (tenant.appUiConfig as Record<string, any> | null) || null;
|
2026-07-30 18:19:53 +03:30
|
|
|
const isHamkarUi = tenant.product === 'hamkar' && ui?.kind === 'hamkar';
|
|
|
|
|
const hamkarBrand = isHamkarUi ? ui?.brand : null;
|
|
|
|
|
const khanegiColors = ui?.app?.theme?.colors;
|
|
|
|
|
const uiSettings =
|
|
|
|
|
isHamkarUi && ui?.settingsManaged === true
|
|
|
|
|
? this.buildHamkarUiSettings(ui)
|
|
|
|
|
: [];
|
|
|
|
|
|
|
|
|
|
// Preserve named CSS modes from live registry (e.g. limangold / Montpar).
|
|
|
|
|
// Fallback to notes marker if JSON field missing on older rows.
|
|
|
|
|
const noteMode = tenant.notes?.match(/registryMode:([^\s|]+)/)?.[1] || null
|
|
|
|
|
const registryMode =
|
|
|
|
|
(typeof ui?.registryMode === 'string' && ui.registryMode.trim()) ||
|
|
|
|
|
noteMode ||
|
|
|
|
|
null
|
|
|
|
|
const themeMode =
|
|
|
|
|
registryMode && registryMode !== 'dynamicMode'
|
|
|
|
|
? registryMode
|
|
|
|
|
: 'dynamicMode'
|
|
|
|
|
|
2026-08-09 12:58:45 +03:30
|
|
|
const isKhanegi = tenant.product === 'khanegi';
|
|
|
|
|
const uiVariant =
|
|
|
|
|
isKhanegi && tenant.uiVariant === 'newTheme' ? 'newTheme' : 'modular';
|
|
|
|
|
|
2026-07-30 18:19:53 +03:30
|
|
|
const color =
|
|
|
|
|
hamkarBrand?.primaryColor ||
|
2026-08-09 12:58:45 +03:30
|
|
|
(uiVariant === 'newTheme' ? khanegiColors?.primary : null) ||
|
2026-07-30 18:19:53 +03:30
|
|
|
tenant.primaryColor ||
|
|
|
|
|
null
|
|
|
|
|
const secondary =
|
|
|
|
|
hamkarBrand?.secondaryColor ||
|
2026-08-09 12:58:45 +03:30
|
|
|
(uiVariant === 'newTheme' ? khanegiColors?.secondary : null) ||
|
2026-07-30 18:19:53 +03:30
|
|
|
tenant.secondaryColor ||
|
|
|
|
|
null
|
|
|
|
|
const background =
|
|
|
|
|
hamkarBrand?.backgroundColor ||
|
2026-08-09 12:58:45 +03:30
|
|
|
(uiVariant === 'newTheme' ? khanegiColors?.background : null) ||
|
2026-07-30 18:19:53 +03:30
|
|
|
tenant.backgroundColor ||
|
|
|
|
|
null
|
2026-07-23 10:25:32 +03:30
|
|
|
|
|
|
|
|
const branding = {
|
|
|
|
|
id: tenant.id,
|
2026-07-23 19:41:21 +03:30
|
|
|
name: tenant.nameEn || tenant.slug,
|
2026-07-23 10:25:32 +03:30
|
|
|
name_fa: tenant.nameFa,
|
|
|
|
|
name_en: tenant.nameEn,
|
|
|
|
|
avatar: tenant.avatarUrl,
|
2026-07-30 18:19:53 +03:30
|
|
|
color,
|
|
|
|
|
secondary_color: secondary,
|
|
|
|
|
background_color: background,
|
2026-07-23 10:25:32 +03:30
|
|
|
node_server: tenant.nodeServer,
|
|
|
|
|
node_ws: tenant.nodeWs,
|
|
|
|
|
app_url: tenant.appUrl,
|
|
|
|
|
admin_url: tenant.adminUrl,
|
|
|
|
|
product: tenant.product,
|
2026-07-30 18:19:53 +03:30
|
|
|
theme: themeMode,
|
|
|
|
|
mode: themeMode,
|
2026-08-09 12:58:45 +03:30
|
|
|
/** Khanegi front-end family for consumers */
|
|
|
|
|
ui_variant: isKhanegi ? uiVariant : undefined,
|
2026-08-03 21:13:46 +03:30
|
|
|
app_ui_config:
|
2026-08-09 12:58:45 +03:30
|
|
|
isKhanegi && uiVariant === 'newTheme'
|
|
|
|
|
? this.stripKhanegiSaMeta(ui)
|
|
|
|
|
: undefined,
|
2026-07-30 18:19:53 +03:30
|
|
|
/** Laravel-compatible overlay; Hamkar merges only when present */
|
|
|
|
|
ui_settings: uiSettings.length ? uiSettings : undefined,
|
2026-07-23 10:25:32 +03:30
|
|
|
};
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
...branding,
|
|
|
|
|
user: { ...branding },
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2026-08-03 21:13:46 +03:30
|
|
|
private stripKhanegiSaMeta(ui: Record<string, any> | null) {
|
|
|
|
|
if (!ui || typeof ui !== 'object') return ui;
|
|
|
|
|
const { defaultSnapshot: _snap, _backfill, ...rest } = ui;
|
|
|
|
|
return rest;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-30 18:19:53 +03:30
|
|
|
private buildHamkarUiSettings(ui: Record<string, any>): UiSetting[] {
|
|
|
|
|
const settings = ui?.settings;
|
|
|
|
|
if (!settings || typeof settings !== 'object') return [];
|
|
|
|
|
|
|
|
|
|
const out: UiSetting[] = [];
|
|
|
|
|
for (const key of ['pricing_box_theme', 'price_font_size', 'orderUI'] as const) {
|
|
|
|
|
const value = settings[key];
|
|
|
|
|
if (value == null || value === '') continue;
|
|
|
|
|
out.push({ key, value: String(value) });
|
|
|
|
|
}
|
|
|
|
|
return out;
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-23 19:41:21 +03:30
|
|
|
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 [];
|
|
|
|
|
}
|
|
|
|
|
|
2026-07-23 10:25:32 +03:30
|
|
|
private normalizeHost(value: string) {
|
|
|
|
|
return value
|
|
|
|
|
.trim()
|
|
|
|
|
.toLowerCase()
|
|
|
|
|
.replace(/^https?:\/\//, '')
|
|
|
|
|
.split('/')[0]
|
|
|
|
|
.split(':')[0];
|
|
|
|
|
}
|
|
|
|
|
}
|