You've already forked obsidian-visualiser
Migration to Nuxt v4 file structure and dependencies update
This commit is contained in:
18
app/pages/user/(automatic)/mailvalidated.vue
Normal file
18
app/pages/user/(automatic)/mailvalidated.vue
Normal file
@@ -0,0 +1,18 @@
|
||||
<template>
|
||||
<Head>
|
||||
<Title>d[any] - Validation de votre adresse mail</Title>
|
||||
</Head>
|
||||
<div class="flex flex-col justify-center items-center">
|
||||
<h2 class="text-2xl font-bold">Votre compte a été validé ! 🎉</h2>
|
||||
<div class="flex flex-row gap-8">
|
||||
<Button class="bg-light-25 dark:bg-dark-25"><NuxtLink :to="{ name: 'user-login', replace: true }">Se connecter</NuxtLink></Button>
|
||||
<Button class="bg-light-25 dark:bg-dark-25"><NuxtLink :to="{ name: 'index', replace: true }">Retourner à l'accueil</NuxtLink></Button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
definePageMeta({
|
||||
layout: 'login',
|
||||
})
|
||||
</script>
|
||||
45
app/pages/user/(automatic)/reset-password.vue
Normal file
45
app/pages/user/(automatic)/reset-password.vue
Normal file
@@ -0,0 +1,45 @@
|
||||
<template>
|
||||
<Head>
|
||||
<Title>d[any] - Reinitialisation de mon mot de passe</Title>
|
||||
</Head>
|
||||
<div class="flex flex-1 flex-col justify-center items-center">
|
||||
<div class="flex gap-8 items-center">
|
||||
<span class="border border-transparent hover:border-light-35 dark:hover:border-dark-35 p-1 cursor-pointer" @click="() => $router.go(-1)"><Icon icon="radix-icons:arrow-left" class="text-light-50 dark:text-dark-50 w-6 h-6"/></span>
|
||||
<h4 class="text-xl font-bold">Reinitialisation de mon mot de passe</h4>
|
||||
</div>
|
||||
<form @submit.prevent="() => submit()" class="flex flex-1 flex-col justify-center items-stretch">
|
||||
<TextInput type="text" label="Utilisateur ou email" autocomplete="username" v-model="email"/>
|
||||
<Button class="border border-light-35 dark:border-dark-35 self-center" :loading="status === 'pending'">Envoyer un email</Button>
|
||||
</form>
|
||||
<div v-if="status === 'success'" class="border border-light-green dark:border-dark-green bg-light-greenBack dark:bg-dark-greenBack text-wrap mt-4 py-2 px-4 max-w-96">
|
||||
Un mail vous a été envoyé si un compte existe pour cet identifiant.
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue';
|
||||
|
||||
definePageMeta({
|
||||
layout: 'login',
|
||||
usersGoesTo: '/user/profile',
|
||||
});
|
||||
|
||||
const email = ref(''), status = ref<'idle' | 'pending' | 'success' | 'error'>('idle');
|
||||
|
||||
async function submit()
|
||||
{
|
||||
status.value = 'pending';
|
||||
try {
|
||||
await $fetch(`/api/auth/request-reset`, {
|
||||
body: { profile: email.value },
|
||||
method: 'post',
|
||||
});
|
||||
status.value = 'success';
|
||||
}
|
||||
catch(e)
|
||||
{
|
||||
status.value = 'error';
|
||||
}
|
||||
}
|
||||
</script>
|
||||
87
app/pages/user/(automatic)/resetting-password.vue
Normal file
87
app/pages/user/(automatic)/resetting-password.vue
Normal file
@@ -0,0 +1,87 @@
|
||||
<template>
|
||||
<Head>
|
||||
<Title>d[any] - Reinitialisation de mon mot de passe</Title>
|
||||
</Head>
|
||||
<div class="flex flex-1 flex-col justify-center items-center">
|
||||
<div class="flex gap-8 items-center">
|
||||
<span class="border border-transparent hover:border-light-35 dark:hover:border-dark-35 p-1 cursor-pointer" @click="() => $router.go(-1)"><Icon icon="radix-icons:arrow-left" class="text-light-50 dark:text-dark-50 w-6 h-6"/></span>
|
||||
<h4 class="text-center flex-1 text-xl font-bold">Reinitialisation de mon mot de passe</h4>
|
||||
</div>
|
||||
<form @submit.prevent="submit" class="flex flex-1 flex-col justify-center items-stretch">
|
||||
<TextInput type="password" label="Nouveau mot de passe" autocomplete="newPassword" v-model="newPasswd" :class="{ '!border-light-red !dark:border-dark-red': error }"/>
|
||||
<div class="grid grid-cols-2 flex-col font-light border border-light-35 dark:border-dark-35 px-4 py-2 m-4 ms-0 text-sm leading-[18px] lg:text-base order-8 col-span-2 md:col-span-1 md:order-none">
|
||||
<span class="col-span-2">Prérequis de sécurité</span>
|
||||
<span class="ps-4 flex items-center gap-2" :class="{'text-light-red dark:text-dark-red': !checkedLength}"><Icon v-show="!checkedLength" icon="radix-icons:cross-2" />8 à 128 caractères</span>
|
||||
<span class="ps-4 flex items-center gap-2" :class="{'text-light-red dark:text-dark-red': !checkedLower}"><Icon v-show="!checkedLower" icon="radix-icons:cross-2" />Une minuscule</span>
|
||||
<span class="ps-4 flex items-center gap-2" :class="{'text-light-red dark:text-dark-red': !checkedUpper}"><Icon v-show="!checkedUpper" icon="radix-icons:cross-2" />Une majuscule</span>
|
||||
<span class="ps-4 flex items-center gap-2" :class="{'text-light-red dark:text-dark-red': !checkedDigit}"><Icon v-show="!checkedDigit" icon="radix-icons:cross-2" />Un chiffre</span>
|
||||
<span class="ps-4 flex items-center gap-2" :class="{'text-light-red dark:text-dark-red': !checkedSymbol}"><Icon v-show="!checkedSymbol" icon="radix-icons:cross-2" />Un caractère special</span>
|
||||
</div>
|
||||
<TextInput type="password" label="Repeter le nouveau mot de passe" autocomplete="newPassword" v-model="repeatPasswd" :class="{ 'border-light-red dark:border-dark-red': manualError }"/>
|
||||
<Button class="border border-light-35 dark:border-dark-35 self-center" :loading="status === 'pending'">Reinitialiser</Button>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue';
|
||||
import { Toaster } from '#shared/components.util';
|
||||
|
||||
definePageMeta({
|
||||
layout: 'login',
|
||||
usersGoesTo: '/user/login',
|
||||
});
|
||||
|
||||
const query = useRouter().currentRoute.value.query;
|
||||
|
||||
const status = ref<'idle' | 'pending' | 'success' | 'error'>('idle'), manualError = ref(false);
|
||||
const oldPasswd = ref(''), newPasswd = ref(''), repeatPasswd = ref('');
|
||||
|
||||
const checkedLength = computed(() => newPasswd.value.length >= 8 && newPasswd.value.length <= 128);
|
||||
const checkedLower = computed(() => newPasswd.value.toUpperCase() !== newPasswd.value);
|
||||
const checkedUpper = computed(() => newPasswd.value.toLowerCase() !== newPasswd.value);
|
||||
const checkedDigit = computed(() => /[0-9]/.test(newPasswd.value));
|
||||
const checkedSymbol = computed(() => " !\"#$%&'()*+,-./:;<=>?@[]^_`{|}~".split("").some(e => newPasswd.value.includes(e)));
|
||||
|
||||
const equalsPasswd = computed(() => newPasswd.value && repeatPasswd.value && newPasswd.value === repeatPasswd.value);
|
||||
|
||||
const error = computed(() => !checkedLength.value || !checkedLower.value || !checkedUpper.value || !checkedDigit.value || !checkedSymbol.value);
|
||||
|
||||
async function submit()
|
||||
{
|
||||
if(!equalsPasswd.value)
|
||||
{
|
||||
manualError.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
manualError.value = false;
|
||||
status.value = 'pending';
|
||||
try {
|
||||
const result = await $fetch(`/api/users/${query.i}/reset-password`, {
|
||||
method: 'post',
|
||||
body: {
|
||||
password: newPasswd.value,
|
||||
},
|
||||
query: query,
|
||||
});
|
||||
|
||||
if(result && result.success)
|
||||
{
|
||||
status.value = 'success';
|
||||
|
||||
Toaster.add({ content: 'Votre mot de passe a été modifié avec succès.', duration: 10000, timer: true, type: 'success' });
|
||||
useRouter().push({ name: 'user-login' });
|
||||
}
|
||||
else
|
||||
{
|
||||
throw result.error ?? new Error('Erreur inconnue.');
|
||||
}
|
||||
} catch(e) {
|
||||
status.value = 'error';
|
||||
|
||||
const err = e as any;
|
||||
Toaster.add({ content: err?.data?.message ?? err?.message ?? 'Erreur inconnue', duration: 10000, timer: true, type: 'error' });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
3
app/pages/user/[id].vue
Normal file
3
app/pages/user/[id].vue
Normal file
@@ -0,0 +1,3 @@
|
||||
<template>
|
||||
User profile: {{ $route.params.id }}
|
||||
</template>
|
||||
88
app/pages/user/changing-password.vue
Normal file
88
app/pages/user/changing-password.vue
Normal file
@@ -0,0 +1,88 @@
|
||||
<template>
|
||||
<Head>
|
||||
<Title>d[any] - Modification de mon mot de passe</Title>
|
||||
</Head>
|
||||
<div class="flex flex-1 flex-col justify-center items-center">
|
||||
<div class="flex gap-8 items-center">
|
||||
<span class="border border-transparent hover:border-light-35 dark:hover:border-dark-35 p-1 cursor-pointer" @click="() => $router.go(-1)"><Icon icon="radix-icons:arrow-left" class="text-light-50 dark:text-dark-50 w-6 h-6"/></span>
|
||||
<h4 class="text-center flex-1 text-xl font-bold">Modification de mon mot de passe</h4>
|
||||
</div>
|
||||
<form @submit.prevent="submit" class="flex flex-1 flex-col justify-center items-stretch">
|
||||
<TextInput type="password" label="Ancien mot de passe" name="old-password" autocomplete="current-password" v-model="oldPasswd"/>
|
||||
<TextInput type="password" label="Nouveau mot de passe" name="new-password" autocomplete="new-password" v-model="newPasswd" :class="{ 'border-light-red dark:border-dark-red': error }"/>
|
||||
<div class="grid grid-cols-2 flex-col font-light border border-light-35 dark:border-dark-35 px-4 py-2 m-4 ms-0 text-sm leading-[18px] lg:text-base order-8 col-span-2 md:col-span-1 md:order-none">
|
||||
<span class="col-span-2">Prérequis de sécurité</span>
|
||||
<span class="ps-4 flex items-center gap-2" :class="{'text-light-red dark:text-dark-red': !checkedLength}"><Icon v-show="!checkedLength" icon="radix-icons:cross-2" />8 à 128 caractères</span>
|
||||
<span class="ps-4 flex items-center gap-2" :class="{'text-light-red dark:text-dark-red': !checkedLower}"><Icon v-show="!checkedLower" icon="radix-icons:cross-2" />Une minuscule</span>
|
||||
<span class="ps-4 flex items-center gap-2" :class="{'text-light-red dark:text-dark-red': !checkedUpper}"><Icon v-show="!checkedUpper" icon="radix-icons:cross-2" />Une majuscule</span>
|
||||
<span class="ps-4 flex items-center gap-2" :class="{'text-light-red dark:text-dark-red': !checkedDigit}"><Icon v-show="!checkedDigit" icon="radix-icons:cross-2" />Un chiffre</span>
|
||||
<span class="ps-4 flex items-center gap-2" :class="{'text-light-red dark:text-dark-red': !checkedSymbol}"><Icon v-show="!checkedSymbol" icon="radix-icons:cross-2" />Un caractère special</span>
|
||||
</div>
|
||||
<TextInput type="password" label="Repeter le nouveau mot de passe" autocomplete="new-password" v-model="repeatPasswd" :class="{ 'border-light-red dark:border-dark-red': manualError }"/>
|
||||
<Button type="submit" class="border border-light-35 dark:border-dark-35 self-center" :loading="status === 'pending'">Mettre à jour mon mot de passe</Button>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { Icon } from '@iconify/vue';
|
||||
import { Toaster } from '#shared/components.util';
|
||||
|
||||
definePageMeta({
|
||||
layout: 'login',
|
||||
guestsGoesTo: '/user/login',
|
||||
});
|
||||
|
||||
const { user } = useUserSession();
|
||||
const status = ref<'idle' | 'pending' | 'success' | 'error'>('idle'), manualError = ref(false);
|
||||
const oldPasswd = ref(''), newPasswd = ref(''), repeatPasswd = ref('');
|
||||
|
||||
const checkedLength = computed(() => newPasswd.value.length >= 8 && newPasswd.value.length <= 128);
|
||||
const checkedLower = computed(() => newPasswd.value.toUpperCase() !== newPasswd.value);
|
||||
const checkedUpper = computed(() => newPasswd.value.toLowerCase() !== newPasswd.value);
|
||||
const checkedDigit = computed(() => /[0-9]/.test(newPasswd.value));
|
||||
const checkedSymbol = computed(() => " !\"#$%&'()*+,-./:;<=>?@[]^_`{|}~".split("").some(e => newPasswd.value.includes(e)));
|
||||
|
||||
const equalsPasswd = computed(() => newPasswd.value && repeatPasswd.value && newPasswd.value === repeatPasswd.value);
|
||||
|
||||
const error = computed(() => !checkedLength.value || !checkedLower.value || !checkedUpper.value || !checkedDigit.value || !checkedSymbol.value);
|
||||
|
||||
async function submit()
|
||||
{
|
||||
if(!equalsPasswd.value)
|
||||
{
|
||||
manualError.value = true;
|
||||
return;
|
||||
}
|
||||
|
||||
manualError.value = false;
|
||||
status.value = 'pending';
|
||||
try {
|
||||
const result = await $fetch(`/api/users/${user.value?.id}/change-password`, {
|
||||
method: 'post',
|
||||
body: {
|
||||
oldPassword: oldPasswd.value,
|
||||
newPassword: newPasswd.value,
|
||||
}
|
||||
});
|
||||
|
||||
if(result && result.success)
|
||||
{
|
||||
status.value = 'success';
|
||||
|
||||
Toaster.add({ content: 'Votre mot de passe a été modifié avec succès.', duration: 10000, timer: true, type: 'success' });
|
||||
useRouter().push({ name: 'user-profile' });
|
||||
}
|
||||
else
|
||||
{
|
||||
status.value = 'error';
|
||||
|
||||
Toaster.add({ content: result.error ?? 'Erreur inconnue', duration: 10000, timer: true, type: 'error' });
|
||||
}
|
||||
} catch(e) {
|
||||
status.value = 'error';
|
||||
|
||||
Toaster.add({ content: (e as Error).message ?? e, duration: 10000, timer: true, type: 'error' });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
93
app/pages/user/login.vue
Normal file
93
app/pages/user/login.vue
Normal file
@@ -0,0 +1,93 @@
|
||||
<template>
|
||||
<Head>
|
||||
<Title>d[any] - Connexion</Title>
|
||||
</Head>
|
||||
<div class="flex flex-1 flex-col justify-center items-center">
|
||||
<div class="flex gap-8 items-center">
|
||||
<span class="border border-transparent hover:border-light-35 dark:hover:border-dark-35 p-1 cursor-pointer" @click="() => $router.go(-1)"><Icon icon="radix-icons:arrow-left" class="text-light-50 dark:text-dark-50 w-6 h-6"/></span>
|
||||
<h4 class="text-xl font-bold">Connexion</h4>
|
||||
</div>
|
||||
<form @submit.prevent="() => submit()" class="flex flex-1 flex-col justify-center items-stretch">
|
||||
<TextInput type="text" label="Utilisateur ou email" name="username" autocomplete="username email" v-model="state.usernameOrEmail"/>
|
||||
<TextInput type="password" label="Mot de passe" name="password" autocomplete="current-password" v-model="state.password"/>
|
||||
<Button type="submit" class="border border-light-35 dark:border-dark-35 self-center" :loading="status === 'pending'">Se connecter</Button>
|
||||
<NuxtLink class="mt-4 text-center block text-sm font-semibold tracking-wide hover:text-accent-blue" :to="{ name: 'user-reset-password' }">Mot de passe oublié ?</NuxtLink>
|
||||
<NuxtLink class="mt-4 text-center block text-sm font-semibold tracking-wide hover:text-accent-blue" :to="{ name: 'user-register' }">Pas de compte ?</NuxtLink>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { ZodError } from 'zod/v4';
|
||||
import { schema, type Login } from '~/schemas/login';
|
||||
import { Icon } from '@iconify/vue';
|
||||
import { Toaster } from '#shared/components.util';
|
||||
|
||||
definePageMeta({
|
||||
layout: 'login',
|
||||
usersGoesTo: '/user/profile',
|
||||
});
|
||||
|
||||
const state = reactive<Login>({
|
||||
usernameOrEmail: '',
|
||||
password: ''
|
||||
});
|
||||
|
||||
const { data: result, status, error, refresh } = await useFetch('/api/auth/login', {
|
||||
body: state,
|
||||
immediate: false,
|
||||
method: 'POST',
|
||||
watch: false,
|
||||
ignoreResponseError: true,
|
||||
})
|
||||
|
||||
async function submit()
|
||||
{
|
||||
if(state.usernameOrEmail === "")
|
||||
return Toaster.add({ content: 'Veuillez saisir un nom d\'utilisateur ou un email', timer: true, duration: 10000 });
|
||||
if(state.password === "")
|
||||
return Toaster.add({ content: 'Veuillez saisir un mot de passe', timer: true, duration: 10000 });
|
||||
|
||||
const data = schema.safeParse(state);
|
||||
|
||||
if(data.success)
|
||||
{
|
||||
await refresh();
|
||||
|
||||
const login = result.value;
|
||||
if(!login || !login.success)
|
||||
{
|
||||
handleErrors(login?.error ?? error.value!);
|
||||
}
|
||||
else if(status.value === 'success' && login.success)
|
||||
{
|
||||
Toaster.clear();
|
||||
Toaster.add({ duration: 10000, content: 'Vous êtes maintenant connecté', timer: true, type: 'success' });
|
||||
useRouter().push({ name: 'user-profile' });
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
handleErrors(data.error);
|
||||
}
|
||||
}
|
||||
function handleErrors(error: Error | ZodError)
|
||||
{
|
||||
if(!error)
|
||||
return;
|
||||
|
||||
status.value = 'error';
|
||||
|
||||
if(error.hasOwnProperty('issues'))
|
||||
{
|
||||
for(const err of (error as ZodError).issues)
|
||||
{
|
||||
return Toaster.add({ content: err.message, timer: true, duration: 10000, type: 'error' });
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return Toaster.add({ content: error?.message ?? 'Une erreur est survenue', timer: true, duration: 10000, type: 'error' });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
83
app/pages/user/profile.vue
Normal file
83
app/pages/user/profile.vue
Normal file
@@ -0,0 +1,83 @@
|
||||
<script setup lang="ts">
|
||||
import { hasPermissions } from "#shared/auth.util";
|
||||
import { Toaster } from '#shared/components.util';
|
||||
|
||||
definePageMeta({
|
||||
guestsGoesTo: '/user/login',
|
||||
})
|
||||
const { user, clear } = useUserSession();
|
||||
const loading = ref<boolean>(false);
|
||||
|
||||
async function revalidateUser()
|
||||
{
|
||||
loading.value = true;
|
||||
await $fetch(`/api/users/${user.value?.id}/revalidate`, {
|
||||
method: 'post'
|
||||
});
|
||||
loading.value = false;
|
||||
Toaster.add({ closeable: false, duration: 10000, timer: true, content: 'Un mail vous a été envoyé.', type: 'info' });
|
||||
}
|
||||
async function deleteUser()
|
||||
{
|
||||
loading.value = true;
|
||||
await $fetch(`/api/users/${user.value?.id}`, {
|
||||
method: 'delete'
|
||||
});
|
||||
loading.value = false;
|
||||
clear();
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
||||
<Head>
|
||||
<Title>d[any] - Mon profil</Title>
|
||||
</Head>
|
||||
<div class="grid lg:grid-cols-4 grid-col-2 w-full items-start py-8 gap-6 content-start" v-if="user">
|
||||
<div class="flex flex-col gap-4 col-span-4 lg:col-span-3 border border-light-35 dark:border-dark-35 p-4">
|
||||
<div class="flex gap-4">
|
||||
<Avatar icon="radix-icons:person" :src="`/users/${user?.id}.medium.jpg`" class="w-32 h-32" />
|
||||
<div class="flex flex-col items-start">
|
||||
<h4 class="text-xl font-bold">{{ user.username }}</h4>
|
||||
<h4 class="text-xl font-bold">{{ user.email }}</h4>
|
||||
</div>
|
||||
</div>
|
||||
<div class="border-light-red dark:border-dark-red bg-light-redBack dark:bg-dark-redBack text-light-red dark:text-dark-red py-1 px-3 flex items-center justify-between flex-col md:flex-row"
|
||||
v-if="user.state === 0">
|
||||
<HoverCard>
|
||||
<template v-slot:default>Votre adresse mail n'as pas encore été validée. </template>
|
||||
<template v-slot:content><span>Tant que votre adresse mail n'as pas été validée, vous n'avez que
|
||||
des droits de lecture.</span></template>
|
||||
</HoverCard>
|
||||
<Button class="ms-4" @click="revalidateUser" :loading="loading">Renvoyez un mail</Button>
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col self-center flex-1 gap-4">
|
||||
<Button @click="clear">Se deconnecter</Button>
|
||||
<NuxtLink :to="{ name: 'user-changing-password' }" class="flex flex-1"><Button>Modifier mon mot de passe</Button></NuxtLink>
|
||||
<AlertDialogRoot>
|
||||
<AlertDialogTrigger asChild><Button :loading="loading"
|
||||
class="border-light-red dark:border-dark-red hover:border-light-red dark:hover:border-dark-red hover:bg-light-redBack dark:hover:bg-dark-redBack text-light-red dark:text-dark-red focus:shadow-light-red dark:focus:shadow-dark-red">Supprimer
|
||||
mon compte</Button></AlertDialogTrigger>
|
||||
<AlertDialogPortal>
|
||||
<AlertDialogOverlay class="bg-light-0 dark:bg-dark-0 opacity-70 fixed inset-0 z-40" />
|
||||
<AlertDialogContent
|
||||
class="data-[state=open]:animate-contentShow fixed top-[50%] left-[50%] max-h-[85vh] w-[90vw] max-w-[800px] translate-x-[-50%] translate-y-[-50%] bg-light-10 dark:bg-dark-10 border border-light-30 dark:border-dark-30 p-6 z-50 text-light-100 dark:text-dark-100">
|
||||
<AlertDialogTitle class="text-3xl font-light relative -top-2">Suppression de compte
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription class="text-base pb-2">Supprimer votre compte va supprimer toutes vos
|
||||
données personnelles de notre base de données, incluant vos commentaires et vos
|
||||
contributions. <br />
|
||||
Êtes vous sûr de vouloir supprimer votre compte ?
|
||||
</AlertDialogDescription>
|
||||
<div class="flex flex-1 justify-end gap-4">
|
||||
<AlertDialogCancel asChild><Button>Annuler</Button></AlertDialogCancel>
|
||||
<AlertDialogAction asChild><Button @click="() => deleteUser()" class="border-light-red dark:border-dark-red hover:border-light-red dark:hover:border-dark-red hover:bg-light-redBack dark:hover:bg-dark-redBack text-light-red dark:text-dark-red focus:shadow-light-red dark:focus:shadow-dark-red">Supprimer</Button></AlertDialogAction>
|
||||
</div>
|
||||
</AlertDialogContent>
|
||||
</AlertDialogPortal>
|
||||
</AlertDialogRoot>
|
||||
<NuxtLink v-if="hasPermissions(user.permissions, ['admin'])" :href="{ name: 'admin' }" class="flex" no-prefetch><Button class="flex-1">Administration</Button></NuxtLink>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
119
app/pages/user/register.vue
Normal file
119
app/pages/user/register.vue
Normal file
@@ -0,0 +1,119 @@
|
||||
<template>
|
||||
<Head>
|
||||
<Title>d[any] - Inscription</Title>
|
||||
</Head>
|
||||
<div class="flex flex-1 flex-col justify-center items-center">
|
||||
<div class="flex gap-8 items-center">
|
||||
<span class="border border-transparent hover:border-light-35 dark:hover:border-dark-35 p-1 cursor-pointer" @click="() => $router.go(-1)"><Icon icon="radix-icons:arrow-left" class="text-light-50 dark:text-dark-50 w-6 h-6"/></span>
|
||||
<h4 class="text-xl font-bold">Inscription</h4>
|
||||
</div>
|
||||
<form @submit.prevent="() => submit()" class="grid flex-1 p-4 grid-cols-2 md:grid-cols-1 gap-4 md:gap-0">
|
||||
<TextInput type="text" label="Nom d'utilisateur" name="username" autocomplete="username" v-model="state.username" class="w-full md:w-auto"/>
|
||||
<TextInput type="email" label="Email" name="email" autocomplete="email" v-model="state.email" class="w-full md:w-auto"/>
|
||||
<TextInput type="password" label="Mot de passe" name="password" autocomplete="new-password" v-model="state.password" class="w-full md:w-auto"/>
|
||||
<div class="grid grid-cols-2 flex-col font-light border border-light-35 dark:border-dark-35 px-4 py-2 m-4 ms-0 text-sm leading-[18px] lg:text-base order-8 col-span-2 md:col-span-1 md:order-none">
|
||||
<span class="col-span-2">Prérequis de sécurité</span>
|
||||
<span class="ps-4 flex items-center gap-2" :class="{'text-light-red dark:text-dark-red': !checkedLength}"><Icon v-show="!checkedLength" icon="radix-icons:cross-2" />8 à 128 caractères</span>
|
||||
<span class="ps-4 flex items-center gap-2" :class="{'text-light-red dark:text-dark-red': !checkedLower}"><Icon v-show="!checkedLower" icon="radix-icons:cross-2" />Une minuscule</span>
|
||||
<span class="ps-4 flex items-center gap-2" :class="{'text-light-red dark:text-dark-red': !checkedUpper}"><Icon v-show="!checkedUpper" icon="radix-icons:cross-2" />Une majuscule</span>
|
||||
<span class="ps-4 flex items-center gap-2" :class="{'text-light-red dark:text-dark-red': !checkedDigit}"><Icon v-show="!checkedDigit" icon="radix-icons:cross-2" />Un chiffre</span>
|
||||
<span class="ps-4 flex items-center gap-2" :class="{'text-light-red dark:text-dark-red': !checkedSymbol}"><Icon v-show="!checkedSymbol" icon="radix-icons:cross-2" />Un caractère special</span>
|
||||
</div>
|
||||
<TextInput type="password" label="Confirmation du mot de passe" autocomplete="new-password" v-model="confirmPassword" class="w-full md:w-auto"/>
|
||||
<Label class="pb-2 col-span-2 md:col-span-1 flex flex-row gap-2 items-center"><CheckboxRoot v-model:checked="agreeOnRules" class="border border-light-35 dark:border-dark-35 hover:border-light-50 dark:hover:border-dark-50 w-5 h-5" ><CheckboxIndicator ><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span>J'ai lu et j'accepte les <NuxtLink class="text-accent-blue cursor-pointer" :to="{ name: 'usage' }" target="_blank">conditions d'utilisation</NuxtLink></span></Label>
|
||||
<Button type="submit" class="border border-light-35 dark:border-dark-35 max-w-48 w-full order-9 col-span-2 md:col-span-1 m-auto" :loading="status === 'pending'">S'inscrire</Button>
|
||||
<span class="mt-4 order-10 flex justify-center items-center gap-4 col-span-2 md:col-span-1 m-auto">Vous avez déjà un compte ?<NuxtLink class="text-center block text-sm font-semibold tracking-wide hover:text-accent-blue" :to="{ name: 'user-login' }">Se connecter</NuxtLink></span>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ZodError } from 'zod/v4';
|
||||
import { schema, type Registration } from '~/schemas/registration';
|
||||
import { Icon } from '@iconify/vue';
|
||||
import { Toaster } from '#shared/components.util';
|
||||
|
||||
definePageMeta({
|
||||
layout: 'login',
|
||||
usersGoesTo: '/user/profile',
|
||||
});
|
||||
|
||||
const state = reactive<Registration>({
|
||||
username: '',
|
||||
email: '',
|
||||
password: ''
|
||||
});
|
||||
|
||||
const confirmPassword = ref("");
|
||||
|
||||
const checkedLength = computed(() => state.password.length >= 8 && state.password.length <= 128);
|
||||
const checkedLower = computed(() => state.password.toUpperCase() !== state.password);
|
||||
const checkedUpper = computed(() => state.password.toLowerCase() !== state.password);
|
||||
const checkedDigit = computed(() => /[0-9]/.test(state.password));
|
||||
const checkedSymbol = computed(() => " !\"#$%&'()*+,-./:;<=>?@[]^_`{|}~".split("").some(e => state.password.includes(e)));
|
||||
const agreeOnRules = ref<boolean>(false);
|
||||
|
||||
const { data: result, status, error, refresh } = await useFetch('/api/auth/register', {
|
||||
body: state,
|
||||
immediate: false,
|
||||
method: 'POST',
|
||||
watch: false,
|
||||
ignoreResponseError: true,
|
||||
});
|
||||
|
||||
async function submit()
|
||||
{
|
||||
if(state.username === '')
|
||||
return Toaster.add({ content: 'Veuillez saisir un nom d\'utilisateur', timer: true, duration: 10000 });
|
||||
if(state.email === '')
|
||||
return Toaster.add({ content: 'Veuillez saisir une adresse mail', timer: true, duration: 10000 });
|
||||
if(state.password === "")
|
||||
return Toaster.add({ content: 'Veuillez saisir un mot de passe', timer: true, duration: 10000 });
|
||||
if(state.password !== confirmPassword.value)
|
||||
return Toaster.add({ content: 'Les deux mots de passe saisis ne correspondent pas', timer: true, duration: 10000 });
|
||||
if(agreeOnRules.value !== true)
|
||||
return Toaster.add({ content: 'Veuillez accepter des conditions d\'utilisations pour vous inscrire', timer: true, duration: 10000 });
|
||||
|
||||
const data = schema.safeParse(state);
|
||||
|
||||
if(data.success)
|
||||
{
|
||||
await refresh();
|
||||
|
||||
const login = result.value;
|
||||
if(!login || !login.success)
|
||||
{
|
||||
handleErrors(login?.error ?? error.value!);
|
||||
}
|
||||
else if(status.value === 'success' && login.success)
|
||||
{
|
||||
Toaster.clear();
|
||||
Toaster.add({ duration: 10000, content: 'Vous avez été enregistré. Pensez à valider votre adresse mail.', timer: true, type: 'success' });
|
||||
await navigateTo('/user/profile');
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
handleErrors(data.error);
|
||||
}
|
||||
}
|
||||
function handleErrors(error: Error | ZodError)
|
||||
{
|
||||
if(!error)
|
||||
return;
|
||||
|
||||
status.value = 'error';
|
||||
|
||||
if(error.hasOwnProperty('issues'))
|
||||
{
|
||||
for(const err of (error as ZodError).issues)
|
||||
{
|
||||
return Toaster.add({ content: err.message, timer: true, duration: 10000, type: 'error' });
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
return Toaster.add({ content: error?.message ?? 'Une erreur est survenue', timer: true, duration: 10000, type: 'error' });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
Reference in New Issue
Block a user