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?
|
*.sw?
|
||||||
|
|
||||||
*.tsbuildinfo
|
*.tsbuildinfo
|
||||||
|
|
||||||
|
# Environment variables
|
||||||
|
.env
|
||||||
|
|||||||
+16
-1
@@ -1,14 +1,29 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { RouterView } 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 { defineAsyncComponent } from 'vue'
|
||||||
|
|
||||||
const settingsStore = useSettingsStore()
|
const settingsStore = useSettingsStore()
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const excludedNavBarRoutes = ['/login', '/register']
|
||||||
|
|
||||||
|
const NavBarLoaded = defineAsyncComponent(() => import('./components/NavBar.vue'))
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<VThemeProvider :theme="settingsStore.darkModeEnabled ? 'dark' : 'light'" with-background>
|
<VThemeProvider :theme="settingsStore.darkModeEnabled ? 'dark' : 'light'" with-background>
|
||||||
<VApp>
|
<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 />
|
<RouterView />
|
||||||
|
</section>
|
||||||
|
</VMain>
|
||||||
|
<FooterBar app />
|
||||||
|
</VLayout>
|
||||||
</VApp>
|
</VApp>
|
||||||
</VThemeProvider>
|
</VThemeProvider>
|
||||||
</template>
|
</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">
|
<script setup lang="ts">
|
||||||
import { useSettingsStore } from '@/stores/settings'
|
|
||||||
import { useRouter } from 'vue-router'
|
import { useRouter } from 'vue-router'
|
||||||
|
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const settingsStore = useSettingsStore()
|
|
||||||
|
|
||||||
const navItems = [
|
const navItems = [
|
||||||
{
|
{
|
||||||
name: 'Dashboard',
|
name: 'Dashboard',
|
||||||
active: router.currentRoute.value.path === '/'
|
icon: 'mdi-view-dashboard',
|
||||||
|
path: '/'
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: 'Categories',
|
name: 'Categories',
|
||||||
active: router.currentRoute.value.path === '/categories'
|
icon: 'mdi-shape-plus',
|
||||||
},
|
path: '/categories'
|
||||||
{
|
|
||||||
name: 'snippets',
|
|
||||||
active: router.currentRoute.value.path === '/snippets'
|
|
||||||
}
|
}
|
||||||
|
// {
|
||||||
|
// name: 'Snippets',
|
||||||
|
// icon: 'mdi-code-braces-box',
|
||||||
|
// path: '/snippets'
|
||||||
|
// },
|
||||||
|
// {
|
||||||
|
// name: 'Account',
|
||||||
|
// icon: 'mdi-account-box',
|
||||||
|
// path: '/account'
|
||||||
|
// }
|
||||||
]
|
]
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<VNavigationDrawer :width="300" persistent>
|
<VNavigationDrawer :width="300" persistent permanent>
|
||||||
<VListItem title="SnippetsManager" subtitle="by Florian Sylvain" class="py-2"></VListItem>
|
<VListItem title="SnippetsManager" subtitle="by Florian Sylvain" class="py-2" />
|
||||||
<VDivider></VDivider>
|
<VDivider />
|
||||||
<VList class="pa-0">
|
<VList class="pa-0">
|
||||||
<VListItem
|
<VListItem
|
||||||
v-for="item of navItems"
|
v-for="item of navItems"
|
||||||
:key="item.name"
|
:key="item.name"
|
||||||
:title="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"
|
class="ma-2"
|
||||||
color="primary"
|
color="primary"
|
||||||
rounded
|
rounded
|
||||||
link
|
/>
|
||||||
></VListItem>
|
|
||||||
</VList>
|
</VList>
|
||||||
<template v-slot:append>
|
<template v-slot:append>
|
||||||
<div class="d-flex align-center ga-2 pa-2">
|
<VListItem
|
||||||
<VBtn variant="tonal" class="flex-grow-1" color="error"> Logout </VBtn>
|
prepend-icon="mdi-logout"
|
||||||
<VBtn
|
to="/login"
|
||||||
|
class="ma-2"
|
||||||
|
color="error"
|
||||||
variant="plain"
|
variant="plain"
|
||||||
icon="mdi-theme-light-dark"
|
active
|
||||||
:onclick="settingsStore.switchDarkMode"
|
rounded
|
||||||
></VBtn>
|
>
|
||||||
</div>
|
Disconnect
|
||||||
|
</VListItem>
|
||||||
</template>
|
</template>
|
||||||
</VNavigationDrawer>
|
</VNavigationDrawer>
|
||||||
</template>
|
</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: '/',
|
path: '/',
|
||||||
name: 'home',
|
name: 'home',
|
||||||
component: HomeView
|
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 { ref } from 'vue'
|
||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
|
import { useCookie } from '@/composables/cookies'
|
||||||
|
|
||||||
export const useSettingsStore = defineStore('settings', () => {
|
export const useSettingsStore = defineStore('settings', () => {
|
||||||
const getCookie = (name: string): string | null => {
|
const cookie = useCookie()
|
||||||
const match = document.cookie.match(new RegExp('(^| )' + name + '=([^;]+)'))
|
|
||||||
return match ? match[2] : null
|
|
||||||
}
|
|
||||||
|
|
||||||
const setCookie = (name: string, value: string, days: number = 365) => {
|
const darkModeCookie = cookie.getCookie('darkModeEnabled')
|
||||||
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 darkModeEnabled = ref(
|
const darkModeEnabled = ref(
|
||||||
darkModeCookie !== null
|
darkModeCookie !== null
|
||||||
? darkModeCookie === 'true'
|
? darkModeCookie === 'true'
|
||||||
: window.matchMedia('(prefers-color-scheme: dark)').matches
|
: 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() {
|
function switchDarkMode() {
|
||||||
darkModeEnabled.value = !darkModeEnabled.value
|
darkModeEnabled.value = !darkModeEnabled.value
|
||||||
setCookie('darkModeEnabled', darkModeEnabled.value.toString())
|
cookie.setCookie('darkModeEnabled', darkModeEnabled.value.toString())
|
||||||
}
|
}
|
||||||
|
|
||||||
return { darkModeEnabled, switchDarkMode }
|
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">
|
<script setup lang="ts"></script>
|
||||||
import NavBar from '@/components/NavBar.vue'
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<NavBar></NavBar>
|
<h1 class="text-h3">Dashboard</h1>
|
||||||
</template>
|
</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