forked from mirrors/homebox
setup basic auth
This commit is contained in:
parent
5471cb16ff
commit
7361dcc5f7
14 changed files with 382 additions and 28 deletions
3
frontend/components.d.ts
vendored
3
frontend/components.d.ts
vendored
|
@ -8,11 +8,14 @@ export {}
|
||||||
declare module '@vue/runtime-core' {
|
declare module '@vue/runtime-core' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
AppHeader: typeof import('./src/components/AppHeader.vue')['default']
|
AppHeader: typeof import('./src/components/AppHeader.vue')['default']
|
||||||
|
Icon: typeof import('./src/components/Icon.vue')['default']
|
||||||
'Icon:bx:bxMoon': typeof import('~icons/bx/bx-moon')['default']
|
'Icon:bx:bxMoon': typeof import('~icons/bx/bx-moon')['default']
|
||||||
'Icon:bx:bxsMoon': typeof import('~icons/bx/bxs-moon')['default']
|
'Icon:bx:bxsMoon': typeof import('~icons/bx/bxs-moon')['default']
|
||||||
'IconAkarIcons:githubFill': typeof import('~icons/akar-icons/github-fill')['default']
|
'IconAkarIcons:githubFill': typeof import('~icons/akar-icons/github-fill')['default']
|
||||||
|
Notifier: typeof import('./src/components/App/Notifier.vue')['default']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
TextField: typeof import('./src/components/Form/TextField.vue')['default']
|
TextField: typeof import('./src/components/Form/TextField.vue')['default']
|
||||||
|
Toast: typeof import('./src/components/App/Toast.vue')['default']
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -23,6 +23,7 @@
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "vite-ssg build",
|
"build": "vite-ssg build",
|
||||||
"serve": "vite preview",
|
"serve": "vite preview",
|
||||||
|
"test:watch": "vitest --watch",
|
||||||
"https-preview": "serve dist"
|
"https-preview": "serve dist"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
@ -46,6 +47,7 @@
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@iconify/json": "^2.1.78",
|
"@iconify/json": "^2.1.78",
|
||||||
|
"@iconify/vue": "^3.2.1",
|
||||||
"@intlify/vite-plugin-vue-i18n": "^5.0.0",
|
"@intlify/vite-plugin-vue-i18n": "^5.0.0",
|
||||||
"@vitejs/plugin-vue": "^3.0.0",
|
"@vitejs/plugin-vue": "^3.0.0",
|
||||||
"@vue/compiler-sfc": "^3.2.37",
|
"@vue/compiler-sfc": "^3.2.37",
|
||||||
|
@ -66,4 +68,4 @@
|
||||||
"vitest": "^0.18.0",
|
"vitest": "^0.18.0",
|
||||||
"vue-tsc": "^0.38.5"
|
"vue-tsc": "^0.38.5"
|
||||||
}
|
}
|
||||||
}
|
}
|
|
@ -2,6 +2,7 @@ lockfileVersion: 5.4
|
||||||
|
|
||||||
specifiers:
|
specifiers:
|
||||||
'@iconify/json': ^2.1.78
|
'@iconify/json': ^2.1.78
|
||||||
|
'@iconify/vue': ^3.2.1
|
||||||
'@intlify/vite-plugin-vue-i18n': ^5.0.0
|
'@intlify/vite-plugin-vue-i18n': ^5.0.0
|
||||||
'@tailwindcss/aspect-ratio': ^0.4.0
|
'@tailwindcss/aspect-ratio': ^0.4.0
|
||||||
'@tailwindcss/forms': ^0.5.2
|
'@tailwindcss/forms': ^0.5.2
|
||||||
|
@ -60,6 +61,7 @@ dependencies:
|
||||||
|
|
||||||
devDependencies:
|
devDependencies:
|
||||||
'@iconify/json': 2.1.78
|
'@iconify/json': 2.1.78
|
||||||
|
'@iconify/vue': 3.2.1_vue@3.2.37
|
||||||
'@intlify/vite-plugin-vue-i18n': 5.0.0_vite@3.0.0+vue-i18n@9.1.10
|
'@intlify/vite-plugin-vue-i18n': 5.0.0_vite@3.0.0+vue-i18n@9.1.10
|
||||||
'@vitejs/plugin-vue': 3.0.0_vite@3.0.0+vue@3.2.37
|
'@vitejs/plugin-vue': 3.0.0_vite@3.0.0+vue@3.2.37
|
||||||
'@vue/compiler-sfc': 3.2.37
|
'@vue/compiler-sfc': 3.2.37
|
||||||
|
@ -1269,6 +1271,14 @@ packages:
|
||||||
- supports-color
|
- supports-color
|
||||||
dev: true
|
dev: true
|
||||||
|
|
||||||
|
/@iconify/vue/3.2.1_vue@3.2.37:
|
||||||
|
resolution: {integrity: sha512-c4R6ZgFo1JrJ8aPMMgOPgfU7lBswihMGR+yWe/P4ZukC3kTkeT4+lkt9Pc/itVFMkwva/S/7u9YofmYv57fnNQ==}
|
||||||
|
peerDependencies:
|
||||||
|
vue: 3.x
|
||||||
|
dependencies:
|
||||||
|
vue: 3.2.37
|
||||||
|
dev: true
|
||||||
|
|
||||||
/@intlify/bundle-utils/3.1.0_vue-i18n@9.1.10:
|
/@intlify/bundle-utils/3.1.0_vue-i18n@9.1.10:
|
||||||
resolution: {integrity: sha512-ghlJ0kR2cCQ8D+poKknC0Xx0ncOt3J3os7CcIAqqIWVF7k6AtGoCDnIru+YzlZcvFRNmP9wEZ7jKliojCdAWNg==}
|
resolution: {integrity: sha512-ghlJ0kR2cCQ8D+poKknC0Xx0ncOt3J3os7CcIAqqIWVF7k6AtGoCDnIru+YzlZcvFRNmP9wEZ7jKliojCdAWNg==}
|
||||||
engines: {node: '>= 12'}
|
engines: {node: '>= 12'}
|
||||||
|
|
|
@ -1,3 +1,8 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import Toast from './components/App/Toast.vue';
|
||||||
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
<Toast />
|
||||||
<router-view />
|
<router-view />
|
||||||
</template>
|
</template>
|
||||||
|
|
71
frontend/src/components/App/Toast.vue
Normal file
71
frontend/src/components/App/Toast.vue
Normal file
|
@ -0,0 +1,71 @@
|
||||||
|
<template>
|
||||||
|
<div class="force-above fixed top-2 right-2 w-[300px]">
|
||||||
|
<TransitionGroup name="notify" tag="div">
|
||||||
|
<div
|
||||||
|
v-for="(notify, index) in notifications.slice(0, 4)"
|
||||||
|
:key="notify.id"
|
||||||
|
class="my-2 w-[300px] rounded-md p-3 text-sm text-white opacity-75"
|
||||||
|
:class="{
|
||||||
|
'bg-primary': notify.type === 'info',
|
||||||
|
'bg-red-600': notify.type === 'error',
|
||||||
|
'bg-green-600': notify.type === 'success',
|
||||||
|
}"
|
||||||
|
@click="dropNotification(index)"
|
||||||
|
>
|
||||||
|
<div class="flex gap-1">
|
||||||
|
<template v-if="notify.type == 'info'">
|
||||||
|
<Icon
|
||||||
|
icon="mdi-information-outline"
|
||||||
|
class="h-5 w-5"
|
||||||
|
height="25"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<template v-if="notify.type == 'success'">
|
||||||
|
<Icon
|
||||||
|
icon="mdi-check-circle-outline"
|
||||||
|
class="h-5 w-5"
|
||||||
|
height="25"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-if="notify.type == 'error'">
|
||||||
|
<Icon
|
||||||
|
icon="mdi-alert-circle-outline"
|
||||||
|
class="h-5 w-5"
|
||||||
|
height="25"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
{{ notify.message }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</TransitionGroup>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Icon } from '@iconify/vue';
|
||||||
|
import { useNotifications } from '@/composables/use-notifier';
|
||||||
|
|
||||||
|
const { notifications, dropNotification } = useNotifications();
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.force-above {
|
||||||
|
z-index: 9999;
|
||||||
|
}
|
||||||
|
|
||||||
|
.notify-move,
|
||||||
|
.notify-enter-active,
|
||||||
|
.notify-leave-active {
|
||||||
|
transition: all 0.5s ease;
|
||||||
|
}
|
||||||
|
.notify-enter-from,
|
||||||
|
.notify-leave-to {
|
||||||
|
opacity: 0;
|
||||||
|
transform: translateY(-30px);
|
||||||
|
}
|
||||||
|
.notify-leave-active {
|
||||||
|
position: absolute;
|
||||||
|
transform: translateY(30px);
|
||||||
|
}
|
||||||
|
</style>
|
23
frontend/src/composables/use-api.ts
Normal file
23
frontend/src/composables/use-api.ts
Normal file
|
@ -0,0 +1,23 @@
|
||||||
|
import { PublicApi } from '@/api/public';
|
||||||
|
import { UserApi } from '@/api/user';
|
||||||
|
import { Requests } from '@/lib/requests';
|
||||||
|
import { useAuthStore } from '@/store/auth';
|
||||||
|
|
||||||
|
async function ApiDebugger(r: Response) {
|
||||||
|
console.table({
|
||||||
|
'Request Url': r.url,
|
||||||
|
'Response Status': r.status,
|
||||||
|
'Response Status Text': r.statusText,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function usePublicApi(): PublicApi {
|
||||||
|
const requests = new Requests('', '', {}, ApiDebugger);
|
||||||
|
return new PublicApi(requests);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useUserApi(): UserApi {
|
||||||
|
const authStore = useAuthStore();
|
||||||
|
const requests = new Requests('', () => authStore.token, {}, ApiDebugger);
|
||||||
|
return new UserApi(requests);
|
||||||
|
}
|
31
frontend/src/composables/use-ids.ts
Executable file
31
frontend/src/composables/use-ids.ts
Executable file
|
@ -0,0 +1,31 @@
|
||||||
|
function slugify(text: string) {
|
||||||
|
return text
|
||||||
|
.toString()
|
||||||
|
.toLowerCase()
|
||||||
|
.replace(/\s+/g, '-') // Replace spaces with -
|
||||||
|
.replace(/[^\w\-]+/g, '') // Remove all non-word chars
|
||||||
|
.replace(/\-\-+/g, '-') // Replace multiple - with single -
|
||||||
|
.replace(/^-+/, '') // Trim - from start of text
|
||||||
|
.replace(/-+$/, ''); // Trim - from end of text
|
||||||
|
}
|
||||||
|
|
||||||
|
function idGenerator(): string {
|
||||||
|
const id =
|
||||||
|
Math.random().toString(32).substring(2, 6) +
|
||||||
|
Math.random().toString(36).substring(2, 6);
|
||||||
|
return slugify(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* useFormIds uses the provided label to generate a unique id for the
|
||||||
|
* form element. If no label is provided the id is generated using a
|
||||||
|
* random string.
|
||||||
|
*/
|
||||||
|
export function useFormIds(label: string): string {
|
||||||
|
const slug = label ? slugify(label) : idGenerator();
|
||||||
|
return `${slug}-${idGenerator()}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useId(): string {
|
||||||
|
return idGenerator();
|
||||||
|
}
|
57
frontend/src/composables/use-notifier.ts
Normal file
57
frontend/src/composables/use-notifier.ts
Normal file
|
@ -0,0 +1,57 @@
|
||||||
|
import { useId } from './use-ids';
|
||||||
|
|
||||||
|
interface Notification {
|
||||||
|
id: string;
|
||||||
|
message: string;
|
||||||
|
type: 'success' | 'error' | 'info';
|
||||||
|
}
|
||||||
|
|
||||||
|
const notifications = ref<Notification[]>([]);
|
||||||
|
|
||||||
|
function addNotification(notification: Notification) {
|
||||||
|
notifications.value.unshift(notification);
|
||||||
|
|
||||||
|
if (notifications.value.length > 4) {
|
||||||
|
notifications.value.pop();
|
||||||
|
} else {
|
||||||
|
setTimeout(() => {
|
||||||
|
// Remove notification with ID
|
||||||
|
notifications.value = notifications.value.filter(
|
||||||
|
n => n.id !== notification.id
|
||||||
|
);
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useNotifications() {
|
||||||
|
return {
|
||||||
|
notifications,
|
||||||
|
dropNotification: (idx: number) => notifications.value.splice(idx, 1),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function useNotifier() {
|
||||||
|
return {
|
||||||
|
success: (message: string) => {
|
||||||
|
addNotification({
|
||||||
|
id: useId(),
|
||||||
|
message,
|
||||||
|
type: 'success',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
error: (message: string) => {
|
||||||
|
addNotification({
|
||||||
|
id: useId(),
|
||||||
|
message,
|
||||||
|
type: 'error',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
info: (message: string) => {
|
||||||
|
addNotification({
|
||||||
|
id: useId(),
|
||||||
|
message,
|
||||||
|
type: 'info',
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
|
@ -1,17 +1,14 @@
|
||||||
<script setup lang="ts"></script>
|
<script setup lang="ts">
|
||||||
|
import Toast from '@/components/App/Toast.vue';
|
||||||
|
</script>
|
||||||
<template>
|
<template>
|
||||||
|
<Toast />
|
||||||
<header>
|
<header>
|
||||||
<app-header />
|
<app-header />
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main
|
<main
|
||||||
class="
|
class="p-8 dark:bg-gray-800 dark:text-white bg-white text-gray-800 min-h-screen"
|
||||||
p-8
|
|
||||||
dark:bg-gray-800 dark:text-white
|
|
||||||
bg-white
|
|
||||||
text-gray-800
|
|
||||||
min-h-screen
|
|
||||||
"
|
|
||||||
>
|
>
|
||||||
<router-view />
|
<router-view />
|
||||||
</main>
|
</main>
|
||||||
|
|
59
frontend/src/pages/home.vue
Normal file
59
frontend/src/pages/home.vue
Normal file
|
@ -0,0 +1,59 @@
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { useUserApi } from '@/composables/use-api';
|
||||||
|
useHead({
|
||||||
|
title: 'Homebox | Home',
|
||||||
|
});
|
||||||
|
|
||||||
|
const links = [
|
||||||
|
{
|
||||||
|
name: 'Home',
|
||||||
|
href: '/home',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Logout',
|
||||||
|
href: '/logout',
|
||||||
|
last: true,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
const api = useUserApi();
|
||||||
|
|
||||||
|
const user = ref({});
|
||||||
|
|
||||||
|
onMounted(async () => {
|
||||||
|
const { data } = await api.self();
|
||||||
|
|
||||||
|
if (data) {
|
||||||
|
user.value = data.item;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<section class="max-w-7xl mx-auto">
|
||||||
|
<header class="sm:px-6 py-2 lg:p-14 sm:py-6">
|
||||||
|
<h2
|
||||||
|
class="mt-1 text-4xl font-bold tracking-tight text-gray-200 sm:text-5xl lg:text-6xl"
|
||||||
|
>
|
||||||
|
Homebox
|
||||||
|
</h2>
|
||||||
|
<div class="ml-1 text-lg text-gray-400 space-x-2 italic">
|
||||||
|
<template v-for="link in links">
|
||||||
|
<router-link
|
||||||
|
class="hover:text-base-content transition-color duration-200"
|
||||||
|
:to="link.href"
|
||||||
|
>
|
||||||
|
{{ link.name }}
|
||||||
|
</router-link>
|
||||||
|
<span v-if="!link.last"> / </span>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</header>
|
||||||
|
</section>
|
||||||
|
<section class="max-w-7xl mx-auto sm:px-6 lg:px-14">
|
||||||
|
{{ user }}
|
||||||
|
</section>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<route lang="yaml">
|
||||||
|
name: home
|
||||||
|
</route>
|
|
@ -1,5 +1,8 @@
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import TextField from '@/components/Form/TextField.vue';
|
import TextField from '@/components/Form/TextField.vue';
|
||||||
|
import { useNotifier } from '@/composables/use-notifier';
|
||||||
|
import { usePublicApi } from '@/composables/use-api';
|
||||||
|
import { useAuthStore } from '@/store/auth';
|
||||||
useHead({
|
useHead({
|
||||||
title: 'Homebox | Organize and Tag Your Stuff',
|
title: 'Homebox | Organize and Tag Your Stuff',
|
||||||
});
|
});
|
||||||
|
@ -29,12 +32,29 @@
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
function registerUser() {
|
const api = usePublicApi();
|
||||||
|
|
||||||
|
async function registerUser() {
|
||||||
|
loading.value = true;
|
||||||
// Print Values of registerFields
|
// Print Values of registerFields
|
||||||
|
|
||||||
for (let i = 0; i < registerFields.length; i++) {
|
const { data, error } = await api.register({
|
||||||
console.log(registerFields[i].label, registerFields[i].value);
|
user: {
|
||||||
|
name: registerFields[0].value,
|
||||||
|
email: registerFields[1].value,
|
||||||
|
password: registerFields[3].value,
|
||||||
|
},
|
||||||
|
groupName: registerFields[2].value,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
toast.error('Problem registering user');
|
||||||
|
} else {
|
||||||
|
toast.success('User registered');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
console.log(data);
|
||||||
|
loading.value = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const loginFields = [
|
const loginFields = [
|
||||||
|
@ -49,8 +69,38 @@
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const registerForm = ref(false);
|
const authStore = useAuthStore();
|
||||||
|
|
||||||
|
const toast = useNotifier();
|
||||||
|
const loading = ref(false);
|
||||||
|
|
||||||
|
const router = useRouter();
|
||||||
|
|
||||||
|
async function login() {
|
||||||
|
loading.value = true;
|
||||||
|
const { data, error } = await api.login(
|
||||||
|
loginFields[0].value,
|
||||||
|
loginFields[1].value
|
||||||
|
);
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
toast.error('Invalid email or password');
|
||||||
|
} else {
|
||||||
|
toast.success('Logged in successfully');
|
||||||
|
|
||||||
|
console.log(data);
|
||||||
|
|
||||||
|
authStore.$patch({
|
||||||
|
token: data.token,
|
||||||
|
expires: data.expiresAt,
|
||||||
|
});
|
||||||
|
|
||||||
|
router.push({ name: 'home' });
|
||||||
|
}
|
||||||
|
loading.value = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const registerForm = ref(false);
|
||||||
function toggleLogin() {
|
function toggleLogin() {
|
||||||
registerForm.value = !registerForm.value;
|
registerForm.value = !registerForm.value;
|
||||||
}
|
}
|
||||||
|
@ -83,17 +133,19 @@
|
||||||
:type="field.type"
|
:type="field.type"
|
||||||
/>
|
/>
|
||||||
<div class="card-actions justify-end">
|
<div class="card-actions justify-end">
|
||||||
<button type="submit" class="btn btn-primary mt-2">
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-primary mt-2"
|
||||||
|
:class="loading ? 'loading' : ''"
|
||||||
|
:disabled="loading"
|
||||||
|
>
|
||||||
Register
|
Register
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-center mt-2">
|
|
||||||
<button @click="toggleLogin">Already a User? Login</button>
|
|
||||||
</div>
|
|
||||||
</form>
|
</form>
|
||||||
<div v-else>
|
<form v-else @submit.prevent="login">
|
||||||
<div
|
<div
|
||||||
class="card w-max-[500px] md:w-[500px] bg-base-100 shadow-xl"
|
class="card w-max-[500px] md:w-[500px] bg-base-100 shadow-xl"
|
||||||
>
|
>
|
||||||
|
@ -107,15 +159,28 @@
|
||||||
:type="field.type"
|
:type="field.type"
|
||||||
/>
|
/>
|
||||||
<div class="card-actions justify-end mt-2">
|
<div class="card-actions justify-end mt-2">
|
||||||
<button class="btn btn-primary">Login</button>
|
<button
|
||||||
|
type="submit"
|
||||||
|
class="btn btn-primary"
|
||||||
|
:class="loading ? 'loading' : ''"
|
||||||
|
:disabled="loading"
|
||||||
|
>
|
||||||
|
Login
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="text-center mt-2">
|
</form>
|
||||||
<button @click="toggleLogin">Not a User? Register</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</Transition>
|
</Transition>
|
||||||
|
<div class="text-center mt-2">
|
||||||
|
<button @click="toggleLogin">
|
||||||
|
{{
|
||||||
|
registerForm
|
||||||
|
? 'Already a User? Login'
|
||||||
|
: 'Not a User? Register'
|
||||||
|
}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="min-w-full absolute bottom-0 z-[-1]">
|
<div class="min-w-full absolute bottom-0 z-[-1]">
|
||||||
<svg
|
<svg
|
||||||
|
@ -140,10 +205,10 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<route lang="yaml">
|
<route lang="yaml">
|
||||||
name: home
|
name: login
|
||||||
</route>
|
</route>
|
||||||
|
|
||||||
<style lang="css">
|
<style lang="css" scoped>
|
||||||
.slide-fade-enter-active {
|
.slide-fade-enter-active {
|
||||||
transition: all 0.2s ease-out;
|
transition: all 0.2s ease-out;
|
||||||
}
|
}
|
||||||
|
|
21
frontend/src/store/auth.ts
Normal file
21
frontend/src/store/auth.ts
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
import { defineStore } from 'pinia';
|
||||||
|
|
||||||
|
export const useAuthStore = defineStore('auth', {
|
||||||
|
state: () => ({
|
||||||
|
token: useLocalStorage('pinia/auth/token', ''),
|
||||||
|
expires: useLocalStorage('pinia/auth/expires', ''),
|
||||||
|
}),
|
||||||
|
getters: {
|
||||||
|
isTokenExpired: state => {
|
||||||
|
if (!state.expires) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof state.expires === 'string') {
|
||||||
|
return new Date(state.expires) < new Date();
|
||||||
|
}
|
||||||
|
|
||||||
|
return state.expires < new Date();
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
3
frontend/typed-router.d.ts
vendored
3
frontend/typed-router.d.ts
vendored
|
@ -29,8 +29,9 @@ import type {
|
||||||
|
|
||||||
declare module '@vue-router/routes' {
|
declare module '@vue-router/routes' {
|
||||||
export interface RouteNamedMap {
|
export interface RouteNamedMap {
|
||||||
'home': RouteRecordInfo<'home', '/', Record<never, never>, Record<never, never>>,
|
'login': RouteRecordInfo<'login', '/', Record<never, never>, Record<never, never>>,
|
||||||
'not-found': RouteRecordInfo<'not-found', '/:all(.*)', { all: ParamValue<true> }, { all: ParamValue<false> }>,
|
'not-found': RouteRecordInfo<'not-found', '/:all(.*)', { all: ParamValue<true> }, { all: ParamValue<false> }>,
|
||||||
|
'home': RouteRecordInfo<'home', '/home', Record<never, never>, Record<never, never>>,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -102,6 +102,11 @@ export default defineConfig({
|
||||||
fs: {
|
fs: {
|
||||||
strict: true,
|
strict: true,
|
||||||
},
|
},
|
||||||
|
proxy: {
|
||||||
|
'/api': {
|
||||||
|
target: 'http://localhost:7745',
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
optimizeDeps: {
|
optimizeDeps: {
|
||||||
include: ['vue', 'vue-router', '@vueuse/core', '@vueuse/head'],
|
include: ['vue', 'vue-router', '@vueuse/core', '@vueuse/head'],
|
||||||
|
@ -114,11 +119,15 @@ export default defineConfig({
|
||||||
onFinished() {
|
onFinished() {
|
||||||
generateSitemap();
|
generateSitemap();
|
||||||
},
|
},
|
||||||
mock: true
|
mock: true,
|
||||||
},
|
},
|
||||||
// https://github.com/vitest-dev/vitest
|
// https://github.com/vitest-dev/vitest
|
||||||
test: {
|
test: {
|
||||||
include: ['src/__test__/**/*.test.ts', 'src/__test__/**/*.spec.ts'],
|
include: [
|
||||||
|
'src/__test__/**/*.test.ts',
|
||||||
|
'src/**/*.test.ts',
|
||||||
|
'src/__test__/**/*.spec.ts',
|
||||||
|
],
|
||||||
environment: 'jsdom',
|
environment: 'jsdom',
|
||||||
deps: {
|
deps: {
|
||||||
inline: ['@vue', '@vueuse', 'vue-demi'],
|
inline: ['@vue', '@vueuse', 'vue-demi'],
|
||||||
|
|
Loading…
Reference in a new issue