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
+40
View File
@@ -0,0 +1,40 @@
/** Shared Iran timezone helpers for SuperAdmin API. */
export const IRAN_TZ = 'Asia/Tehran';
/** Gregorian YYYY-MM-DD for the calendar day in Asia/Tehran. */
export function iranYmd(date: Date = new Date()): string {
return new Intl.DateTimeFormat('en-CA', {
timeZone: IRAN_TZ,
year: 'numeric',
month: '2-digit',
day: '2-digit',
}).format(date);
}
export function formatIranDate(date: Date): string {
return new Intl.DateTimeFormat('fa-IR-u-ca-persian', {
timeZone: IRAN_TZ,
year: 'numeric',
month: '2-digit',
day: '2-digit',
}).format(date);
}
export function formatIranDateTime(date: Date): string {
return new Intl.DateTimeFormat('fa-IR-u-ca-persian', {
timeZone: IRAN_TZ,
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
}).format(date);
}
/** Whole days from Iran "today" to Iran calendar day of `expiresAt`. */
export function iranDaysUntil(expiresAt: Date, now: Date = new Date()): number {
const start = Date.parse(`${iranYmd(now)}T12:00:00+03:30`);
const end = Date.parse(`${iranYmd(expiresAt)}T12:00:00+03:30`);
return Math.round((end - start) / 86_400_000);
}
+19
View File
@@ -0,0 +1,19 @@
/** Normalize Iranian mobile to 09xxxxxxxxx, or null if invalid. */
export function normalizeMobile(input?: string | null): string | null {
if (!input) return null;
let digits = String(input).replace(/\D/g, '');
if (digits.startsWith('98') && digits.length >= 12) {
digits = `0${digits.slice(2)}`;
}
if (digits.startsWith('9') && digits.length === 10) {
digits = `0${digits}`;
}
if (!/^09\d{9}$/.test(digits)) {
return null;
}
return digits;
}
export function displayName(firstName: string, lastName: string) {
return `${firstName} ${lastName}`.replace(/\s+/g, ' ').trim();
}