TorbatYar/frontend/lib/identity.ts
Mortezakoohjani 800b0ba2c5 Deploy TorbatYar for torbatyar.ir with nginx multi-tenant routing.
Wire production domain, CORS for tenant subdomains, celery volume mounts, and nginx reverse proxy configs for apex, API, identity, auth, and wildcard tenants.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-21 21:43:33 +03:30

248 lines
6.8 KiB
TypeScript

/**
* API ثبت‌نام و تأیید موبایل — Identity Service
*/
import type { TokenBundle } from "@/lib/auth";
const IDENTITY_API =
process.env.NEXT_PUBLIC_IDENTITY_API_URL || "http://localhost:8001";
export interface OtpRequestResult {
message: string;
expires_in: number;
skipped?: boolean;
}
export interface MobileAuthStartResult extends OtpRequestResult {
intent: "login" | "register";
}
export interface AuthSessionResult extends TokenBundle {
message: string;
intent: "login" | "register";
user_id?: string;
mobile_verified: boolean;
}
export interface RegisterCompleteResult {
message: string;
mobile_verified: boolean;
user_id?: string;
}
export interface MeProfile {
user_id: string;
mobile: string | null;
mobile_verified: boolean;
requires_mobile_verification: boolean;
}
export interface TenantMembershipSummary {
tenant_id: string;
role: string;
is_active: boolean;
}
export interface AccountProfile {
user_id: string;
keycloak_sub: string;
email: string | null;
username: string | null;
display_name: string | null;
mobile: string | null;
mobile_verified: boolean;
requires_mobile_verification: boolean;
roles: string[];
tenant_memberships: TenantMembershipSummary[];
}
export interface AdminUser {
id: string;
keycloak_sub: string;
email: string;
username: string | null;
display_name: string | null;
mobile: string | null;
mobile_verified: boolean;
mobile_verified_at: string | null;
core_user_id: string | null;
status: string;
created_at: string;
}
export type MembershipRole = "tenant_admin" | "tenant_member";
export interface TenantMember {
id: string;
tenant_id: string;
user_id: string;
role: MembershipRole;
is_active: boolean;
joined_at: string;
}
function parseIdentityError(body: Record<string, unknown>, fallback: string): string {
const err = body?.error as { message?: string } | undefined;
if (err?.message) return err.message;
if (typeof body?.detail === "string") return body.detail;
if (Array.isArray(body?.detail) && body.detail[0]?.msg) {
return String(body.detail[0].msg);
}
return fallback;
}
async function identityRequest<T>(
path: string,
options: RequestInit & { token?: string | null } = {}
): Promise<T> {
const { token, headers: customHeaders, ...init } = options;
const headers = new Headers(customHeaders);
headers.set("Content-Type", "application/json");
if (token) headers.set("Authorization", `Bearer ${token}`);
let res: Response;
try {
res = await fetch(`${IDENTITY_API}${path}`, { ...init, headers });
} catch {
throw new Error(`اتصال به سرویس هویت برقرار نشد (${IDENTITY_API})`);
}
if (!res.ok) {
const body = (await res.json().catch(() => ({}))) as Record<string, unknown>;
throw new Error(parseIdentityError(body, res.statusText));
}
return res.json() as Promise<T>;
}
export const identityApi = {
mobileStart: (mobile: string, forceSms = false) =>
identityRequest<MobileAuthStartResult>("/api/v1/auth/mobile/start", {
method: "POST",
body: JSON.stringify({ mobile, force_sms: forceSms }),
}),
mobileComplete: (payload: {
mobile: string;
code: string;
display_name?: string;
email?: string;
username?: string;
password?: string;
}) =>
identityRequest<AuthSessionResult>("/api/v1/auth/mobile/complete", {
method: "POST",
body: JSON.stringify(payload),
}),
register: (data: {
email: string;
username: string;
password: string;
mobile: string;
display_name?: string;
}) =>
identityRequest<OtpRequestResult>("/api/v1/auth/register", {
method: "POST",
body: JSON.stringify(data),
}),
verifyRegistrationMobile: (mobile: string, code: string) =>
identityRequest<AuthSessionResult>("/api/v1/auth/register/verify-mobile", {
method: "POST",
body: JSON.stringify({ mobile, code }),
}),
registerMobileRequest: (mobile: string, forceSms = false) =>
identityRequest<OtpRequestResult>("/api/v1/auth/register/mobile", {
method: "POST",
body: JSON.stringify({ mobile, force_sms: forceSms }),
}),
registerMobileVerify: (mobile: string, code: string, display_name?: string) =>
identityRequest<AuthSessionResult>("/api/v1/auth/register/mobile/verify", {
method: "POST",
body: JSON.stringify({ mobile, code, display_name }),
}),
requestMobileVerify: (mobile: string, token: string) =>
identityRequest<OtpRequestResult>("/api/v1/auth/mobile/request", {
method: "POST",
token,
body: JSON.stringify({ mobile }),
}),
verifyMobile: (mobile: string, code: string, token: string) =>
identityRequest<RegisterCompleteResult>("/api/v1/auth/mobile/verify", {
method: "POST",
token,
body: JSON.stringify({ mobile, code }),
}),
me: (token: string) =>
identityRequest<MeProfile>("/api/v1/auth/me", { token }),
profile: (token: string) =>
identityRequest<AccountProfile>("/api/v1/auth/me", { token }),
updateProfile: (
token: string,
data: { display_name?: string | null; email?: string }
) =>
identityRequest<AccountProfile>("/api/v1/auth/me", {
method: "PATCH",
token,
body: JSON.stringify(data),
}),
changePassword: (
token: string,
data: { current_password?: string; new_password: string }
) =>
identityRequest<{ message: string }>("/api/v1/auth/password", {
method: "POST",
token,
body: JSON.stringify(data),
}),
listUsers: (token: string, options?: { offset?: number; limit?: number }) => {
const params = new URLSearchParams({
offset: String(options?.offset ?? 0),
limit: String(options?.limit ?? 50),
});
return identityRequest<AdminUser[]>(`/api/v1/users?${params.toString()}`, { token });
},
getUser: (token: string, userId: string) =>
identityRequest<AdminUser>(`/api/v1/users/${userId}`, { token }),
createUser: (
token: string,
data: {
email: string;
username: string;
password: string;
mobile: string;
display_name?: string;
}
) =>
identityRequest<AdminUser>("/api/v1/users", {
method: "POST",
token,
body: JSON.stringify(data),
}),
listMembers: (token: string, tenantId: string) =>
identityRequest<TenantMember[]>(`/api/v1/tenants/${tenantId}/members`, { token }),
addMember: (
token: string,
tenantId: string,
data: { user_id: string; role?: MembershipRole }
) =>
identityRequest<TenantMember>(`/api/v1/tenants/${tenantId}/members`, {
method: "POST",
token,
body: JSON.stringify(data),
}),
};