feat: auth flow, categories list t0

This commit is contained in:
Florian Sylvain
2024-10-10 21:24:58 +02:00
parent f8a75e9cdd
commit 81bc46ebfa
9 changed files with 139 additions and 47 deletions
+10 -3
View File
@@ -2,21 +2,28 @@
import { RouterView, useRouter } from 'vue-router' import { RouterView, useRouter } from 'vue-router'
import { useSettingsStore } from './stores/settings' import { useSettingsStore } from './stores/settings'
import FooterBar from './components/FooterBar.vue' import FooterBar from './components/FooterBar.vue'
import { defineAsyncComponent } from 'vue' import NavBar from './components/NavBar.vue'
import { onMounted, ref } from 'vue'
const settingsStore = useSettingsStore() const settingsStore = useSettingsStore()
const router = useRouter() const router = useRouter()
const excludedNavBarRoutes = ['/login', '/register'] const excludedNavBarRoutes = ['/login', '/register']
const NavBarLoaded = defineAsyncComponent(() => import('./components/NavBar.vue')) const isRouterReady = ref(false)
onMounted(() => {
router.isReady().then(() => (isRouterReady.value = true))
})
</script> </script>
<template> <template>
<VThemeProvider :theme="settingsStore.darkModeEnabled ? 'dark' : 'light'" with-background> <VThemeProvider :theme="settingsStore.darkModeEnabled ? 'dark' : 'light'" with-background>
<VApp> <VApp>
<VLayout> <VLayout>
<NavBarLoaded v-if="!excludedNavBarRoutes.includes(router.currentRoute.value.path)" /> <NavBar
v-if="isRouterReady && !excludedNavBarRoutes.includes(router?.currentRoute.value.path)"
/>
<VMain> <VMain>
<section class="pa-4 d-flex flex-column ga-4 w-100 h-100"> <section class="pa-4 d-flex flex-column ga-4 w-100 h-100">
<RouterView /> <RouterView />
+12 -9
View File
@@ -1,4 +1,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { useCategoriesStore } from '@/stores/categories'
import { onMounted, ref, type Ref } from 'vue'
interface Category { interface Category {
categories: { categories: {
id: number id: number
@@ -15,6 +18,14 @@ interface Category {
} }
total: number total: number
} }
const categoriesStore = useCategoriesStore()
const fetchedCategories: Ref<Category | undefined> = ref()
onMounted(async () => {
const categoriesPromise = await categoriesStore.getCategories()
fetchedCategories.value = await categoriesPromise.json()
})
</script> </script>
<template> <template>
@@ -23,15 +34,7 @@ interface Category {
{ title: 'Name', key: 'name', sortable: true }, { title: 'Name', key: 'name', sortable: true },
{ title: 'Actions', key: 'actions', align: 'end', sortable: false } { title: 'Actions', key: 'actions', align: 'end', sortable: false }
]" ]"
:items="[ :items="fetchedCategories?.categories"
{ name: 'pouet' },
{ name: 'pouet' },
{ name: 'pouet' },
{ name: 'pouet' },
{ name: 'pouet' },
{ name: 'pouet' },
{ name: 'pouet' }
]"
> >
<template <template
v-slot:[`item.actions`]="{ v-slot:[`item.actions`]="{
+11 -1
View File
@@ -1,7 +1,11 @@
<script setup lang="ts"> <script setup lang="ts">
import { useCookie } from '@/composables/cookies'
import { useAuthStore } from '@/stores/auth'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
const router = useRouter() const router = useRouter()
const authStore = useAuthStore()
const cookie = useCookie()
const navItems = [ const navItems = [
{ {
@@ -25,6 +29,12 @@ const navItems = [
// path: '/account' // path: '/account'
// } // }
] ]
function disconnect(): void {
authStore.postLogout()
cookie.setCookie('loggedIn', '', 0)
router.push('/login')
}
</script> </script>
<template> <template>
@@ -47,12 +57,12 @@ const navItems = [
<template v-slot:append> <template v-slot:append>
<VListItem <VListItem
prepend-icon="mdi-logout" prepend-icon="mdi-logout"
to="/login"
class="ma-2" class="ma-2"
color="error" color="error"
variant="plain" variant="plain"
active active
rounded rounded
@click="disconnect"
> >
Disconnect Disconnect
</VListItem> </VListItem>
+5 -1
View File
@@ -1,5 +1,9 @@
export function useApi() { export function useApi() {
const url = import.meta.env.VITE_API as string const url = import.meta.env.VITE_API as string
const options = {
headers: { 'Content-Type': 'application/json' },
credentials: 'include' as RequestCredentials
}
return { url } return { url, options }
} }
+2 -8
View File
@@ -4,17 +4,11 @@ export function useCookie() {
return match ? match[2] : null return match ? match[2] : null
} }
const setCookie = ( const setCookie = (name: string, value: string, days: number = 365) => {
name: string,
value: string,
days: number = 365,
httpOnly: boolean = false
) => {
const date = new Date() const date = new Date()
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000) date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000)
const expires = 'expires=' + date.toUTCString() const expires = 'expires=' + date.toUTCString()
const httpOnlyFlag = httpOnly ? '; HttpOnly; Secure' : '' document.cookie = `${name}=${value}; ${expires}; path=/`
document.cookie = `${name}=${value}; ${expires}; path=/${httpOnlyFlag}`
} }
return { getCookie, setCookie } return { getCookie, setCookie }
+28 -4
View File
@@ -1,5 +1,26 @@
import { createRouter, createWebHashHistory } from 'vue-router' import { createRouter, createWebHashHistory, useRouter } from 'vue-router'
import HomeView from '../views/HomeView.vue' import HomeView from '../views/HomeView.vue'
import { useCookie } from '@/composables/cookies'
function isLoggedIn(): boolean {
const cookie = useCookie()
const loggedIn = cookie.getCookie('loggedIn')
if (!loggedIn) return false
if (new Date(loggedIn) < new Date()) return false
return true
}
function privateRoutesGuard(): void {
const router = useRouter()
if (isLoggedIn()) return
router.push('/login')
}
function loginRouteGuard(): void {
const router = useRouter()
if (!isLoggedIn()) return
router.push('/')
}
const router = createRouter({ const router = createRouter({
history: createWebHashHistory(import.meta.env.BASE_URL), history: createWebHashHistory(import.meta.env.BASE_URL),
@@ -7,17 +28,20 @@ const router = createRouter({
{ {
path: '/', path: '/',
name: 'home', name: 'home',
component: HomeView component: HomeView,
beforeEnter: privateRoutesGuard
}, },
{ {
path: '/categories', path: '/categories',
name: 'categories', name: 'categories',
component: () => import('../views/CategoriesView.vue') component: () => import('../views/CategoriesView.vue'),
beforeEnter: privateRoutesGuard
}, },
{ {
path: '/login', path: '/login',
name: 'login', name: 'login',
component: () => import('../views/LoginView.vue') component: () => import('../views/LoginView.vue'),
beforeEnter: loginRouteGuard
} }
] ]
}) })
+18 -5
View File
@@ -1,4 +1,5 @@
import { useApi } from '@/composables/api' import { useApi } from '@/composables/api'
import { useCookie } from '@/composables/cookies'
import { defineStore } from 'pinia' import { defineStore } from 'pinia'
interface LoginFields { interface LoginFields {
@@ -6,16 +7,28 @@ interface LoginFields {
password: string password: string
} }
export const useAuthStore = defineStore('auth', async () => { export const useAuthStore = defineStore('auth', () => {
const api = useApi() const api = useApi()
const cookie = useCookie()
const postLogin = async (credentials: LoginFields) => { const postLogin = async (credentials: LoginFields): Promise<Response> => {
const loginPromise = await fetch(`${api.url}/session/login`, { const loginPromise = await fetch(`${api.url}/session/login`, {
headers: { 'Content-Type': 'application/json' },
method: 'POST', method: 'POST',
body: JSON.stringify(credentials) body: JSON.stringify(credentials),
credentials: 'include'
}) })
return await loginPromise.json() cookie.setCookie('loggedIn', new Date(new Date().getTime() + 7200000).toString())
return loginPromise
} }
return { postLogin } const postLogout = async (): Promise<Response> => {
return await fetch(`${api.url}/session/logout`, {
headers: { 'Content-Type': 'application/json' },
method: 'POST',
credentials: 'include'
})
}
return { postLogin, postLogout }
}) })
+12
View File
@@ -0,0 +1,12 @@
import { useApi } from '@/composables/api'
import { defineStore } from 'pinia'
export const useCategoriesStore = defineStore('categories', () => {
const api = useApi()
const getCategories = async () => {
return await fetch(`${api.url}/category`, api.options)
}
return { getCategories }
})
+43 -18
View File
@@ -1,11 +1,32 @@
<script setup lang="ts"> <script setup lang="ts">
import { ref } from 'vue' import { useAuthStore } from '@/stores/auth'
import { ref, type Ref } from 'vue'
import { useRouter } from 'vue-router'
const valid = ref() const router = useRouter()
const email = ref() const authStore = useAuthStore()
const emailRules = ref()
const password = ref() const valid: Ref<boolean> = ref(false)
const passwordRules = ref() const email: Ref<string> = ref('')
const emailRules: Ref<Array<(v: string) => boolean | string>> = ref([
(value) => {
if (/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value)) return true
return 'E-mail must respect standard e-mails format.'
}
])
const password: Ref<string> = ref('')
const errorMessage: Ref<string> = ref('')
async function onSubmit(): Promise<void> {
if (!valid.value) return
const loginPromise = await authStore.postLogin({ email: email.value, password: password.value })
if (!loginPromise.ok) {
errorMessage.value = 'Wrong e-mail and/or password.'
return
}
router.push('/')
}
</script> </script>
<template> <template>
@@ -14,33 +35,37 @@ const passwordRules = ref()
<VCard max-width="350" class="w-100 pa-2" variant="flat"> <VCard max-width="350" class="w-100 pa-2" variant="flat">
<VCardTitle>Login</VCardTitle> <VCardTitle>Login</VCardTitle>
<VCardSubtitle>Enter your credentials to log in.</VCardSubtitle> <VCardSubtitle>Enter your credentials to log in.</VCardSubtitle>
<VForm v-model="valid" class="pa-4 d-flex flex-column ga-4"> <VForm v-model="valid" class="pa-4 d-flex flex-column ga-2" @submit.prevent="onSubmit">
<VTextField <VTextField
color="primary"
v-model="email" v-model="email"
:rules="emailRules" :rules="emailRules"
label="E-mail" label="E-mail"
hide-details
required
type="email" type="email"
/> variant="outlined"
<div class="d-flex flex-column ga-1">
<VTextField
color="primary" color="primary"
v-model="password" hide-details="auto"
:rules="passwordRules"
label="Password"
hide-details
required required
/>
<div class="d-flex flex-column">
<VTextField
v-model="password"
label="Password"
type="password" type="password"
variant="outlined"
color="primary"
hide-details="auto"
required
autocomplete autocomplete
/> />
<VBtn variant="plain" color="primary-darken-1" size="small" class="pa-0 align-self-start"> <VBtn variant="plain" color="primary-darken-1" size="small" class="pa-0 align-self-start">
Reset password Reset password
</VBtn> </VBtn>
</div> </div>
<p v-if="errorMessage !== ''" class="text-body-1" style="color: rgb(var(--v-theme-error))">
{{ errorMessage }}
</p>
<section class="d-flex flex-column ga-2"> <section class="d-flex flex-column ga-2">
<VBtn variant="flat" color="primary">Log in</VBtn> <VBtn variant="flat" color="primary" type="submit" :disabled="!valid">Log in</VBtn>
<VBtn variant="outlined" color="primary">Create account</VBtn> <VBtn variant="outlined" color="primary">Create account</VBtn>
</section> </section>
</VForm> </VForm>