diff --git a/apps/api/src/lib/auth.ts b/apps/api/src/lib/auth.ts index 8e90768..11acfd8 100644 --- a/apps/api/src/lib/auth.ts +++ b/apps/api/src/lib/auth.ts @@ -28,14 +28,26 @@ function hashToken(token: string) { return createHash('sha256').update(`${env.sessionSecret}:${token}`).digest('hex'); } -export function createSessionCookie(token: string) { +function isSecureRequest(headers?: { get(name: string): string | null }) { + if (env.secureCookies) return true; + // Detect HTTPS behind proxy (X-Forwarded-Proto) + const proto = headers?.get('x-forwarded-proto'); + if (proto === 'https') return true; + return false; +} + +function buildSameSite(headers?: { get(name: string): string | null }) { + return isSecureRequest(headers) ? 'SameSite=None; Secure' : 'SameSite=Lax'; +} + +export function createSessionCookie(token: string, headers?: { get(name: string): string | null }) { const maxAge = 60 * 60 * 24 * 30; - const sameSite = env.secureCookies ? 'SameSite=None; Secure' : 'SameSite=Lax'; + const sameSite = buildSameSite(headers); return `${sessionCookieName}=${token}; HttpOnly; Path=/; ${sameSite}; Max-Age=${maxAge}`; } -export function clearSessionCookie() { - const sameSite = env.secureCookies ? 'SameSite=None; Secure' : 'SameSite=Lax'; +export function clearSessionCookie(headers?: { get(name: string): string | null }) { + const sameSite = buildSameSite(headers); return `${sessionCookieName}=; HttpOnly; Path=/; ${sameSite}; Max-Age=0`; } diff --git a/apps/api/src/routes/auth.ts b/apps/api/src/routes/auth.ts index 770d1e5..cb60458 100644 --- a/apps/api/src/routes/auth.ts +++ b/apps/api/src/routes/auth.ts @@ -38,7 +38,7 @@ export const authRoutes = new Elysia({ prefix: '/api/v1/auth' }) features: getAuthFeatures(), }; }) - .post('/register', async ({ body, set }) => { + .post('/register', async ({ body, set, request }) => { const req = body as Partial | undefined; const email = normalizeEmail(req?.email); const name = normalizeName(req?.name); @@ -62,7 +62,7 @@ export const authRoutes = new Elysia({ prefix: '/api/v1/auth' }) const user = inserted[0]; const token = await createSession(user.id); - set.headers['Set-Cookie'] = createSessionCookie(token); + set.headers['Set-Cookie'] = createSessionCookie(token, request.headers); authCounter.labels('register', 'true').inc(); @@ -79,7 +79,7 @@ export const authRoutes = new Elysia({ prefix: '/api/v1/auth' }) return serviceUnavailable('Database unavailable'); } }) - .post('/login', async ({ body, set }) => { + .post('/login', async ({ body, set, request }) => { const req = body as Partial | undefined; const email = normalizeEmail(req?.email); @@ -98,7 +98,7 @@ export const authRoutes = new Elysia({ prefix: '/api/v1/auth' }) } const token = await createSession(user.id); - set.headers['Set-Cookie'] = createSessionCookie(token); + set.headers['Set-Cookie'] = createSessionCookie(token, request.headers); authCounter.labels('login', 'true').inc(); @@ -116,8 +116,9 @@ export const authRoutes = new Elysia({ prefix: '/api/v1/auth' }) } }) .post('/logout', async ({ request, set }) => { - await deleteSession(readSessionToken(request.headers.get('cookie'))); - set.headers['Set-Cookie'] = clearSessionCookie(); + const cookieHeader = request.headers.get('cookie'); + await deleteSession(readSessionToken(cookieHeader)); + set.headers['Set-Cookie'] = clearSessionCookie(request.headers); return { ok: true }; }) .get('/google', ({ set }) => { diff --git a/apps/web/src/components/auth-guard.tsx b/apps/web/src/components/auth-guard.tsx index ed4ac06..ed76d53 100644 --- a/apps/web/src/components/auth-guard.tsx +++ b/apps/web/src/components/auth-guard.tsx @@ -10,27 +10,32 @@ type AuthGuardProps = { }; export function AuthGuard({ children, requireExpert = false }: AuthGuardProps) { + const user = useAuthStore((state) => state.user); const setUser = useAuthStore((state) => state.setUser); const query = useQuery({ queryKey: ['auth', 'me'], queryFn: () => apiClient.getMe(), + staleTime: 30_000, }); useEffect(() => { - if (query.data) { + if (query.data?.user) { setUser(query.data.user); } }, [query.data, setUser]); - if (query.isLoading) { + // Tunjukkan loading hanya jika belum ada user di store + if (query.isLoading && !user) { return
Memeriksa sesi...
; } - if (!query.data?.user) { + // Cek store dulu, baru query — mencegah redirect saat refetch background + const currentUser = query.data?.user ?? user; + if (!currentUser) { return ; } - if (requireExpert && query.data.user.role !== 'expert') { + if (requireExpert && currentUser.role !== 'expert') { return ; } diff --git a/apps/web/src/components/auth-initializer.tsx b/apps/web/src/components/auth-initializer.tsx index 765408a..a5239ac 100644 --- a/apps/web/src/components/auth-initializer.tsx +++ b/apps/web/src/components/auth-initializer.tsx @@ -9,6 +9,7 @@ export function AuthInitializer() { queryKey: ['auth', 'me'], queryFn: () => apiClient.getMe(), retry: false, + staleTime: 30_000, }); useEffect(() => {