Files
tessera-ctl/apps/web/src/lib/auth-actions.ts
T
schalli 46a6e277b8 fix(03): login redirect + API internal URL for Docker networking
- Use window.location.href for full page reload after login (ensures auth state)
- Add API_INTERNAL_URL for server-side requests within Docker network
- Remove unnecessary credentials:'include' from SSR fetch calls
- Update planning state for Phase 03 progress

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-20 09:28:05 +02:00

128 lines
3.2 KiB
TypeScript

'use server';
import { cookies } from 'next/headers';
import { redirect } from 'next/navigation';
const API_URL = process.env.API_INTERNAL_URL || process.env.NEXT_PUBLIC_API_URL || 'http://localhost:3001';
export interface AuthUser {
id: string;
username: string;
displayName: string | null;
role: 'SUPER_ADMIN' | 'ADMIN' | 'USER';
tenantId: string;
mustChangePassword: boolean;
}
export interface LoginResult {
success: boolean;
error?: string;
user?: AuthUser;
}
/**
* Login action: POST credentials to API, forward session cookie.
* In development, the API runs on a different port (3001) so we
* must manually forward the Set-Cookie header from the API response.
*/
export async function login(formData: FormData): Promise<LoginResult> {
const username = formData.get('username') as string;
const password = formData.get('password') as string;
const rememberMe = formData.get('rememberMe') === 'on';
if (!username || !password) {
return { success: false, error: 'invalidCredentials' };
}
try {
const response = await fetch(`${API_URL}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password }),
});
if (!response.ok) {
return { success: false, error: 'invalidCredentials' };
}
const user: AuthUser = await response.json();
// Forward the session cookie from the API response to the browser
const setCookieHeader = response.headers.get('set-cookie');
if (setCookieHeader) {
const sessionMatch = setCookieHeader.match(/session=([^;]+)/);
if (sessionMatch) {
const cookieStore = await cookies();
cookieStore.set('session', sessionMatch[1], {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
...(rememberMe
? { maxAge: 30 * 24 * 60 * 60 }
: {}),
path: '/',
});
}
}
return { success: true, user };
} catch {
return { success: false, error: 'networkError' };
}
}
/**
* Logout action: POST to API, clear local cookie, redirect to /login.
*/
export async function logout(): Promise<void> {
const cookieStore = await cookies();
const session = cookieStore.get('session')?.value;
try {
await fetch(`${API_URL}/auth/logout`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
...(session ? { Cookie: `session=${session}` } : {}),
},
credentials: 'include',
});
} catch {
// Logout should still clear the cookie even if API call fails
}
cookieStore.delete('session');
redirect('/login');
}
/**
* Fetch the current authenticated user from the API.
* Uses the session cookie for authentication.
*/
export async function fetchCurrentUser(): Promise<AuthUser | null> {
const cookieStore = await cookies();
const session = cookieStore.get('session')?.value;
if (!session) {
return null;
}
try {
const response = await fetch(`${API_URL}/auth/me`, {
headers: {
Cookie: `session=${session}`,
},
credentials: 'include',
cache: 'no-store',
});
if (!response.ok) {
return null;
}
return await response.json();
} catch {
return null;
}
}