--- /dev/null
+<script setup lang="ts">
+ import { ref } from "vue"
+ import { useI18n } from "vue-i18n"
+
+ const { t } = useI18n()
+
+ import Notification from "../components/Notification.vue"
+
+ // Error string shown to the user in case something went wrong
+ const error = ref<string | null>(null)
+
+ // Credentials
+ const username = ref<string>("")
+ const password = ref<string>("")
+
+ async function login() {
+ // Reset the error
+ error.value = null
+
+ try {
+ const response = await fetch("/api/v1/auth/user", {
+ method : "POST",
+
+ // Headers
+ headers : {
+ "Content-Type" : "application/x-www-form-urlencoded",
+ },
+
+ // Body
+ body : new URLSearchParams({
+ username: username.value,
+ password: password.value,
+ }),
+ })
+
+ if (!response.ok) {
+ throw new Error(t('Invalid username or password'))
+ }
+
+ const data = await response.json() as { access_token: string }
+
+ localStorage.setItem('token', data.access_token)
+ alert(t('Login successful!'))
+ // TODO: router.push('/dashboard') or similar
+
+ // Catch any errors
+ } catch (err: unknown) {
+ error.value = err instanceof Error ? err.message : String(err)
+ }
+ }
+</script>
+
+<template>
+ <Section>
+ <Container>
+ <div class="columns is-centered">
+ <div class="column is-one-third">
+ <h1 class="title is-1">{{ t("Sign In") }}</h1>
+
+ <Notification v-if="error">
+ {{ error }}
+ </Notification>
+
+ <form @submit.prevent="login">
+ <div class="field">
+ <p class="control has-icons-left">
+ <input class="input" type="text" v-model="username"
+ :placeholder="t('Username')" required>
+
+ <span class="icon is-small is-left">
+ <i class="fas fa-user"></i>
+ </span>
+ </p>
+ </div>
+
+ <div class="field">
+ <p class="control has-icons-left">
+ <input class="input" type="password" v-model="password"
+ :placeholder="t('Password')" required autocomplete="off">
+
+ <span class="icon is-small is-left">
+ <i class="fas fa-lock"></i>
+ </span>
+ </p>
+ </div>
+
+ <div class="field">
+ <p class="control">
+ <button class="button is-primary is-fullwidth">
+ {{ t("Sign In") }}
+ </button>
+ </p>
+ </div>
+ </form>
+ </div>
+ </div>
+ </Container>
+ </Section>
+</template>