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 { 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 />
|
||||
|
||||
@@ -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`]="{
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 }
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
@@ -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 }
|
||||
})
|
||||
|
||||
@@ -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">
|
||||
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"
|
||||
variant="outlined"
|
||||
color="primary"
|
||||
hide-details="auto"
|
||||
required
|
||||
/>
|
||||
<div class="d-flex flex-column ga-1">
|
||||
<div class="d-flex flex-column">
|
||||
<VTextField
|
||||
color="primary"
|
||||
v-model="password"
|
||||
:rules="passwordRules"
|
||||
label="Password"
|
||||
hide-details
|
||||
required
|
||||
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>
|
||||
|
||||
Reference in New Issue
Block a user