refactor(web): DRY platform-sync.ts — remove 88 lines of hand-rolled auth, delegate to auth-client
This commit is contained in:
parent
8a5a40676a
commit
f80602d2b1
@ -54,11 +54,9 @@ export interface OfflineQueueItem {
|
|||||||
enqueuedAt: number;
|
enqueuedAt: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── API Client ────────────────────────────────────────────────
|
// ── API Client (delegated to @bytelyst/auth-client) ──────────
|
||||||
|
|
||||||
const STORAGE_KEYS = {
|
const STORAGE_KEYS = {
|
||||||
authToken: 'chronomind-auth-token',
|
|
||||||
refreshToken: 'chronomind-refresh-token',
|
|
||||||
lastSync: 'chronomind-platform-last-sync',
|
lastSync: 'chronomind-platform-last-sync',
|
||||||
offlineQueue: 'chronomind-offline-queue',
|
offlineQueue: 'chronomind-offline-queue',
|
||||||
syncEnabled: 'chronomind-platform-sync-enabled',
|
syncEnabled: 'chronomind-platform-sync-enabled',
|
||||||
@ -66,77 +64,9 @@ const STORAGE_KEYS = {
|
|||||||
|
|
||||||
export const PRODUCT_ID = _PRODUCT_ID;
|
export const PRODUCT_ID = _PRODUCT_ID;
|
||||||
|
|
||||||
function getBaseUrl(): string {
|
export class SyncConflictError extends Error {
|
||||||
if (typeof window !== 'undefined' && (window as unknown as Record<string, unknown>).__PLATFORM_URL__) {
|
constructor(public serverData: unknown) {
|
||||||
return (window as unknown as Record<string, unknown>).__PLATFORM_URL__ as string;
|
super('Sync conflict — server has newer version');
|
||||||
}
|
|
||||||
return process.env.NEXT_PUBLIC_PLATFORM_SERVICE_URL ?? 'https://api.chronomind.app';
|
|
||||||
}
|
|
||||||
|
|
||||||
function getAuthToken(): string | null {
|
|
||||||
if (typeof window === 'undefined') return null;
|
|
||||||
return localStorage.getItem(STORAGE_KEYS.authToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getRefreshToken(): string | null {
|
|
||||||
if (typeof window === 'undefined') return null;
|
|
||||||
return localStorage.getItem(STORAGE_KEYS.refreshToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
export function setRefreshToken(token: string | null): void {
|
|
||||||
if (typeof window === 'undefined') return;
|
|
||||||
if (token) {
|
|
||||||
localStorage.setItem(STORAGE_KEYS.refreshToken, token);
|
|
||||||
} else {
|
|
||||||
localStorage.removeItem(STORAGE_KEYS.refreshToken);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let _refreshPromise: Promise<boolean> | null = null;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attempt to refresh the access token using the stored refresh token.
|
|
||||||
* Returns true if refresh succeeded, false otherwise.
|
|
||||||
* Deduplicates concurrent refresh attempts.
|
|
||||||
*/
|
|
||||||
export async function refreshAccessToken(): Promise<boolean> {
|
|
||||||
if (_refreshPromise) return _refreshPromise;
|
|
||||||
|
|
||||||
_refreshPromise = (async () => {
|
|
||||||
const rt = getRefreshToken();
|
|
||||||
if (!rt) return false;
|
|
||||||
|
|
||||||
try {
|
|
||||||
const res = await fetch(`${getBaseUrl()}/auth/refresh`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
'x-product-id': PRODUCT_ID,
|
|
||||||
'x-request-id': crypto.randomUUID(),
|
|
||||||
},
|
|
||||||
body: JSON.stringify({ refreshToken: rt }),
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!res.ok) {
|
|
||||||
// Refresh token is invalid/expired — clear both tokens
|
|
||||||
setAuthToken(null);
|
|
||||||
setRefreshToken(null);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
const data = await res.json() as { accessToken: string; refreshToken: string };
|
|
||||||
setAuthToken(data.accessToken);
|
|
||||||
setRefreshToken(data.refreshToken);
|
|
||||||
return true;
|
|
||||||
} catch {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
})();
|
|
||||||
|
|
||||||
try {
|
|
||||||
return await _refreshPromise;
|
|
||||||
} finally {
|
|
||||||
_refreshPromise = null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -145,7 +75,9 @@ async function apiRequest<T>(
|
|||||||
method: string,
|
method: string,
|
||||||
body?: unknown
|
body?: unknown
|
||||||
): Promise<T> {
|
): Promise<T> {
|
||||||
const token = getAuthToken();
|
const client = getAuthClient();
|
||||||
|
const token = client.getAccessToken();
|
||||||
|
const baseUrl = process.env.NEXT_PUBLIC_PLATFORM_SERVICE_URL ?? 'https://api.chronomind.app';
|
||||||
const headers: Record<string, string> = {
|
const headers: Record<string, string> = {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
'x-request-id': crypto.randomUUID(),
|
'x-request-id': crypto.randomUUID(),
|
||||||
@ -153,7 +85,7 @@ async function apiRequest<T>(
|
|||||||
};
|
};
|
||||||
if (token) headers['Authorization'] = `Bearer ${token}`;
|
if (token) headers['Authorization'] = `Bearer ${token}`;
|
||||||
|
|
||||||
const res = await fetch(`${getBaseUrl()}${path}`, {
|
const res = await fetch(`${baseUrl}${path}`, {
|
||||||
method,
|
method,
|
||||||
headers,
|
headers,
|
||||||
body: body ? JSON.stringify(body) : undefined,
|
body: body ? JSON.stringify(body) : undefined,
|
||||||
@ -166,7 +98,7 @@ async function apiRequest<T>(
|
|||||||
|
|
||||||
// On 401, attempt a silent token refresh and retry once
|
// On 401, attempt a silent token refresh and retry once
|
||||||
if (res.status === 401 && !path.startsWith('/auth/')) {
|
if (res.status === 401 && !path.startsWith('/auth/')) {
|
||||||
const refreshed = await refreshAccessToken();
|
const refreshed = await client.refreshAccessToken();
|
||||||
if (refreshed) {
|
if (refreshed) {
|
||||||
return apiRequest<T>(path, method, body);
|
return apiRequest<T>(path, method, body);
|
||||||
}
|
}
|
||||||
@ -180,26 +112,19 @@ async function apiRequest<T>(
|
|||||||
return res.json();
|
return res.json();
|
||||||
}
|
}
|
||||||
|
|
||||||
export class SyncConflictError extends Error {
|
|
||||||
constructor(public serverData: unknown) {
|
|
||||||
super('Sync conflict — server has newer version');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── Public API ────────────────────────────────────────────────
|
// ── Public API ────────────────────────────────────────────────
|
||||||
|
|
||||||
export function setAuthToken(token: string | null): void {
|
export function setAuthToken(token: string | null): void {
|
||||||
if (typeof window === 'undefined') return;
|
const client = getAuthClient();
|
||||||
if (token) {
|
if (token) {
|
||||||
localStorage.setItem(STORAGE_KEYS.authToken, token);
|
client.setTokens(token, client.getRefreshToken() ?? '');
|
||||||
} else {
|
} else {
|
||||||
localStorage.removeItem(STORAGE_KEYS.authToken);
|
client.clearTokens();
|
||||||
localStorage.removeItem(STORAGE_KEYS.refreshToken);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isAuthenticated(): boolean {
|
export function isAuthenticated(): boolean {
|
||||||
return getAuthToken() !== null;
|
return getAuthClient().isAuthenticated();
|
||||||
}
|
}
|
||||||
|
|
||||||
export function isSyncEnabled(): boolean {
|
export function isSyncEnabled(): boolean {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user