mirror of
https://github.com/Floriansylvain/snippets-manager-front.git
synced 2026-08-19 11:43:16 +02:00
feat: auth w.i.p.
This commit is contained in:
@@ -0,0 +1 @@
|
||||
VITE_API=http://localhost:9999/v1
|
||||
@@ -28,3 +28,6 @@ coverage
|
||||
*.sw?
|
||||
|
||||
*.tsbuildinfo
|
||||
|
||||
# Environment variables
|
||||
.env
|
||||
|
||||
+16
-1
@@ -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 +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 |
@@ -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>
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
@@ -0,0 +1,5 @@
|
||||
export function useApi() {
|
||||
const url = import.meta.env.VITE_API as string
|
||||
|
||||
return { url }
|
||||
}
|
||||
@@ -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 }
|
||||
}
|
||||
@@ -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')
|
||||
}
|
||||
]
|
||||
})
|
||||
|
||||
@@ -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
@@ -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 }
|
||||
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user