add login with sessian and cleanup
All checks were successful
Build and Push Docker Images / build (push) Successful in 2m34s
All checks were successful
Build and Push Docker Images / build (push) Successful in 2m34s
This commit is contained in:
27
client/.dockerignore
Normal file
27
client/.dockerignore
Normal 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
|
||||
@@ -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,
|
||||
|
||||
56
client/src/pages/login.vue
Normal file
56
client/src/pages/login.vue
Normal 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>
|
||||
@@ -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
109
client/src/stores/auth.ts
Normal 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,
|
||||
}
|
||||
})
|
||||
5
client/src/typed-router.d.ts
vendored
5
client/src/typed-router.d.ts
vendored
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user