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 { useSettingsStore } from './stores/settings'
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 router = useRouter()
const excludedNavBarRoutes = ['/login', '/register']
const NavBarLoaded = defineAsyncComponent(() => import('./components/NavBar.vue'))
const isRouterReady = ref(false)
onMounted(() => {
router.isReady().then(() => (isRouterReady.value = true))
})
</script>
<template>
<VThemeProvider :theme="settingsStore.darkModeEnabled ? 'dark' : 'light'" with-background>
<VApp>
<VLayout>
<NavBarLoaded v-if="!excludedNavBarRoutes.includes(router.currentRoute.value.path)" />
<NavBar
v-if="isRouterReady && !excludedNavBarRoutes.includes(router?.currentRoute.value.path)"
/>
<VMain>
<section class="pa-4 d-flex flex-column ga-4 w-100 h-100">
<RouterView />
+12 -9
View File
@@ -1,4 +1,7 @@
<script setup lang="ts">
import { useCategoriesStore } from '@/stores/categories'
import { onMounted, ref, type Ref } from 'vue'
interface Category {
categories: {
id: number
@@ -15,6 +18,14 @@ interface Category {
}
total: number
}
const categoriesStore = useCategoriesStore()
const fetchedCategories: Ref<Category | undefined> = ref()
onMounted(async () => {
const categoriesPromise = await categoriesStore.getCategories()
fetchedCategories.value = await categoriesPromise.json()
})
</script>
<template>
@@ -23,15 +34,7 @@ interface Category {
{ title: 'Name', key: 'name', sortable: true },
{ title: 'Actions', key: 'actions', align: 'end', sortable: false }
]"
:items="[
{ name: 'pouet' },
{ name: 'pouet' },
{ name: 'pouet' },
{ name: 'pouet' },
{ name: 'pouet' },
{ name: 'pouet' },
{ name: 'pouet' }
]"
:items="fetchedCategories?.categories"
>
<template
v-slot:[`item.actions`]="{
+11 -1
View File
@@ -1,7 +1,11 @@
<script setup lang="ts">
import { useCookie } from '@/composables/cookies'
import { useAuthStore } from '@/stores/auth'
import { useRouter } from 'vue-router'
const router = useRouter()
const authStore = useAuthStore()
const cookie = useCookie()
const navItems = [
{
@@ -25,6 +29,12 @@ const navItems = [
// path: '/account'
// }
]
function disconnect(): void {
authStore.postLogout()
cookie.setCookie('loggedIn', '', 0)
router.push('/login')
}
</script>
<template>
@@ -47,12 +57,12 @@ const navItems = [
<template v-slot:append>
<VListItem
prepend-icon="mdi-logout"
to="/login"
class="ma-2"
color="error"
variant="plain"
active
rounded
@click="disconnect"
>
Disconnect
</VListItem>
+5 -1
View File
@@ -1,5 +1,9 @@
export function useApi() {
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
}
const setCookie = (
name: string,
value: string,
days: number = 365,
httpOnly: boolean = false
) => {
const setCookie = (name: string, value: string, days: number = 365) => {
const date = new Date()
date.setTime(date.getTime() + days * 24 * 60 * 60 * 1000)
const expires = 'expires=' + date.toUTCString()
const httpOnlyFlag = httpOnly ? '; HttpOnly; Secure' : ''
document.cookie = `${name}=${value}; ${expires}; path=/${httpOnlyFlag}`
document.cookie = `${name}=${value}; ${expires}; path=/`
}
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 { 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({
history: createWebHashHistory(import.meta.env.BASE_URL),
@@ -7,17 +28,20 @@ const router = createRouter({
{
path: '/',
name: 'home',
component: HomeView
component: HomeView,
beforeEnter: privateRoutesGuard
},
{
path: '/categories',
name: 'categories',
component: () => import('../views/CategoriesView.vue')
component: () => import('../views/CategoriesView.vue'),
beforeEnter: privateRoutesGuard
},
{
path: '/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 { useCookie } from '@/composables/cookies'
import { defineStore } from 'pinia'
interface LoginFields {
@@ -6,16 +7,28 @@ interface LoginFields {
password: string
}
export const useAuthStore = defineStore('auth', async () => {
export const useAuthStore = defineStore('auth', () => {
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`, {
headers: { 'Content-Type': 'application/json' },
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">
import { ref } from 'vue'
import { useAuthStore } from '@/stores/auth'
import { ref, type Ref } from 'vue'
import { useRouter } from 'vue-router'
const valid = ref()
const email = ref()
const emailRules = ref()
const password = ref()
const passwordRules = ref()
const router = useRouter()
const authStore = useAuthStore()
const valid: Ref<boolean> = ref(false)
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>
<template>
@@ -14,33 +35,37 @@ const passwordRules = ref()
<VCard max-width="350" class="w-100 pa-2" variant="flat">
<VCardTitle>Login</VCardTitle>
<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
color="primary"
v-model="email"
:rules="emailRules"
label="E-mail"
hide-details
required
type="email"
/>
<div class="d-flex flex-column ga-1">
<VTextField
variant="outlined"
color="primary"
v-model="password"
:rules="passwordRules"
label="Password"
hide-details
hide-details="auto"
required
/>
<div class="d-flex flex-column">
<VTextField
v-model="password"
label="Password"
type="password"
variant="outlined"
color="primary"
hide-details="auto"
required
autocomplete
/>
<VBtn variant="plain" color="primary-darken-1" size="small" class="pa-0 align-self-start">
Reset password
</VBtn>
</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">
<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>
</section>
</VForm>