mirror of
https://github.com/Floriansylvain/snippets-manager-front.git
synced 2026-08-19 11:43:16 +02:00
feat: auth flow, categories list t0
This commit is contained in:
+10
-3
@@ -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 />
|
||||||
|
|||||||
@@ -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`]="{
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
@@ -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
@@ -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 }
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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 }
|
||||||
|
})
|
||||||
+41
-16
@@ -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"
|
||||||
|
color="primary"
|
||||||
|
hide-details="auto"
|
||||||
|
required
|
||||||
/>
|
/>
|
||||||
<div class="d-flex flex-column ga-1">
|
<div class="d-flex flex-column">
|
||||||
<VTextField
|
<VTextField
|
||||||
color="primary"
|
|
||||||
v-model="password"
|
v-model="password"
|
||||||
:rules="passwordRules"
|
|
||||||
label="Password"
|
label="Password"
|
||||||
hide-details
|
|
||||||
required
|
|
||||||
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>
|
||||||
|
|||||||
Reference in New Issue
Block a user