add login with sessian and cleanup
All checks were successful
Build and Push Docker Images / build (push) Successful in 2m34s

This commit is contained in:
Rene Kievits
2025-10-22 02:07:56 +02:00
parent 673d29b05f
commit 1980e14e88
31 changed files with 830 additions and 68 deletions

27
client/.dockerignore Normal file
View File

@@ -0,0 +1,27 @@
# Node modules
node_modules
**/node_modules
# Git
.git
.gitignore
# Logs
*.log
# Local dev / editor
.vscode
.idea
# Build output
dist
build
.vite
# TypeScript build info
*.tsbuildinfo
# Environment files (if you want them injected via Docker ENV)
.env
.env.local
.env.*.local

View File

@@ -1,5 +1,10 @@
<template lang="pug">
v-container.fill-height.d-flex.justify-center.align-center
v-container.fill-height.d-flex.justify-center.align-center.position-relative
v-btn.position-absolute.top-0.right-0.ma-4(
color="error"
variant="outlined"
@click="logoutHandler"
) Logout
v-card.pa-8.pb-6(
elevation="12"
rounded="xl"
@@ -134,12 +139,14 @@ v-container.fill-height.d-flex.justify-center.align-center
<script lang="ts" setup>
import { onMounted, ref } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '../stores/auth'
import { Reviews } from '../composables/subject.ts'
const reviews = ref<Reviews | null>()
const router = useRouter()
const auth = useAuthStore()
const apiKey = ref<string>('')
const direction = ref<'en->jp' | 'jp->en' | 'both'>('jp->en')
const stats = ref<{ kanjiCount: number, vocabCount: number }>({
@@ -153,6 +160,11 @@ const options = ref({
kanji: false,
})
async function logoutHandler() {
await auth.logout()
router.push('/login')
}
function _startTraining(type: 'kanji' | 'vocab' | 'all' | 'writing') {
router.push({
path: '/' + type,

View File

@@ -0,0 +1,56 @@
<template lang="pug">
v-container.fill-height.d-flex.justify-center.align-center
v-card.elevation-12.rounded-lg.pa-8(style="max-width: 400px; width: 100%;")
v-card-title.text-h4.text-center.font-weight-bold.mb-6 Login
v-card-text
v-form
v-text-field(
label="Username"
prepend-inner-icon="mdi-account"
v-model="username"
outlined
dense
required
)
v-text-field(
label="Password"
prepend-inner-icon="mdi-lock"
type="password"
v-model="password"
outlined
dense
required
)
v-checkbox(
label="Remember me"
class="my-4"
)
v-btn(
color="primary"
large
block
@click="loginHandler"
) Login
v-card-actions.justify-center
span.text-caption Don't have an account?
v-btn.text.small( color="primary") Sign Up
</template>
<script lang="ts" setup>
import { ref } from 'vue'
import { useRouter } from 'vue-router'
import { useAuthStore } from '../stores/auth'
const router = useRouter()
const auth = useAuthStore()
const username = ref('')
const password = ref('')
async function loginHandler() {
const success = await auth.login(username.value, password.value)
if (success) router.push('/')
else console.error(auth.error)
}
</script>

View File

@@ -8,6 +8,7 @@
import { createRouter, createWebHistory } from 'vue-router'
import { setupLayouts } from 'virtual:generated-layouts'
import { routes } from 'vue-router/auto-routes'
import { useAuthStore } from '../stores/auth.ts'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
@@ -33,4 +34,19 @@ router.isReady().then(() => {
localStorage.removeItem('vuetify:dynamic-reload')
})
router.beforeEach(async (to) => {
const auth = useAuthStore()
if (!auth.user && !auth.loading) {
await auth.fetchUser()
}
if (to.path === '/login') return true
if (!auth.isAuthenticated) {
return { path: '/login' }
}
return true
})
export default router

109
client/src/stores/auth.ts Normal file
View File

@@ -0,0 +1,109 @@
import { defineStore } from 'pinia'
import { ref } from 'vue'
interface User {
id: number
username: string
email?: string
}
export const useAuthStore = defineStore('auth', () => {
const user = ref<User | null>(null)
const loading = ref(false)
const error = ref<string | null>(null)
async function fetchUser() {
try {
loading.value = true
const res = await fetch('/api/v1/user/info', {
method: 'GET',
credentials: 'include',
})
if (res.ok) {
const data = await res.json()
user.value = data.user
error.value = null
} else if (res.status === 401) {
const refreshed = await refreshToken()
if (refreshed) return await fetchUser()
user.value = null
} else {
throw new Error('Failed to fetch user')
}
} catch (err: any) {
error.value = err.message
user.value = null
} finally {
loading.value = false
}
}
async function login(username: string, password: string) {
try {
loading.value = true
error.value = null
const res = await fetch('/api/v1/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
credentials: 'include',
body: JSON.stringify({ username, password }),
})
if (!res.ok) throw new Error('Invalid credentials')
const data = await res.json()
if (data.ok && data.user) {
user.value = data.user
return true
}
throw new Error('Login failed')
} catch (err: any) {
error.value = err.message
return false
} finally {
loading.value = false
}
}
async function refreshToken() {
if (!document.cookie.includes('refresh_token')) return false
try {
const res = await fetch('/api/v1/auth/refresh', {
method: 'POST',
credentials: 'include',
})
if (res.ok) {
console.info('Token refreshed')
return true
}
console.warn('Token refresh failed')
return false
} catch {
return false
}
}
async function logout() {
try {
await fetch('/api/v1/auth/logout', {
method: 'POST',
credentials: 'include',
})
} catch (err) {
console.warn('Logout error:', err)
} finally {
user.value = null
}
}
const isAuthenticated = computed(() => !!user.value)
return {
user,
loading,
error,
isAuthenticated,
login,
logout,
fetchUser,
refreshToken,
}
})

View File

@@ -21,6 +21,7 @@ declare module 'vue-router/auto-routes' {
'/': RouteRecordInfo<'/', '/', Record<never, never>, Record<never, never>>,
'/all': RouteRecordInfo<'/all', '/all', Record<never, never>, Record<never, never>>,
'/kanji': RouteRecordInfo<'/kanji', '/kanji', Record<never, never>, Record<never, never>>,
'/login': RouteRecordInfo<'/login', '/login', Record<never, never>, Record<never, never>>,
'/vocab': RouteRecordInfo<'/vocab', '/vocab', Record<never, never>, Record<never, never>>,
'/writing': RouteRecordInfo<'/writing', '/writing', Record<never, never>, Record<never, never>>,
}
@@ -48,6 +49,10 @@ declare module 'vue-router/auto-routes' {
routes: '/kanji'
views: never
}
'src/pages/login.vue': {
routes: '/login'
views: never
}
'src/pages/vocab.vue': {
routes: '/vocab'
views: never