Merge branch 'dev' of https://git.peaceultime.com/peaceultime/obsidian-visualiser into dev
This commit is contained in:
commit
cb2c19fada
BIN
db.sqlite-shm
BIN
db.sqlite-shm
Binary file not shown.
BIN
db.sqlite-wal
BIN
db.sqlite-wal
Binary file not shown.
|
|
@ -48,6 +48,11 @@ export const explorerContentTable = sqliteTable("explorer_content", {
|
||||||
timestamp: int({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
timestamp: int({ mode: 'timestamp' }).notNull().$defaultFn(() => new Date()),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const emailValidationTable = sqliteTable("email_validation", {
|
||||||
|
id: text().primaryKey(),
|
||||||
|
timestamp: int({ mode: 'timestamp' }).notNull(),
|
||||||
|
})
|
||||||
|
|
||||||
export const usersRelation = relations(usersTable, ({ one, many }) => ({
|
export const usersRelation = relations(usersTable, ({ one, many }) => ({
|
||||||
data: one(usersDataTable, { fields: [usersTable.id], references: [usersDataTable.id], }),
|
data: one(usersDataTable, { fields: [usersTable.id], references: [usersDataTable.id], }),
|
||||||
session: many(userSessionsTable),
|
session: many(userSessionsTable),
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,4 @@
|
||||||
|
CREATE TABLE `email_validation` (
|
||||||
|
`id` text PRIMARY KEY NOT NULL,
|
||||||
|
`timestamp` integer NOT NULL
|
||||||
|
);
|
||||||
|
|
@ -0,0 +1,359 @@
|
||||||
|
{
|
||||||
|
"version": "6",
|
||||||
|
"dialect": "sqlite",
|
||||||
|
"id": "a2731c1f-4150-4423-946e-670d794f8961",
|
||||||
|
"prevId": "b6acf5d6-d8df-4308-8d4d-55c25741cc4f",
|
||||||
|
"tables": {
|
||||||
|
"email_validation": {
|
||||||
|
"name": "email_validation",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"name": "timestamp",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"explorer_content": {
|
||||||
|
"name": "explorer_content",
|
||||||
|
"columns": {
|
||||||
|
"path": {
|
||||||
|
"name": "path",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"owner": {
|
||||||
|
"name": "owner",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"title": {
|
||||||
|
"name": "title",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"name": "type",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"content": {
|
||||||
|
"name": "content",
|
||||||
|
"type": "blob",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": false,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"navigable": {
|
||||||
|
"name": "navigable",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": true
|
||||||
|
},
|
||||||
|
"private": {
|
||||||
|
"name": "private",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": false
|
||||||
|
},
|
||||||
|
"order": {
|
||||||
|
"name": "order",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"visit": {
|
||||||
|
"name": "visit",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"name": "timestamp",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"explorer_content_owner_users_id_fk": {
|
||||||
|
"name": "explorer_content_owner_users_id_fk",
|
||||||
|
"tableFrom": "explorer_content",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"owner"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "cascade"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"user_permissions": {
|
||||||
|
"name": "user_permissions",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"permission": {
|
||||||
|
"name": "permission",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"user_permissions_id_users_id_fk": {
|
||||||
|
"name": "user_permissions_id_users_id_fk",
|
||||||
|
"tableFrom": "user_permissions",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "cascade"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"user_permissions_id_permission_pk": {
|
||||||
|
"columns": [
|
||||||
|
"id",
|
||||||
|
"permission"
|
||||||
|
],
|
||||||
|
"name": "user_permissions_id_permission_pk"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"user_sessions": {
|
||||||
|
"name": "user_sessions",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"user_id": {
|
||||||
|
"name": "user_id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"timestamp": {
|
||||||
|
"name": "timestamp",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"user_sessions_user_id_users_id_fk": {
|
||||||
|
"name": "user_sessions_user_id_users_id_fk",
|
||||||
|
"tableFrom": "user_sessions",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "cascade"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {
|
||||||
|
"user_sessions_id_user_id_pk": {
|
||||||
|
"columns": [
|
||||||
|
"id",
|
||||||
|
"user_id"
|
||||||
|
],
|
||||||
|
"name": "user_sessions_id_user_id_pk"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"users_data": {
|
||||||
|
"name": "users_data",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"signin": {
|
||||||
|
"name": "signin",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"lastTimestamp": {
|
||||||
|
"name": "lastTimestamp",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"logCount": {
|
||||||
|
"name": "logCount",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {},
|
||||||
|
"foreignKeys": {
|
||||||
|
"users_data_id_users_id_fk": {
|
||||||
|
"name": "users_data_id_users_id_fk",
|
||||||
|
"tableFrom": "users_data",
|
||||||
|
"tableTo": "users",
|
||||||
|
"columnsFrom": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"columnsTo": [
|
||||||
|
"id"
|
||||||
|
],
|
||||||
|
"onDelete": "cascade",
|
||||||
|
"onUpdate": "cascade"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
},
|
||||||
|
"users": {
|
||||||
|
"name": "users",
|
||||||
|
"columns": {
|
||||||
|
"id": {
|
||||||
|
"name": "id",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": true,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": true
|
||||||
|
},
|
||||||
|
"username": {
|
||||||
|
"name": "username",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"email": {
|
||||||
|
"name": "email",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"hash": {
|
||||||
|
"name": "hash",
|
||||||
|
"type": "text",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false
|
||||||
|
},
|
||||||
|
"state": {
|
||||||
|
"name": "state",
|
||||||
|
"type": "integer",
|
||||||
|
"primaryKey": false,
|
||||||
|
"notNull": true,
|
||||||
|
"autoincrement": false,
|
||||||
|
"default": 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"indexes": {
|
||||||
|
"users_username_unique": {
|
||||||
|
"name": "users_username_unique",
|
||||||
|
"columns": [
|
||||||
|
"username"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
},
|
||||||
|
"users_email_unique": {
|
||||||
|
"name": "users_email_unique",
|
||||||
|
"columns": [
|
||||||
|
"email"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
},
|
||||||
|
"users_hash_unique": {
|
||||||
|
"name": "users_hash_unique",
|
||||||
|
"columns": [
|
||||||
|
"hash"
|
||||||
|
],
|
||||||
|
"isUnique": true
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"foreignKeys": {},
|
||||||
|
"compositePrimaryKeys": {},
|
||||||
|
"uniqueConstraints": {},
|
||||||
|
"checkConstraints": {}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"views": {},
|
||||||
|
"enums": {},
|
||||||
|
"_meta": {
|
||||||
|
"schemas": {},
|
||||||
|
"tables": {},
|
||||||
|
"columns": {}
|
||||||
|
},
|
||||||
|
"internal": {
|
||||||
|
"indexes": {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -36,6 +36,13 @@
|
||||||
"when": 1732722840534,
|
"when": 1732722840534,
|
||||||
"tag": "0004_ancient_thunderball",
|
"tag": "0004_ancient_thunderball",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 5,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1734426608563,
|
||||||
|
"tag": "0005_panoramic_slayback",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
@ -153,7 +153,7 @@ export default defineNuxtConfig({
|
||||||
xssValidator: false,
|
xssValidator: false,
|
||||||
},
|
},
|
||||||
sitemap: {
|
sitemap: {
|
||||||
exclude: ['/admin/**', '/explore/edit', '/user/mailvalidated'],
|
exclude: ['/admin/**', '/explore/edit', '/user/mailvalidated', '/user/changing-password', '/user/reset-password'],
|
||||||
sources: ['/api/__sitemap__/urls']
|
sources: ['/api/__sitemap__/urls']
|
||||||
},
|
},
|
||||||
experimental: {
|
experimental: {
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
<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>
|
||||||
|
<ProseH4>Reinitialisation de mon mot de passe</ProseH4>
|
||||||
|
</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/dist/iconify.js';
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'login',
|
||||||
|
usersGoesTo: '/user/profile',
|
||||||
|
});
|
||||||
|
|
||||||
|
const toaster = useToast();
|
||||||
|
|
||||||
|
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>
|
||||||
|
|
@ -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>
|
||||||
|
<ProseH4>Reinitialisation de mon mot de passe</ProseH4>
|
||||||
|
</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/dist/iconify.js';
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'login',
|
||||||
|
usersGoesTo: '/user/login',
|
||||||
|
});
|
||||||
|
|
||||||
|
const query = useRouter().currentRoute.value.query;
|
||||||
|
|
||||||
|
const toaster = useToast();
|
||||||
|
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>
|
||||||
|
|
@ -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>
|
||||||
|
<ProseH4>Modification de mon mot de passe</ProseH4>
|
||||||
|
</div>
|
||||||
|
<form @submit.prevent="submit" class="flex flex-1 flex-col justify-center items-stretch">
|
||||||
|
<TextInput type="password" label="Ancien mot de passe" autocomplete="currentPassword" v-model="oldPasswd"/>
|
||||||
|
<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'">Mettre à jour mon mot de passe</Button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { Icon } from '@iconify/vue/dist/iconify.js';
|
||||||
|
|
||||||
|
definePageMeta({
|
||||||
|
layout: 'login',
|
||||||
|
guestsGoesTo: '/user/login',
|
||||||
|
});
|
||||||
|
|
||||||
|
const toaster = useToast();
|
||||||
|
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>
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
<TextInput type="text" label="Utilisateur ou email" autocomplete="username" v-model="state.usernameOrEmail"/>
|
<TextInput type="text" label="Utilisateur ou email" autocomplete="username" v-model="state.usernameOrEmail"/>
|
||||||
<TextInput type="password" label="Mot de passe" autocomplete="current-password" v-model="state.password"/>
|
<TextInput type="password" label="Mot de passe" autocomplete="current-password" v-model="state.password"/>
|
||||||
<Button class="border border-light-35 dark:border-dark-35 self-center" :loading="status === 'pending'">Se connecter</Button>
|
<Button 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>
|
<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>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@ async function revalidateUser()
|
||||||
{
|
{
|
||||||
loading.value = true;
|
loading.value = true;
|
||||||
await $fetch(`/api/users/${user.value?.id}/revalidate`, {
|
await $fetch(`/api/users/${user.value?.id}/revalidate`, {
|
||||||
method: 'get'
|
method: 'post'
|
||||||
});
|
});
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
toaster.add({ closeable: false, duration: 10000, timer: true, content: 'Un mail vous a été envoyé.', type: 'info' });
|
toaster.add({ closeable: false, duration: 10000, timer: true, content: 'Un mail vous a été envoyé.', type: 'info' });
|
||||||
|
|
@ -54,7 +54,7 @@ async function deleteUser()
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col self-center flex-1 gap-4">
|
<div class="flex flex-col self-center flex-1 gap-4">
|
||||||
<Button @click="clear">Se deconnecter</Button>
|
<Button @click="clear">Se deconnecter</Button>
|
||||||
<Button disabled><Tooltip message="En cours de développement">Modifier mon profil</Tooltip></Button>
|
<NuxtLink :to="{ name: 'user-changing-password' }" class="flex flex-1"><Button>Modifier mon mot de passe</Button></NuxtLink>
|
||||||
<AlertDialogRoot>
|
<AlertDialogRoot>
|
||||||
<AlertDialogTrigger asChild><Button :loading="loading"
|
<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
|
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
|
||||||
|
|
|
||||||
|
|
@ -4,9 +4,9 @@ import useDatabase from '~/composables/useDatabase';
|
||||||
|
|
||||||
export default defineSitemapEventHandler(() => {
|
export default defineSitemapEventHandler(() => {
|
||||||
const db = useDatabase();
|
const db = useDatabase();
|
||||||
const pages = db.select({ path: explorerContentTable.path, lastMod: explorerContentTable.timestamp, navigable: explorerContentTable.navigable, private: explorerContentTable.private }).from(explorerContentTable).all();
|
const pages = db.select({ path: explorerContentTable.path, lastMod: explorerContentTable.timestamp, navigable: explorerContentTable.navigable, private: explorerContentTable.private, type: explorerContentTable.type }).from(explorerContentTable).all();
|
||||||
|
|
||||||
return pages.filter(e => e.navigable && !e.private && e.path.split('/').map((_, i, a) => a.slice(0, i).join('/')).every(p => !pages.find(_p => _p.path === p)?.private)).map(e => ({
|
return pages.filter(e => e.type !== 'folder' && e.navigable && !e.private && e.path.split('/').map((_, i, a) => a.slice(0, i).join('/')).every(p => !pages.find(_p => _p.path === p)?.private)).map(e => ({
|
||||||
loc: `/explore/${encodeURIComponent(e.path)}`,
|
loc: `/explore/${encodeURIComponent(e.path)}`,
|
||||||
lastmod: e.lastMod,
|
lastmod: e.lastMod,
|
||||||
})) satisfies SitemapUrlInput[];
|
})) satisfies SitemapUrlInput[];
|
||||||
|
|
|
||||||
|
|
@ -71,18 +71,27 @@ export default defineEventHandler(async (e): Promise<Return> => {
|
||||||
|
|
||||||
db.insert(usersDataTable).values({ id: sql.placeholder('id') }).prepare().run({ id: id.id });
|
db.insert(usersDataTable).values({ id: sql.placeholder('id') }).prepare().run({ id: id.id });
|
||||||
|
|
||||||
logSession(e, await setUserSession(e, { user: { id: id.id, username: body.data.username, email: body.data.email, state: 0, signin: new Date(), permissions: [] } }) as UserSessionRequired);
|
logSession(e, await setUserSession(e, { user: { id: id.id, username: body.data.username, email: body.data.email, state: 0, signin: new Date(), permissions: [], lastTimestamp: new Date(), logCount: 1 } }) as UserSessionRequired);
|
||||||
|
|
||||||
|
const emailId = Bun.hash('register' + id.id + hash, Date.now());
|
||||||
|
const timestamp = Date.now() + 1000 * 60 * 60;
|
||||||
|
|
||||||
|
await runTask('validation', {
|
||||||
|
payload: {
|
||||||
|
type: 'validation',
|
||||||
|
id: emailId, timestamp,
|
||||||
|
}
|
||||||
|
});
|
||||||
await runTask('mail', {
|
await runTask('mail', {
|
||||||
payload: {
|
payload: {
|
||||||
type: 'mail',
|
type: 'mail',
|
||||||
to: [body.data.email],
|
to: [body.data.email],
|
||||||
template: 'registration',
|
template: 'registration',
|
||||||
data: {
|
data: {
|
||||||
|
id: emailId, timestamp,
|
||||||
|
userId: id,
|
||||||
username: body.data.username,
|
username: body.data.username,
|
||||||
timestamp: Date.now(),
|
},
|
||||||
id: id.id,
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
import { hash } from 'bun';
|
||||||
|
import { eq, or } from 'drizzle-orm';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import useDatabase from '~/composables/useDatabase';
|
||||||
|
import { usersTable } from '~/db/schema';
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
profile: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default defineEventHandler(async (e) => {
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const db = useDatabase();
|
||||||
|
const body = await readValidatedBody(e, schema.safeParse);
|
||||||
|
|
||||||
|
if (!body.success)
|
||||||
|
{
|
||||||
|
setResponseStatus(e, 406);
|
||||||
|
return { success: false, error: body.error };
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = db.select({ id: usersTable.id, email: usersTable.email, username: usersTable.username, hash: usersTable.hash }).from(usersTable).where(or(eq(usersTable.email, body.data.profile), eq(usersTable.username, body.data.profile))).get();
|
||||||
|
|
||||||
|
if(result && result.id)
|
||||||
|
{
|
||||||
|
const id = hash('reset' + result.id + result.hash, Date.now());
|
||||||
|
const timestamp = Date.now() + 1000 * 60 * 60;
|
||||||
|
await runTask('validation', {
|
||||||
|
payload: {
|
||||||
|
type: 'validation',
|
||||||
|
id, timestamp,
|
||||||
|
}
|
||||||
|
});
|
||||||
|
await runTask('mail', {
|
||||||
|
payload: {
|
||||||
|
type: 'mail',
|
||||||
|
data: {
|
||||||
|
id, timestamp,
|
||||||
|
userId: result.id,
|
||||||
|
username: result.username,
|
||||||
|
},
|
||||||
|
template: 'reset-password',
|
||||||
|
to: [result.email],
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(err: any)
|
||||||
|
{
|
||||||
|
console.error(err);
|
||||||
|
|
||||||
|
return { success: false, error: err as Error };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,99 @@
|
||||||
|
import { count, eq, sql } from 'drizzle-orm';
|
||||||
|
import { ZodError, type ZodIssue } from 'zod';
|
||||||
|
import useDatabase from '~/composables/useDatabase';
|
||||||
|
import { usersDataTable, usersTable } from '~/db/schema';
|
||||||
|
import { schema } from '~/schemas/registration';
|
||||||
|
import { checkSession, logSession } from '~/server/utils/user';
|
||||||
|
import type { UserSession, UserSessionRequired } from '~/types/auth';
|
||||||
|
|
||||||
|
interface SuccessHandler
|
||||||
|
{
|
||||||
|
success: true;
|
||||||
|
session: UserSession;
|
||||||
|
}
|
||||||
|
interface ErrorHandler
|
||||||
|
{
|
||||||
|
success: false;
|
||||||
|
error: Error | ZodError<{
|
||||||
|
username: string;
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
type Return = SuccessHandler | ErrorHandler;
|
||||||
|
|
||||||
|
export default defineEventHandler(async (e): Promise<Return> => {
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const session = await getUserSession(e);
|
||||||
|
const db = useDatabase();
|
||||||
|
|
||||||
|
const checkedSession = await checkSession(e, session);
|
||||||
|
|
||||||
|
if(checkedSession !== undefined)
|
||||||
|
return checkedSession;
|
||||||
|
|
||||||
|
const body = await readValidatedBody(e, schema.safeParse);
|
||||||
|
|
||||||
|
if (!body.success)
|
||||||
|
{
|
||||||
|
await clearUserSession(e);
|
||||||
|
|
||||||
|
setResponseStatus(e, 406);
|
||||||
|
return { success: false, error: body.error };
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkUsername = db.select({ count: count() }).from(usersTable).where(eq(usersTable.username, sql.placeholder('username'))).prepare().get({ username: body.data.username });
|
||||||
|
const checkEmail = db.select({ count: count() }).from(usersTable).where(eq(usersTable.email, sql.placeholder('email'))).prepare().get({ email: body.data.email });
|
||||||
|
|
||||||
|
const errors: ZodIssue[] = [];
|
||||||
|
if(!checkUsername || checkUsername.count !== 0)
|
||||||
|
errors.push({ code: 'custom', path: ['username'], message: "Ce nom d'utilisateur est déjà utilisé" });
|
||||||
|
if(!checkEmail || checkEmail.count !== 0)
|
||||||
|
errors.push({ code: 'custom', path: ['email'], message: "Cette adresse mail est déjà utilisée" });
|
||||||
|
|
||||||
|
if(errors.length > 0)
|
||||||
|
{
|
||||||
|
setResponseStatus(e, 406);
|
||||||
|
return { success: false, error: new ZodError(errors) };
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
const hash = await Bun.password.hash(body.data.password);
|
||||||
|
db.insert(usersTable).values({ username: sql.placeholder('username'), email: sql.placeholder('email'), hash: sql.placeholder('hash'), state: sql.placeholder('state') }).prepare().run({ username: body.data.username, email: body.data.email, hash, state: 0 });
|
||||||
|
const id = db.select({ id: usersTable.id }).from(usersTable).where(eq(usersTable.username, sql.placeholder('username'))).prepare().get({ username: body.data.username });
|
||||||
|
|
||||||
|
if(!id || !id.id)
|
||||||
|
{
|
||||||
|
setResponseStatus(e, 406);
|
||||||
|
return { success: false, error: new Error('Erreur de création de compte') };
|
||||||
|
}
|
||||||
|
|
||||||
|
db.insert(usersDataTable).values({ id: sql.placeholder('id') }).prepare().run({ id: id.id });
|
||||||
|
|
||||||
|
logSession(e, await setUserSession(e, { user: { id: id.id, username: body.data.username, email: body.data.email, state: 0, signin: new Date(), permissions: [], lastTimestamp: new Date(), logCount: 1 } }) as UserSessionRequired);
|
||||||
|
|
||||||
|
await runTask('mail', {
|
||||||
|
payload: {
|
||||||
|
type: 'mail',
|
||||||
|
to: [body.data.email],
|
||||||
|
template: 'registration',
|
||||||
|
data: {
|
||||||
|
username: body.data.username,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
id: id.id,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
setResponseStatus(e, 201);
|
||||||
|
return { success: true, session };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch(err: any)
|
||||||
|
{
|
||||||
|
console.error(err);
|
||||||
|
|
||||||
|
return { success: false, error: err as Error };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,75 @@
|
||||||
|
import { and, count, eq } from 'drizzle-orm';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import { usersTable } from '~/db/schema';
|
||||||
|
import { schema as registration } from '~/schemas/registration';
|
||||||
|
import useDatabase from '~/composables/useDatabase';
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
newPassword: registration.shape.password,
|
||||||
|
oldPassword: registration.shape.password,
|
||||||
|
});
|
||||||
|
|
||||||
|
export default defineEventHandler(async (e) => {
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const session = await getUserSession(e);
|
||||||
|
|
||||||
|
if(!session || !session.user || !session.user.id)
|
||||||
|
{
|
||||||
|
return createError({
|
||||||
|
statusCode: 401,
|
||||||
|
message: 'Unauthorized',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const id = getRouterParam(e, 'id');
|
||||||
|
|
||||||
|
if(!id)
|
||||||
|
{
|
||||||
|
return createError({
|
||||||
|
statusCode: 403,
|
||||||
|
message: 'Forbidden',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if(session.user.id.toString() !== id)
|
||||||
|
{
|
||||||
|
return createError({
|
||||||
|
statusCode: 401,
|
||||||
|
message: 'Unauthorized',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await readValidatedBody(e, schema.safeParse);
|
||||||
|
|
||||||
|
if(!body.success)
|
||||||
|
throw body.error;
|
||||||
|
|
||||||
|
const db = useDatabase();
|
||||||
|
console.log(body.data.oldPassword, await Bun.password.hash(body.data.oldPassword));
|
||||||
|
const check = db.select({ hash: usersTable.hash }).from(usersTable).where(eq(usersTable.id, session.user.id)).get();
|
||||||
|
|
||||||
|
if(!check || !check.hash)
|
||||||
|
{
|
||||||
|
return createError({
|
||||||
|
statusCode: 401,
|
||||||
|
message: 'Unauthorized',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if(!await Bun.password.verify(body.data.oldPassword, check.hash))
|
||||||
|
{
|
||||||
|
return { success: false, error: "Ancien mot de passe incorrect" };
|
||||||
|
}
|
||||||
|
|
||||||
|
db.update(usersTable).set({ hash: await Bun.password.hash(body.data.newPassword) }).where(eq(usersTable.id, session.user.id)).run();
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
catch(err: any)
|
||||||
|
{
|
||||||
|
console.error(err);
|
||||||
|
|
||||||
|
return createError({
|
||||||
|
statusCode: 500,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -0,0 +1,78 @@
|
||||||
|
import { eq, getTableColumns, lte } from 'drizzle-orm';
|
||||||
|
import { z } from 'zod';
|
||||||
|
import useDatabase from '~/composables/useDatabase';
|
||||||
|
import { emailValidationTable, usersTable } from '~/db/schema';
|
||||||
|
|
||||||
|
const querySchema = z.object({
|
||||||
|
h: z.coerce.string(),
|
||||||
|
i: z.coerce.string(),
|
||||||
|
u: z.coerce.number(),
|
||||||
|
t: z.coerce.number(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const bodySchema = z.object({
|
||||||
|
password: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default defineEventHandler(async (e) => {
|
||||||
|
try
|
||||||
|
{
|
||||||
|
const query = await getValidatedQuery(e, querySchema.safeParse);
|
||||||
|
|
||||||
|
if(!query.success)
|
||||||
|
throw query.error;
|
||||||
|
|
||||||
|
if(Bun.hash('2' + query.data.u.toString(), query.data.t).toString() !== query.data.h)
|
||||||
|
{
|
||||||
|
return createError({
|
||||||
|
statusCode: 400,
|
||||||
|
message: 'Requete incorrecte',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if(Date.now() > query.data.t + (60 * 60 * 1000))
|
||||||
|
{
|
||||||
|
return createError({
|
||||||
|
statusCode: 400,
|
||||||
|
message: 'La requete a expirée',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const body = await readValidatedBody(e, bodySchema.safeParse);
|
||||||
|
|
||||||
|
if(!body.success)
|
||||||
|
throw body.error;
|
||||||
|
|
||||||
|
const db = useDatabase();
|
||||||
|
db.delete(emailValidationTable).where(lte(emailValidationTable.timestamp, new Date())).run();
|
||||||
|
const validate = db.select(getTableColumns(emailValidationTable)).from(emailValidationTable).where(eq(emailValidationTable.id, query.data.i)).get();
|
||||||
|
|
||||||
|
if(!validate || validate.timestamp <= new Date())
|
||||||
|
{
|
||||||
|
return createError({
|
||||||
|
statusCode: 400,
|
||||||
|
message: 'La requete a expirée',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
db.delete(emailValidationTable).where(eq(emailValidationTable.id, query.data.i)).run();
|
||||||
|
const result = db.select({ state: usersTable.state }).from(usersTable).where(eq(usersTable.id, query.data.u)).get();
|
||||||
|
|
||||||
|
if(result === undefined)
|
||||||
|
{
|
||||||
|
return createError({
|
||||||
|
statusCode: 400,
|
||||||
|
message: 'Aucune donnée utilisateur trouvée',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
db.update(usersTable).set({ hash: await Bun.password.hash(body.data.password) }).where(eq(usersTable.id, query.data.u)).run();
|
||||||
|
|
||||||
|
return { success: true };
|
||||||
|
}
|
||||||
|
catch(err: any)
|
||||||
|
{
|
||||||
|
console.error(err);
|
||||||
|
|
||||||
|
return { success: false, error: err as Error };
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
import { hash } from "bun";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
import useDatabase from "~/composables/useDatabase";
|
import useDatabase from "~/composables/useDatabase";
|
||||||
import { usersTable } from "~/db/schema";
|
import { usersTable } from "~/db/schema";
|
||||||
|
|
@ -31,7 +32,7 @@ export default defineEventHandler(async (e) => {
|
||||||
}
|
}
|
||||||
|
|
||||||
const db = useDatabase();
|
const db = useDatabase();
|
||||||
const data = db.select({ state: usersTable.state }).from(usersTable).where(eq(usersTable.id, session.user.id)).get();
|
const data = db.select({ id: usersTable.id, email: usersTable.email, username: usersTable.username, hash: usersTable.hash, state: usersTable.state }).from(usersTable).where(eq(usersTable.id, session.user.id)).get();
|
||||||
|
|
||||||
if(!data)
|
if(!data)
|
||||||
{
|
{
|
||||||
|
|
@ -46,16 +47,25 @@ export default defineEventHandler(async (e) => {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const emailId = hash('register' + data.id + data.hash, Date.now());
|
||||||
|
const timestamp = Date.now() + 1000 * 60 * 60;
|
||||||
|
|
||||||
|
await runTask('validation', {
|
||||||
|
payload: {
|
||||||
|
type: 'validation',
|
||||||
|
id: emailId, timestamp,
|
||||||
|
}
|
||||||
|
});
|
||||||
await runTask('mail', {
|
await runTask('mail', {
|
||||||
payload: {
|
payload: {
|
||||||
type: 'mail',
|
type: 'mail',
|
||||||
to: [session.user.email],
|
to: [data.email],
|
||||||
template: 'registration', //@TODO
|
template: 'registration',
|
||||||
data: {
|
data: {
|
||||||
username: session.user.username,
|
id: emailId, timestamp,
|
||||||
timestamp: Date.now(),
|
userId: id,
|
||||||
id: session.user.id,
|
username: data.username,
|
||||||
}
|
},
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
@ -3,12 +3,12 @@
|
||||||
<p style="font-variant: small-caps; margin-bottom: 1rem; font-size: 1.25rem; line-height: 1.75rem;">Bienvenue sur d[any], <span>{{ username }}</span>.</p>
|
<p style="font-variant: small-caps; margin-bottom: 1rem; font-size: 1.25rem; line-height: 1.75rem;">Bienvenue sur d[any], <span>{{ username }}</span>.</p>
|
||||||
<p>Nous vous invitons à valider votre compte afin de profiter de toutes les fonctionnalités de d[any].</p>
|
<p>Nous vous invitons à valider votre compte afin de profiter de toutes les fonctionnalités de d[any].</p>
|
||||||
<div style="padding-top: 1rem; padding-bottom: 1rem; text-align: center;">
|
<div style="padding-top: 1rem; padding-bottom: 1rem; text-align: center;">
|
||||||
<a :href="`https://d-any.com/user/mailvalidation?u=${id}&t=${timestamp}&h=${hash}`" target="_blank"><span style="display: inline-block; border-width: 1px; border-color: #525252; background-color: #e5e5e5; padding-left: 0.75rem; padding-right: 0.75rem; padding-top: 0.25rem; padding-bottom: 0.25rem; font-weight: 200; color: #171717; text-decoration: none;">Je valide mon compte</span></a>
|
<a :href="`https://d-any.com/user/mailvalidation?u=${userId}&i=${id}&t=${timestamp}&h=${hash}`" target="_blank"><span style="display: inline-block; border-width: 1px; border-color: #525252; background-color: #e5e5e5; padding-left: 0.75rem; padding-right: 0.75rem; padding-top: 0.25rem; padding-bottom: 0.25rem; font-weight: 200; color: #171717; text-decoration: none;">Je valide mon compte</span></a>
|
||||||
<span style="display: block; padding-top: 0.5rem; font-size: 0.75rem; line-height: 1rem;">Ce lien est valable 1 heure.</span>
|
<span style="display: block; padding-top: 0.5rem; font-size: 0.75rem; line-height: 1rem;">Ce lien est valable 1 heure.</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<span>Vous pouvez egalement copier le lien suivant pour valider votre compte: </span>
|
<span>Vous pouvez egalement copier le lien suivant pour valider votre compte: </span>
|
||||||
<pre style="display: inline-block; border-bottom-width: 1px; font-size: 0.75rem; line-height: 1rem; color: #171717; font-weight: 400; text-decoration: none;">{{ `https://d-any.com/user/mailvalidation?u=${id}&t=${timestamp}&h=${hash}` }}</pre>
|
<pre style="display: inline-block; border-bottom-width: 1px; font-size: 0.75rem; line-height: 1rem; color: #171717; font-weight: 400; text-decoration: none;">{{ `https://d-any.com/user/mailvalidation?u=${userId}&i=${id}&t=${timestamp}&h=${hash}` }}</pre>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -17,10 +17,11 @@
|
||||||
import { computed } from 'vue';
|
import { computed } from 'vue';
|
||||||
import Bun from 'bun';
|
import Bun from 'bun';
|
||||||
|
|
||||||
const { id, username, timestamp } = defineProps<{
|
const { id, userId, username, timestamp } = defineProps<{
|
||||||
id: number
|
id: number
|
||||||
|
userId: number
|
||||||
username: string
|
username: string
|
||||||
timestamp: number
|
timestamp: number
|
||||||
}>();
|
}>();
|
||||||
const hash = computed(() => Bun.hash(id.toString(), timestamp));
|
const hash = computed(() => Bun.hash('1' + userId.toString(), timestamp));
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -0,0 +1,29 @@
|
||||||
|
<template>
|
||||||
|
<div style="max-width: 800px; margin-left: auto; margin-right: auto;">
|
||||||
|
<p style="font-variant: small-caps; margin-bottom: 1rem; font-size: 1.25rem; line-height: 1.75rem;">Bonjour <span>{{ username }}</span>.</p>
|
||||||
|
<p>Vous avez demandé à réinitialiser votre mot de passe aujourd'hui à {{ format(new Date(timestamp), 'HH:mm') }}.</p>
|
||||||
|
<div style="padding-top: 1rem; padding-bottom: 1rem; text-align: center;">
|
||||||
|
<a :href="`https://d-any.com/user/resetting-password?u=${userId}&i=${id}&t=${timestamp}&h=${hash}`" target="_blank"><span style="display: inline-block; border-width: 1px; border-color: #525252; background-color: #e5e5e5; padding-left: 0.75rem; padding-right: 0.75rem; padding-top: 0.25rem; padding-bottom: 0.25rem; font-weight: 200; color: #171717; text-decoration: none;">Je change mon mot de passe.</span></a>
|
||||||
|
<span style="display: block; padding-top: 0.5rem; font-size: 0.75rem; line-height: 1rem;">Ce lien est valable 1 heure.</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>Vous pouvez egalement copier le lien suivant pour changer votre mot de passe: </span>
|
||||||
|
<pre style="display: inline-block; border-bottom-width: 1px; font-size: 0.75rem; line-height: 1rem; color: #171717; font-weight: 400; text-decoration: none;">{{ `https://d-any.com/user/resetting-password?u=${userId}&i=${id}&t=${timestamp}&h=${hash}` }}</pre>
|
||||||
|
</div>
|
||||||
|
<span>Si vous n'êtes pas à l'origine de cette demande, vous n'avez pas à modifier votre mot de passe, ce dernier est conservé tant que vous n'interagissez pas avec le lien ci-dessus.</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import Bun from 'bun';
|
||||||
|
import { format } from '~/shared/general.utils';
|
||||||
|
|
||||||
|
const { id, userId, username, timestamp } = defineProps<{
|
||||||
|
id: number
|
||||||
|
userId: number
|
||||||
|
username: string
|
||||||
|
timestamp: number
|
||||||
|
}>();
|
||||||
|
const hash = computed(() => Bun.hash('2' + userId.toString(), timestamp));
|
||||||
|
</script>
|
||||||
|
|
@ -1,10 +1,11 @@
|
||||||
import { eq } from "drizzle-orm";
|
import { eq, getTableColumns, lte } from "drizzle-orm";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import useDatabase from "~/composables/useDatabase";
|
import useDatabase from "~/composables/useDatabase";
|
||||||
import { usersTable } from "~/db/schema";
|
import { emailValidationTable, usersTable } from "~/db/schema";
|
||||||
|
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
h: z.coerce.string(),
|
h: z.coerce.string(),
|
||||||
|
i: z.coerce.string(),
|
||||||
u: z.coerce.number(),
|
u: z.coerce.number(),
|
||||||
t: z.coerce.number(),
|
t: z.coerce.number(),
|
||||||
});
|
});
|
||||||
|
|
@ -15,22 +16,33 @@ export default defineEventHandler(async (e) => {
|
||||||
if(!query.success)
|
if(!query.success)
|
||||||
throw query.error;
|
throw query.error;
|
||||||
|
|
||||||
if(Bun.hash(query.data.u.toString(), query.data.t).toString() !== query.data.h)
|
if(Bun.hash('1' + query.data.u.toString(), query.data.t).toString() !== query.data.h)
|
||||||
{
|
{
|
||||||
return createError({
|
return createError({
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
message: 'Lien incorrect',
|
message: 'Lien incorrect',
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
if(Date.now() > query.data.t + (60 * 60 * 1000))
|
if(Date.now() > query.data.t + (60 * 60 * 1000))
|
||||||
{
|
{
|
||||||
return createError({
|
return createError({
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
message: 'Le lien a expiré',
|
message: 'Le lien a expiré',
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const db = useDatabase();
|
const db = useDatabase();
|
||||||
|
const validate = db.select(getTableColumns(emailValidationTable)).from(emailValidationTable).where(eq(emailValidationTable.id, query.data.i)).get();
|
||||||
|
|
||||||
|
if(!validate || validate.timestamp <= new Date())
|
||||||
|
{
|
||||||
|
return createError({
|
||||||
|
statusCode: 400,
|
||||||
|
message: 'Le lien a expiré',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
db.delete(emailValidationTable).where(lte(emailValidationTable.timestamp, new Date())).run();
|
||||||
const result = db.select({ state: usersTable.state }).from(usersTable).where(eq(usersTable.id, query.data.u)).get();
|
const result = db.select({ state: usersTable.state }).from(usersTable).where(eq(usersTable.id, query.data.u)).get();
|
||||||
|
|
||||||
if(result === undefined)
|
if(result === undefined)
|
||||||
|
|
@ -38,14 +50,14 @@ export default defineEventHandler(async (e) => {
|
||||||
return createError({
|
return createError({
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
message: 'Aucune donnée utilisateur trouvée',
|
message: 'Aucune donnée utilisateur trouvée',
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
if(result?.state === 1)
|
if(result?.state === 1)
|
||||||
{
|
{
|
||||||
return createError({
|
return createError({
|
||||||
statusCode: 400,
|
statusCode: 400,
|
||||||
message: 'Votre compte a déjà été validé',
|
message: 'Votre compte a déjà été validé',
|
||||||
})
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
db.update(usersTable).set({ state: 1 }).where(eq(usersTable.id, query.data.u)).run();
|
db.update(usersTable).set({ state: 1 }).where(eq(usersTable.id, query.data.u)).run();
|
||||||
|
|
|
||||||
|
|
@ -3,28 +3,24 @@ import { createSSRApp, h } from 'vue';
|
||||||
import { renderToString } from 'vue/server-renderer';
|
import { renderToString } from 'vue/server-renderer';
|
||||||
|
|
||||||
import base from '../components/mail/base.vue';
|
import base from '../components/mail/base.vue';
|
||||||
import registration from '../components/mail/registration.vue';
|
import Registration from '../components/mail/registration.vue';
|
||||||
//import revalidation from '../components/mail/revalidation.vue';
|
import ResetPassword from '../components/mail/reset-password.vue';
|
||||||
|
|
||||||
const config = useRuntimeConfig();
|
const config = useRuntimeConfig();
|
||||||
const [domain, selector, dkim] = config.mail.dkim.split(":");
|
const [domain, selector, dkim] = config.mail.dkim.split(":");
|
||||||
|
|
||||||
export const templates: Record<string, { component: any, subject: string }> = {
|
export const templates: Record<string, { component: any, subject: string }> = {
|
||||||
"registration": { component: registration, subject: 'Bienvenue sur d[any] 😎' },
|
"registration": { component: Registration, subject: 'Bienvenue sur d[any] 😎' },
|
||||||
// "revalidate-mail": { component: revalidation, subject: 'd[any]: Valider votre email' },
|
"reset-password": { component: ResetPassword, subject: 'Réinitialisation de votre mot de passe' },
|
||||||
};
|
};
|
||||||
|
|
||||||
import 'nitropack/types';
|
|
||||||
import type Mail from 'nodemailer/lib/mailer';
|
import type Mail from 'nodemailer/lib/mailer';
|
||||||
declare module 'nitropack/types'
|
interface MailPayload
|
||||||
{
|
{
|
||||||
interface TaskPayload
|
type: 'mail'
|
||||||
{
|
to: string[]
|
||||||
type: 'mail'
|
template: string
|
||||||
to: string[]
|
data: Record<string, any>
|
||||||
template: string
|
|
||||||
data: Record<string, any>
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const transport = nodemailer.createTransport({
|
const transport = nodemailer.createTransport({
|
||||||
|
|
@ -32,7 +28,7 @@ const transport = nodemailer.createTransport({
|
||||||
pool: true,
|
pool: true,
|
||||||
host: config.mail.host,
|
host: config.mail.host,
|
||||||
port: config.mail.port,
|
port: config.mail.port,
|
||||||
secure: false,
|
secure: true,
|
||||||
auth: {
|
auth: {
|
||||||
user: config.mail.user,
|
user: config.mail.user,
|
||||||
pass: config.mail.passwd,
|
pass: config.mail.passwd,
|
||||||
|
|
@ -57,7 +53,7 @@ export default defineTask({
|
||||||
throw new Error(`Données inconnues`);
|
throw new Error(`Données inconnues`);
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload = e.payload;
|
const payload = e.payload as MailPayload;
|
||||||
const template = templates[payload.template];
|
const template = templates[payload.template];
|
||||||
|
|
||||||
if(!template)
|
if(!template)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,39 @@
|
||||||
|
import { lt } from "drizzle-orm";
|
||||||
|
import { emailValidationTable } from "~/db/schema";
|
||||||
|
import useDatabase from '~/composables/useDatabase';
|
||||||
|
|
||||||
|
interface ValidationPayload
|
||||||
|
{
|
||||||
|
type: 'validation'
|
||||||
|
id: string
|
||||||
|
timestamp: number
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defineTask({
|
||||||
|
meta: {
|
||||||
|
name: 'validation',
|
||||||
|
description: 'Add email ID to DB',
|
||||||
|
},
|
||||||
|
async run(e) {
|
||||||
|
try {
|
||||||
|
if(e.payload.type !== 'validation')
|
||||||
|
{
|
||||||
|
throw new Error(`Données inconnues`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = e.payload as ValidationPayload;
|
||||||
|
const db = useDatabase();
|
||||||
|
|
||||||
|
db.delete(emailValidationTable).where(lt(emailValidationTable.timestamp, new Date())).run();
|
||||||
|
db.insert(emailValidationTable).values({ id: payload.id, timestamp: new Date(payload.timestamp) }).run();
|
||||||
|
|
||||||
|
return { result: true };
|
||||||
|
}
|
||||||
|
catch(e)
|
||||||
|
{
|
||||||
|
console.error(e);
|
||||||
|
|
||||||
|
return { result: false, error: e };
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
@ -0,0 +1,9 @@
|
||||||
|
- [x] Mot de passe oublié
|
||||||
|
- [x] Rename auto des liens au changement de path
|
||||||
|
- [ ] Autocomplete des liens dans l'editeur
|
||||||
|
- [ ] Editeur de graphe
|
||||||
|
- [ ] Filtrage de lien avec le header id
|
||||||
|
- [ ] Embed de lien (le ![[]] de Obsidian)
|
||||||
|
- [ ] Rework la structure projet
|
||||||
|
- [ ] Limite de taille par projet (100 Mo ?)
|
||||||
|
- [ ] Création de projet (!!! limité à 1 par personne au début)
|
||||||
Loading…
Reference in New Issue