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' {
|
||||
export interface GlobalComponents {
|
||||
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:bxsMoon': typeof import('~icons/bx/bxs-moon')['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']
|
||||
RouterView: typeof import('vue-router')['RouterView']
|
||||
TextField: typeof import('./src/components/Form/TextField.vue')['default']
|
||||
Toast: typeof import('./src/components/App/Toast.vue')['default']
|
||||
}
|
||||
}
|
||||
|
|
|
@ -23,6 +23,7 @@
|
|||
"dev": "vite",
|
||||
"build": "vite-ssg build",
|
||||
"serve": "vite preview",
|
||||
"test:watch": "vitest --watch",
|
||||
"https-preview": "serve dist"
|
||||
},
|
||||
"dependencies": {
|
||||
|
@ -46,6 +47,7 @@
|
|||
},
|
||||
"devDependencies": {
|
||||
"@iconify/json": "^2.1.78",
|
||||
"@iconify/vue": "^3.2.1",
|
||||
"@intlify/vite-plugin-vue-i18n": "^5.0.0",
|
||||
"@vitejs/plugin-vue": "^3.0.0",
|
||||
"@vue/compiler-sfc": "^3.2.37",
|
||||
|
@ -66,4 +68,4 @@
|
|||
"vitest": "^0.18.0",
|
||||
"vue-tsc": "^0.38.5"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -2,6 +2,7 @@ lockfileVersion: 5.4
|
|||
|
||||
specifiers:
|
||||
'@iconify/json': ^2.1.78
|
||||
'@iconify/vue': ^3.2.1
|
||||
'@intlify/vite-plugin-vue-i18n': ^5.0.0
|
||||
'@tailwindcss/aspect-ratio': ^0.4.0
|
||||
'@tailwindcss/forms': ^0.5.2
|
||||
|
@ -60,6 +61,7 @@ dependencies:
|
|||
|
||||
devDependencies:
|
||||
'@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
|
||||
'@vitejs/plugin-vue': 3.0.0_vite@3.0.0+vue@3.2.37
|
||||
'@vue/compiler-sfc': 3.2.37
|
||||
|
@ -1269,6 +1271,14 @@ packages:
|
|||
- supports-color
|
||||
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:
|
||||
resolution: {integrity: sha512-ghlJ0kR2cCQ8D+poKknC0Xx0ncOt3J3os7CcIAqqIWVF7k6AtGoCDnIru+YzlZcvFRNmP9wEZ7jKliojCdAWNg==}
|
||||
engines: {node: '>= 12'}
|
||||
|
|
|
@ -1,3 +1,8 @@
|
|||
<script setup lang="ts">
|
||||
import Toast from './components/App/Toast.vue';
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<Toast />
|
||||
<router-view />
|
||||
</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>
|
||||
<Toast />
|
||||
<header>
|
||||
<app-header />
|
||||
</header>
|
||||
|
||||
<main
|
||||
class="
|
||||
p-8
|
||||
dark:bg-gray-800 dark:text-white
|
||||
bg-white
|
||||
text-gray-800
|
||||
min-h-screen
|
||||
"
|
||||
class="p-8 dark:bg-gray-800 dark:text-white bg-white text-gray-800 min-h-screen"
|
||||
>
|
||||
<router-view />
|
||||
</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">
|
||||
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({
|
||||
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
|
||||
|
||||
for (let i = 0; i < registerFields.length; i++) {
|
||||
console.log(registerFields[i].label, registerFields[i].value);
|
||||
const { data, error } = await api.register({
|
||||
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 = [
|
||||
|
@ -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() {
|
||||
registerForm.value = !registerForm.value;
|
||||
}
|
||||
|
@ -83,17 +133,19 @@
|
|||
:type="field.type"
|
||||
/>
|
||||
<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
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="text-center mt-2">
|
||||
<button @click="toggleLogin">Already a User? Login</button>
|
||||
</div>
|
||||
</form>
|
||||
<div v-else>
|
||||
<form v-else @submit.prevent="login">
|
||||
<div
|
||||
class="card w-max-[500px] md:w-[500px] bg-base-100 shadow-xl"
|
||||
>
|
||||
|
@ -107,15 +159,28 @@
|
|||
:type="field.type"
|
||||
/>
|
||||
<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 class="text-center mt-2">
|
||||
<button @click="toggleLogin">Not a User? Register</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</Transition>
|
||||
<div class="text-center mt-2">
|
||||
<button @click="toggleLogin">
|
||||
{{
|
||||
registerForm
|
||||
? 'Already a User? Login'
|
||||
: 'Not a User? Register'
|
||||
}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="min-w-full absolute bottom-0 z-[-1]">
|
||||
<svg
|
||||
|
@ -140,10 +205,10 @@
|
|||
</template>
|
||||
|
||||
<route lang="yaml">
|
||||
name: home
|
||||
name: login
|
||||
</route>
|
||||
|
||||
<style lang="css">
|
||||
<style lang="css" scoped>
|
||||
.slide-fade-enter-active {
|
||||
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' {
|
||||
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> }>,
|
||||
'home': RouteRecordInfo<'home', '/home', Record<never, never>, Record<never, never>>,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -102,6 +102,11 @@ export default defineConfig({
|
|||
fs: {
|
||||
strict: true,
|
||||
},
|
||||
proxy: {
|
||||
'/api': {
|
||||
target: 'http://localhost:7745',
|
||||
},
|
||||
},
|
||||
},
|
||||
optimizeDeps: {
|
||||
include: ['vue', 'vue-router', '@vueuse/core', '@vueuse/head'],
|
||||
|
@ -114,11 +119,15 @@ export default defineConfig({
|
|||
onFinished() {
|
||||
generateSitemap();
|
||||
},
|
||||
mock: true
|
||||
mock: true,
|
||||
},
|
||||
// https://github.com/vitest-dev/vitest
|
||||
test: {
|
||||
include: ['src/__test__/**/*.test.ts', 'src/__test__/**/*.spec.ts'],
|
||||
include: [
|
||||
'src/__test__/**/*.test.ts',
|
||||
'src/**/*.test.ts',
|
||||
'src/__test__/**/*.spec.ts',
|
||||
],
|
||||
environment: 'jsdom',
|
||||
deps: {
|
||||
inline: ['@vue', '@vueuse', 'vue-demi'],
|
||||
|
|
Loading…
Reference in a new issue