Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7693a02c98 | ||
|
|
528a1622d0 |
@@ -61,6 +61,19 @@ export function readSessionToken(cookieHeader: string | null | undefined) {
|
||||
return decodeURIComponent(sessionCookie.slice(sessionCookieName.length + 1));
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract bearer token from Authorization header.
|
||||
* Used as fallback when cookies are blocked (e.g. Android WebView third-party blocking).
|
||||
*/
|
||||
export function readBearerToken(headers?: { get(name: string): string | null }) {
|
||||
if (!headers) return null;
|
||||
const auth = headers.get('authorization');
|
||||
if (!auth) return null;
|
||||
const parts = auth.split(' ');
|
||||
if (parts.length !== 2 || parts[0] !== 'Bearer') return null;
|
||||
return parts[1];
|
||||
}
|
||||
|
||||
export async function createSession(userId: string) {
|
||||
const db = createDbClient();
|
||||
const token = randomBytes(32).toString('base64url');
|
||||
@@ -83,8 +96,12 @@ export async function deleteSession(token: string | null) {
|
||||
await db.delete(sessions).where(eq(sessions.tokenHash, hashToken(token)));
|
||||
}
|
||||
|
||||
export async function getCurrentUser(cookieHeader: string | null | undefined): Promise<CurrentUser | null> {
|
||||
const token = readSessionToken(cookieHeader);
|
||||
export async function getCurrentUser(
|
||||
cookieHeader: string | null | undefined,
|
||||
headers?: { get(name: string): string | null },
|
||||
): Promise<CurrentUser | null> {
|
||||
// Try cookie first, then Authorization header (for Android WebView where 3rd-party cookies are blocked)
|
||||
const token = readSessionToken(cookieHeader) ?? readBearerToken(headers);
|
||||
if (!token) return null;
|
||||
|
||||
const db = createDbClient();
|
||||
|
||||
@@ -32,7 +32,7 @@ function validatePassword(password: unknown) {
|
||||
|
||||
export const authRoutes = new Elysia({ prefix: '/api/v1/auth' })
|
||||
.get('/me', async ({ request }) => {
|
||||
const user = await getCurrentUser(request.headers.get('cookie'));
|
||||
const user = await getCurrentUser(request.headers.get('cookie'), request.headers);
|
||||
return {
|
||||
user,
|
||||
features: getAuthFeatures(),
|
||||
@@ -73,6 +73,7 @@ export const authRoutes = new Elysia({ prefix: '/api/v1/auth' })
|
||||
name: user.name,
|
||||
role: 'user' as const,
|
||||
},
|
||||
token,
|
||||
features: getAuthFeatures(),
|
||||
};
|
||||
} catch (error) {
|
||||
@@ -109,6 +110,7 @@ export const authRoutes = new Elysia({ prefix: '/api/v1/auth' })
|
||||
name: user.name,
|
||||
role: user.role === 'expert' ? 'expert' as const : 'user' as const,
|
||||
},
|
||||
token,
|
||||
features: getAuthFeatures(),
|
||||
};
|
||||
} catch (error) {
|
||||
|
||||
@@ -13,7 +13,7 @@ export function AuthInitializer() {
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (query.data) {
|
||||
if (query.data?.user) {
|
||||
setUser(query.data.user);
|
||||
}
|
||||
}, [query.data, setUser]);
|
||||
|
||||
@@ -16,6 +16,28 @@ import { recordApiCall } from './telemetry';
|
||||
|
||||
const apiBaseUrl = import.meta.env.VITE_API_BASE_URL ?? 'https://zeavisedu.asepharyana.my.id';
|
||||
|
||||
const AUTH_TOKEN_KEY = 'zeavis_auth_token';
|
||||
|
||||
function getAuthToken(): string | null {
|
||||
try {
|
||||
return localStorage.getItem(AUTH_TOKEN_KEY);
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export function setAuthToken(token: string | null) {
|
||||
try {
|
||||
if (token) {
|
||||
localStorage.setItem(AUTH_TOKEN_KEY, token);
|
||||
} else {
|
||||
localStorage.removeItem(AUTH_TOKEN_KEY);
|
||||
}
|
||||
} catch {
|
||||
// localStorage may throw in private browsing
|
||||
}
|
||||
}
|
||||
|
||||
export interface ApiError extends Error {
|
||||
status: number;
|
||||
source?: 'uploader' | 'model-service' | 'unknown';
|
||||
@@ -24,10 +46,21 @@ export interface ApiError extends Error {
|
||||
async function fetchApi<T>(endpoint: string, options?: RequestInit): Promise<T> {
|
||||
const start = performance.now();
|
||||
const url = `${apiBaseUrl}${endpoint}`;
|
||||
const token = getAuthToken();
|
||||
const headers = new Headers(options?.headers);
|
||||
|
||||
if (token) {
|
||||
headers.set('Authorization', `Bearer ${token}`);
|
||||
}
|
||||
|
||||
if (options?.body && !options.method) {
|
||||
// auto-set Content-Type for JSON bodies
|
||||
}
|
||||
|
||||
const response = await fetch(url, {
|
||||
credentials: 'include',
|
||||
...options,
|
||||
headers: options?.headers,
|
||||
headers,
|
||||
});
|
||||
|
||||
const duration = performance.now() - start;
|
||||
@@ -93,23 +126,29 @@ export const apiClient = {
|
||||
},
|
||||
|
||||
async register(payload: RegisterRequest): Promise<AuthResponse> {
|
||||
return fetchApi('/api/v1/auth/register', {
|
||||
const result = await fetchApi<AuthResponse>('/api/v1/auth/register', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (result.token) setAuthToken(result.token);
|
||||
return result;
|
||||
},
|
||||
|
||||
async login(payload: AuthRequest): Promise<AuthResponse> {
|
||||
return fetchApi('/api/v1/auth/login', {
|
||||
const result = await fetchApi<AuthResponse>('/api/v1/auth/login', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify(payload),
|
||||
});
|
||||
if (result.token) setAuthToken(result.token);
|
||||
return result;
|
||||
},
|
||||
|
||||
async logout(): Promise<{ ok: boolean }> {
|
||||
return fetchApi('/api/v1/auth/logout', { method: 'POST' });
|
||||
const result = await fetchApi<{ ok: boolean }>('/api/v1/auth/logout', { method: 'POST' });
|
||||
setAuthToken(null);
|
||||
return result;
|
||||
},
|
||||
|
||||
// Disease catalog methods
|
||||
|
||||
@@ -66,6 +66,7 @@ export type RegisterRequest = AuthRequest & {
|
||||
export type AuthResponse = {
|
||||
user: AuthUser;
|
||||
features: AuthFeatures;
|
||||
token?: string;
|
||||
};
|
||||
|
||||
export type DiagnosisPrediction = {
|
||||
|
||||
Reference in New Issue
Block a user