feat: auth w.i.p.

This commit is contained in:
Florian Sylvain
2024-10-09 22:44:28 +02:00
parent 7afbe2b4c3
commit 87e894ab50
16 changed files with 247 additions and 43 deletions
+1
View File
@@ -0,0 +1 @@
VITE_API=http://localhost:9999/v1
+3
View File
@@ -28,3 +28,6 @@ coverage
*.sw?
*.tsbuildinfo
# Environment variables
.env
+16 -1
View File
@@ -1,14 +1,29 @@
<script setup lang="ts">
import { RouterView } from 'vue-router'
import { RouterView, useRouter } from 'vue-router'
import { useSettingsStore } from './stores/settings'
import FooterBar from './components/FooterBar.vue'
import { defineAsyncComponent } from 'vue'
const settingsStore = useSettingsStore()
const router = useRouter()
const excludedNavBarRoutes = ['/login', '/register']
const NavBarLoaded = defineAsyncComponent(() => import('./components/NavBar.vue'))
</script>
<template>
<VThemeProvider :theme="settingsStore.darkModeEnabled ? 'dark' : 'light'" with-background>
<VApp>
<VLayout>
<NavBarLoaded v-if="!excludedNavBarRoutes.includes(router.currentRoute.value.path)" />
<VMain>
<section class="pa-4 d-flex flex-column ga-4 w-100 h-100">
<RouterView />
</section>
</VMain>
<FooterBar app />
</VLayout>
</VApp>
</VThemeProvider>
</template>
-1
View File
@@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

Before

Width:  |  Height:  |  Size: 276 B

+45
View File
@@ -0,0 +1,45 @@
<script setup lang="ts">
interface Category {
categories: {
id: number
name: string
user_id: number
}[]
pagination: {
skip: number
take: number
}
links: {
next: string
prev: string
}
total: number
}
</script>
<template>
<VDataTable
:headers="[
{ 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' }
]"
>
<template
v-slot:[`item.actions`]="{
// item
}"
>
<VBtn icon="mdi-pencil" variant="flat" size="small"></VBtn>
<VBtn icon="mdi-delete" variant="flat" size="small"></VBtn>
</template>
</VDataTable>
</template>
+10
View File
@@ -0,0 +1,10 @@
<script setup lang="ts">
import ThemeSwitch from './ThemeSwitch.vue'
</script>
<template>
<VFooter class="d-flex align-center justify-space-between">
<div>{{ new Date().getFullYear() }} <strong>SnippetsManager</strong></div>
<ThemeSwitch />
</VFooter>
</template>
+31 -21
View File
@@ -1,51 +1,61 @@
<script setup lang="ts">
import { useSettingsStore } from '@/stores/settings'
import { useRouter } from 'vue-router'
const router = useRouter()
const settingsStore = useSettingsStore()
const navItems = [
{
name: 'Dashboard',
active: router.currentRoute.value.path === '/'
icon: 'mdi-view-dashboard',
path: '/'
},
{
name: 'Categories',
active: router.currentRoute.value.path === '/categories'
},
{
name: 'snippets',
active: router.currentRoute.value.path === '/snippets'
icon: 'mdi-shape-plus',
path: '/categories'
}
// {
// name: 'Snippets',
// icon: 'mdi-code-braces-box',
// path: '/snippets'
// },
// {
// name: 'Account',
// icon: 'mdi-account-box',
// path: '/account'
// }
]
</script>
<template>
<VNavigationDrawer :width="300" persistent>
<VListItem title="SnippetsManager" subtitle="by Florian Sylvain" class="py-2"></VListItem>
<VDivider></VDivider>
<VNavigationDrawer :width="300" persistent permanent>
<VListItem title="SnippetsManager" subtitle="by Florian Sylvain" class="py-2" />
<VDivider />
<VList class="pa-0">
<VListItem
v-for="item of navItems"
:key="item.name"
:title="item.name"
:active="item.active"
:active="(() => router.currentRoute.value.path === item.path)()"
:prepend-icon="item.icon"
:to="item.path"
class="ma-2"
color="primary"
rounded
link
></VListItem>
/>
</VList>
<template v-slot:append>
<div class="d-flex align-center ga-2 pa-2">
<VBtn variant="tonal" class="flex-grow-1" color="error"> Logout </VBtn>
<VBtn
<VListItem
prepend-icon="mdi-logout"
to="/login"
class="ma-2"
color="error"
variant="plain"
icon="mdi-theme-light-dark"
:onclick="settingsStore.switchDarkMode"
></VBtn>
</div>
active
rounded
>
Disconnect
</VListItem>
</template>
</VNavigationDrawer>
</template>
+15
View File
@@ -0,0 +1,15 @@
<script setup lang="ts">
import { useSettingsStore } from '@/stores/settings'
const settingsStore = useSettingsStore()
</script>
<template>
<div class="d-flex align-center ga-2 pa-2">
<VBtn
variant="plain"
icon="mdi-theme-light-dark"
:onclick="settingsStore.switchDarkMode"
></VBtn>
</div>
</template>
+5
View File
@@ -0,0 +1,5 @@
export function useApi() {
const url = import.meta.env.VITE_API as string
return { url }
}
+21
View File
@@ -0,0 +1,21 @@
export function useCookie() {
const getCookie = (name: string): string | null => {
const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'))
return match ? match[2] : null
}
const setCookie = (
name: string,
value: string,
days: number = 365,
httpOnly: boolean = false
) => {
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}`
}
return { getCookie, setCookie }
}
+10
View File
@@ -8,6 +8,16 @@ const router = createRouter({
path: '/',
name: 'home',
component: HomeView
},
{
path: '/categories',
name: 'categories',
component: () => import('../views/CategoriesView.vue')
},
{
path: '/login',
name: 'login',
component: () => import('../views/LoginView.vue')
}
]
})
+21
View File
@@ -0,0 +1,21 @@
import { useApi } from '@/composables/api'
import { defineStore } from 'pinia'
interface LoginFields {
email: string
password: string
}
export const useAuthStore = defineStore('auth', async () => {
const api = useApi()
const postLogin = async (credentials: LoginFields) => {
const loginPromise = await fetch(`${api.url}/session/login`, {
method: 'POST',
body: JSON.stringify(credentials)
})
return await loginPromise.json()
}
return { postLogin }
})
+5 -14
View File
@@ -1,31 +1,22 @@
import { ref } from 'vue'
import { defineStore } from 'pinia'
import { useCookie } from '@/composables/cookies'
export const useSettingsStore = defineStore('settings', () => {
const getCookie = (name: string): string | null => {
const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'))
return match ? match[2] : null
}
const cookie = useCookie()
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()
document.cookie = `${name}=${value}; ${expires}; path=/`
}
const darkModeCookie = getCookie('darkModeEnabled')
const darkModeCookie = cookie.getCookie('darkModeEnabled')
const darkModeEnabled = ref(
darkModeCookie !== null
? darkModeCookie === 'true'
: window.matchMedia('(prefers-color-scheme: dark)').matches
)
if (darkModeCookie === null) setCookie('darkModeEnabled', darkModeEnabled.value.toString())
if (darkModeCookie === null) cookie.setCookie('darkModeEnabled', darkModeEnabled.value.toString())
function switchDarkMode() {
darkModeEnabled.value = !darkModeEnabled.value
setCookie('darkModeEnabled', darkModeEnabled.value.toString())
cookie.setCookie('darkModeEnabled', darkModeEnabled.value.toString())
}
return { darkModeEnabled, switchDarkMode }
+11
View File
@@ -0,0 +1,11 @@
<script setup lang="ts">
import CategoriesTable from '@/components/CategoriesTable.vue'
</script>
<template>
<header class="d-flex align-center justify-space-between">
<h1 class="text-h3">Categories</h1>
<VBtn prepend-icon="mdi-plus" variant="flat" color="primary">Create category</VBtn>
</header>
<CategoriesTable />
</template>
+2 -4
View File
@@ -1,7 +1,5 @@
<script setup lang="ts">
import NavBar from '@/components/NavBar.vue'
</script>
<script setup lang="ts"></script>
<template>
<NavBar></NavBar>
<h1 class="text-h3">Dashboard</h1>
</template>
+49
View File
@@ -0,0 +1,49 @@
<script setup lang="ts">
import { ref } from 'vue'
const valid = ref()
const email = ref()
const emailRules = ref()
const password = ref()
const passwordRules = ref()
</script>
<template>
<section class="d-flex w-100 h-100 justify-center align-center flex-column ga-16 containter">
<h1 class="text-h4 text-md-h3 text-lg-h2 text-xl-h2">SnippetsManager</h1>
<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">
<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
color="primary"
v-model="password"
:rules="passwordRules"
label="Password"
hide-details
required
type="password"
autocomplete
/>
<VBtn variant="plain" color="primary-darken-1" size="small" class="pa-0 align-self-start">
Reset password
</VBtn>
</div>
<section class="d-flex flex-column ga-2">
<VBtn variant="flat" color="primary">Log in</VBtn>
<VBtn variant="outlined" color="primary">Create account</VBtn>
</section>
</VForm>
</VCard>
</section>
</template>