190 lines
5.3 KiB
TypeScript
190 lines
5.3 KiB
TypeScript
import { secureSessionStorage, clearMobileSessionStorage, MOBILE_SESSION_STORAGE_KEY } from '@/lib/secureSessionStorage';
|
|
import { createMobilePlatformSdk, mobileRuntime } from '@/lib/runtime';
|
|
import { createRequestId } from '../../shared/request-id.js';
|
|
|
|
export interface PlatformAuthUser {
|
|
id: string;
|
|
email?: string;
|
|
role?: string;
|
|
plan?: string;
|
|
displayName?: string;
|
|
}
|
|
|
|
export interface StoredPlatformSession {
|
|
accessToken: string;
|
|
refreshToken: string;
|
|
user: PlatformAuthUser;
|
|
}
|
|
|
|
class PlatformAuthError extends Error {
|
|
status?: number;
|
|
|
|
constructor(message: string, status?: number) {
|
|
super(message);
|
|
this.name = 'PlatformAuthError';
|
|
this.status = status;
|
|
}
|
|
}
|
|
|
|
function createPlatformAuthSdk(accessToken: string | null = null) {
|
|
return createMobilePlatformSdk(() => accessToken);
|
|
}
|
|
|
|
function normalizeUser(value: any): PlatformAuthUser {
|
|
return {
|
|
id: String(value?.id || value?.sub || '').trim(),
|
|
email: typeof value?.email === 'string' ? value.email : undefined,
|
|
role: typeof value?.role === 'string' ? value.role : undefined,
|
|
plan: typeof value?.plan === 'string' ? value.plan : undefined,
|
|
displayName: typeof value?.displayName === 'string' ? value.displayName : undefined,
|
|
};
|
|
}
|
|
|
|
async function platformRequest<T>(
|
|
path: string,
|
|
options?: {
|
|
method?: string;
|
|
accessToken?: string;
|
|
body?: Record<string, unknown>;
|
|
}
|
|
): Promise<T> {
|
|
const sdk = createPlatformAuthSdk(options?.accessToken ?? null);
|
|
const response = await sdk.fetch(path, {
|
|
method: options?.method || 'GET',
|
|
headers: {
|
|
'x-request-id': createRequestId('mobile-auth')
|
|
},
|
|
body: options?.body ? JSON.stringify(options.body) : undefined,
|
|
});
|
|
|
|
const payload = await response.json().catch(() => ({}));
|
|
if (!response.ok) {
|
|
throw new PlatformAuthError(
|
|
String((payload as { message?: string; error?: string }).message || (payload as { error?: string }).error || `HTTP ${response.status}`),
|
|
response.status
|
|
);
|
|
}
|
|
|
|
return payload as T;
|
|
}
|
|
|
|
function isStoredSession(value: unknown): value is StoredPlatformSession {
|
|
const candidate = value as StoredPlatformSession | null;
|
|
return Boolean(
|
|
candidate
|
|
&& typeof candidate.accessToken === 'string'
|
|
&& typeof candidate.refreshToken === 'string'
|
|
&& candidate.user
|
|
&& typeof candidate.user.id === 'string'
|
|
);
|
|
}
|
|
|
|
async function writeSession(session: StoredPlatformSession): Promise<void> {
|
|
await secureSessionStorage.setItem(MOBILE_SESSION_STORAGE_KEY, JSON.stringify(session));
|
|
}
|
|
|
|
export async function persistPlatformSession(session: StoredPlatformSession): Promise<void> {
|
|
await writeSession(session);
|
|
}
|
|
|
|
export async function readPlatformSession(): Promise<StoredPlatformSession | null> {
|
|
const raw = await secureSessionStorage.getItem(MOBILE_SESSION_STORAGE_KEY);
|
|
if (!raw) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
const parsed = JSON.parse(raw) as unknown;
|
|
return isStoredSession(parsed) ? parsed : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export async function clearPlatformSession(): Promise<void> {
|
|
await clearMobileSessionStorage();
|
|
}
|
|
|
|
export async function getCurrentPlatformUser(accessToken: string): Promise<PlatformAuthUser> {
|
|
const me = await platformRequest<any>('/auth/me', {
|
|
accessToken,
|
|
});
|
|
return normalizeUser(me);
|
|
}
|
|
|
|
export async function refreshPlatformSession(refreshToken: string): Promise<StoredPlatformSession> {
|
|
const refreshed = await platformRequest<{ accessToken: string; refreshToken: string }>('/auth/refresh', {
|
|
method: 'POST',
|
|
body: { refreshToken },
|
|
});
|
|
const user = await getCurrentPlatformUser(refreshed.accessToken);
|
|
const session: StoredPlatformSession = {
|
|
accessToken: refreshed.accessToken,
|
|
refreshToken: refreshed.refreshToken,
|
|
user,
|
|
};
|
|
await writeSession(session);
|
|
return session;
|
|
}
|
|
|
|
export async function syncPlatformSessionFromTokens(
|
|
accessToken: string,
|
|
refreshToken: string
|
|
): Promise<StoredPlatformSession> {
|
|
const user = await getCurrentPlatformUser(accessToken);
|
|
const session: StoredPlatformSession = {
|
|
accessToken,
|
|
refreshToken,
|
|
user,
|
|
};
|
|
await writeSession(session);
|
|
return session;
|
|
}
|
|
|
|
export async function restorePlatformSession(): Promise<StoredPlatformSession | null> {
|
|
const stored = await readPlatformSession();
|
|
if (!stored) {
|
|
return null;
|
|
}
|
|
|
|
try {
|
|
const user = await getCurrentPlatformUser(stored.accessToken);
|
|
const session = { ...stored, user };
|
|
await writeSession(session);
|
|
return session;
|
|
} catch (error) {
|
|
if ((error as PlatformAuthError)?.status === 401 || (error as PlatformAuthError)?.status === 403) {
|
|
try {
|
|
return await refreshPlatformSession(stored.refreshToken);
|
|
} catch {
|
|
await clearPlatformSession();
|
|
return null;
|
|
}
|
|
}
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
export async function loginPlatformSession(email: string, password: string): Promise<StoredPlatformSession> {
|
|
const response = await platformRequest<{
|
|
accessToken: string;
|
|
refreshToken: string;
|
|
user: PlatformAuthUser;
|
|
}>('/auth/login', {
|
|
method: 'POST',
|
|
body: {
|
|
email,
|
|
password,
|
|
productId: mobileRuntime.productId,
|
|
},
|
|
});
|
|
|
|
const session: StoredPlatformSession = {
|
|
accessToken: response.accessToken,
|
|
refreshToken: response.refreshToken,
|
|
user: normalizeUser(response.user),
|
|
};
|
|
await writeSession(session);
|
|
return session;
|
|
}
|