This commit is contained in:
2025-07-17 16:36:36 +02:00
parent a4cf1064d4
commit 8d707535fd
29 changed files with 751 additions and 26 deletions

View File

@@ -1,3 +1,5 @@
# this file is ignored when the app is built since we're using SSG
NUXT_PUBLIC_API_BASE="http://127.0.0.1:8080/api"
NUXT_COOKIES_SECURE="false"
NUXT_COOKIES_SAME_SITE="strict"

View File

@@ -0,0 +1,20 @@
import type { AuthSession } from '~/types/auth';
const SAME_SITE_SETTINGS = ['strict', 'lax', 'none'] as const;
export function useAuthSession() {
const config = useRuntimeConfig().public;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const sameSite = SAME_SITE_SETTINGS.includes(config.cookiesSameSite as any)
? (config.cookiesSameSite as (typeof SAME_SITE_SETTINGS)[number])
: 'strict';
return useCookie('auth_session', {
default: () => null as AuthSession | null,
secure: config.cookiesSecure.toLowerCase() === 'true',
sameSite,
path: '/',
httpOnly: false,
});
}

View File

@@ -0,0 +1,42 @@
import { toast } from 'vue-sonner';
import type { ApiResponse } from '~/types/api';
export async function loginUser(
email: string,
password: string
): Promise<{ success: boolean }> {
const { data, error } = await useFetch<ApiResponse<{ token: string }>>(
getApiUrl('auth/login'),
{
method: 'POST',
headers: {
'content-type': 'application/json',
},
body: JSON.stringify({
email: email,
password: password,
}),
responseType: 'json',
}
);
if (data.value == null) {
toast.error('Login', {
description: error.value?.data ?? 'Failed to login',
});
return {
success: false,
};
}
useAuthSession().value = { type: 'WarrenAuth', id: data.value.data.token };
toast.success('Login', {
description: `Successfully logged in`,
});
return {
success: true,
};
}

15
frontend/lib/api/index.ts Normal file
View File

@@ -0,0 +1,15 @@
export function getApiHeaders(
includeAuth: boolean = true
): Record<string, string> {
const headers: Record<string, string> = {};
if (includeAuth) {
const authSession = useAuthSession().value;
if (authSession != null) {
headers['authorization'] = `${authSession.type} ${authSession.id}`;
}
}
return headers;
}

View File

@@ -2,12 +2,14 @@ import { toast } from 'vue-sonner';
import type { DirectoryEntry } from '~/types';
import type { ApiResponse } from '~/types/api';
import type { Warren } from '~/types/warrens';
import { getApiHeaders } from '.';
export async function getWarrens(): Promise<Record<string, Warren>> {
const { data, error } = await useFetch<ApiResponse<{ warrens: Warren[] }>>(
getApiUrl('warrens'),
{
method: 'GET',
headers: getApiHeaders(),
}
);

View File

@@ -58,6 +58,8 @@ export default defineNuxtConfig({
runtimeConfig: {
public: {
apiBase: '/api',
cookiesSecure: 'false',
cookiesSameSite: 'strict',
},
},
});

View File

@@ -7,6 +7,7 @@ import {
CardContent,
CardFooter,
} from '@/components/ui/card';
import { loginUser } from '~/lib/api/auth/login';
definePageMeta({
layout: 'auth',
@@ -14,6 +15,35 @@ definePageMeta({
// TODO: Get this from the backend
const OPEN_ID = false;
const loggingIn = ref(false);
const email = ref('');
const password = ref('');
const inputValid = computed(
() => email.value.trim().length > 0 && password.value.trim().length > 0
);
async function submit() {
if (!inputValid.value) {
return;
}
loggingIn.value = true;
const { success } = await loginUser(email.value, password.value);
if (success) {
await navigateTo({ path: '/' });
}
loggingIn.value = false;
}
function onKeyDown(e: KeyboardEvent) {
if (e.key === 'Enter') {
submit();
}
}
</script>
<template>
@@ -29,24 +59,30 @@ const OPEN_ID = false;
<Label for="email">Email</Label>
<Input
id="email"
v-model="email"
type="email"
placeholder="your@email.com"
autocomplete="off"
required
@keydown="onKeyDown"
/>
</div>
<div class="grid gap-2">
<Label for="password">Password</Label>
<Input
id="password"
v-model="password"
type="password"
autocomplete="off"
required
@keydown="onKeyDown"
/>
</div>
</CardContent>
<CardFooter class="flex-col gap-2">
<Button class="w-full">Sign in</Button>
<Button class="w-full" :disabled="!inputValid" @click="submit"
>Sign in</Button
>
<Button class="w-full" variant="outline" :disabled="!OPEN_ID"
>OpenID Connect</Button
>

5
frontend/types/auth.ts Normal file
View File

@@ -0,0 +1,5 @@
export type AuthSessionType = 'WarrenAuth';
export interface AuthSession {
type: AuthSessionType;
id: string;
}