252 lines
6.9 KiB
TypeScript
252 lines
6.9 KiB
TypeScript
/**
|
|
* API ثبتنام و تأیید موبایل — Identity Service
|
|
*/
|
|
|
|
import type { TokenBundle } from "@/lib/auth";
|
|
|
|
const IDENTITY_API =
|
|
typeof window !== "undefined"
|
|
? "/api/identity"
|
|
: process.env.IDENTITY_SERVICE_URL ||
|
|
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),
|
|
}),
|
|
};
|