diff --git a/db.sqlite b/db.sqlite index 36ccc3d..ba7a31e 100644 Binary files a/db.sqlite and b/db.sqlite differ diff --git a/db.sqlite-shm b/db.sqlite-shm index e516319..6a642b2 100644 Binary files a/db.sqlite-shm and b/db.sqlite-shm differ diff --git a/db.sqlite-wal b/db.sqlite-wal index de76773..09ce120 100644 Binary files a/db.sqlite-wal and b/db.sqlite-wal differ diff --git a/db/schema.ts b/db/schema.ts index 88e8078..17d3c34 100644 --- a/db/schema.ts +++ b/db/schema.ts @@ -48,6 +48,11 @@ export const explorerContentTable = sqliteTable("explorer_content", { 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 }) => ({ data: one(usersDataTable, { fields: [usersTable.id], references: [usersDataTable.id], }), session: many(userSessionsTable), diff --git a/drizzle/0005_panoramic_slayback.sql b/drizzle/0005_panoramic_slayback.sql new file mode 100644 index 0000000..506e447 --- /dev/null +++ b/drizzle/0005_panoramic_slayback.sql @@ -0,0 +1,4 @@ +CREATE TABLE `email_validation` ( + `id` text PRIMARY KEY NOT NULL, + `timestamp` integer NOT NULL +); diff --git a/drizzle/meta/0005_snapshot.json b/drizzle/meta/0005_snapshot.json new file mode 100644 index 0000000..52f81f5 --- /dev/null +++ b/drizzle/meta/0005_snapshot.json @@ -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": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 52c388e..609ed12 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -36,6 +36,13 @@ "when": 1732722840534, "tag": "0004_ancient_thunderball", "breakpoints": true + }, + { + "idx": 5, + "version": "6", + "when": 1734426608563, + "tag": "0005_panoramic_slayback", + "breakpoints": true } ] } \ No newline at end of file diff --git a/nuxt.config.ts b/nuxt.config.ts index df2d740..b27f1d9 100644 --- a/nuxt.config.ts +++ b/nuxt.config.ts @@ -153,7 +153,7 @@ export default defineNuxtConfig({ xssValidator: false, }, sitemap: { - exclude: ['/admin/**', '/explore/edit', '/user/mailvalidated'], + exclude: ['/admin/**', '/explore/edit', '/user/mailvalidated', '/user/changing-password', '/user/reset-password'], sources: ['/api/__sitemap__/urls'] }, experimental: { diff --git a/pages/user/mailvalidated.vue b/pages/user/(automatic)/mailvalidated.vue similarity index 100% rename from pages/user/mailvalidated.vue rename to pages/user/(automatic)/mailvalidated.vue diff --git a/pages/user/(automatic)/reset-password.vue b/pages/user/(automatic)/reset-password.vue new file mode 100644 index 0000000..562467f --- /dev/null +++ b/pages/user/(automatic)/reset-password.vue @@ -0,0 +1,47 @@ + + + \ No newline at end of file diff --git a/pages/user/(automatic)/resetting-password.vue b/pages/user/(automatic)/resetting-password.vue new file mode 100644 index 0000000..e8ae7fd --- /dev/null +++ b/pages/user/(automatic)/resetting-password.vue @@ -0,0 +1,87 @@ + + + \ No newline at end of file diff --git a/pages/user/changing-password.vue b/pages/user/changing-password.vue new file mode 100644 index 0000000..5a7ff68 --- /dev/null +++ b/pages/user/changing-password.vue @@ -0,0 +1,88 @@ + + + \ No newline at end of file diff --git a/pages/user/login.vue b/pages/user/login.vue index 22ce527..b44c8c2 100644 --- a/pages/user/login.vue +++ b/pages/user/login.vue @@ -11,6 +11,7 @@ + Mot de passe oublié ? Pas de compte ? diff --git a/pages/user/profile.vue b/pages/user/profile.vue index 4fe637e..c6bacaa 100644 --- a/pages/user/profile.vue +++ b/pages/user/profile.vue @@ -12,7 +12,7 @@ async function revalidateUser() { loading.value = true; await $fetch(`/api/users/${user.value?.id}/revalidate`, { - method: 'get' + method: 'post' }); loading.value = false; toaster.add({ closeable: false, duration: 10000, timer: true, content: 'Un mail vous a été envoyé.', type: 'info' }); @@ -54,7 +54,7 @@ async function deleteUser()
- +
@@ -17,10 +17,11 @@ import { computed } from 'vue'; import Bun from 'bun'; -const { id, username, timestamp } = defineProps<{ +const { id, userId, username, timestamp } = defineProps<{ id: number + userId: number username: string timestamp: number }>(); -const hash = computed(() => Bun.hash(id.toString(), timestamp)); +const hash = computed(() => Bun.hash('1' + userId.toString(), timestamp)); \ No newline at end of file diff --git a/server/components/mail/reset-password.vue b/server/components/mail/reset-password.vue new file mode 100644 index 0000000..d0fcf0d --- /dev/null +++ b/server/components/mail/reset-password.vue @@ -0,0 +1,29 @@ + + + \ No newline at end of file diff --git a/server/routes/user/mailvalidation.get.ts b/server/routes/user/mailvalidation.get.ts index da6c030..ed31af8 100644 --- a/server/routes/user/mailvalidation.get.ts +++ b/server/routes/user/mailvalidation.get.ts @@ -1,10 +1,11 @@ -import { eq } from "drizzle-orm"; +import { eq, getTableColumns, lte } from "drizzle-orm"; import { z } from "zod"; import useDatabase from "~/composables/useDatabase"; -import { usersTable } from "~/db/schema"; +import { emailValidationTable, usersTable } from "~/db/schema"; const schema = z.object({ h: z.coerce.string(), + i: z.coerce.string(), u: z.coerce.number(), t: z.coerce.number(), }); @@ -15,22 +16,33 @@ export default defineEventHandler(async (e) => { if(!query.success) 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({ statusCode: 400, message: 'Lien incorrect', - }) + }); } if(Date.now() > query.data.t + (60 * 60 * 1000)) { return createError({ statusCode: 400, message: 'Le lien a expiré', - }) + }); } 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(); if(result === undefined) @@ -38,14 +50,14 @@ export default defineEventHandler(async (e) => { return createError({ statusCode: 400, message: 'Aucune donnée utilisateur trouvée', - }) + }); } if(result?.state === 1) { return createError({ statusCode: 400, message: 'Votre compte a déjà été validé', - }) + }); } db.update(usersTable).set({ state: 1 }).where(eq(usersTable.id, query.data.u)).run(); diff --git a/server/tasks/mail.ts b/server/tasks/mail.ts index bd22a49..c203f59 100644 --- a/server/tasks/mail.ts +++ b/server/tasks/mail.ts @@ -3,28 +3,24 @@ import { createSSRApp, h } from 'vue'; import { renderToString } from 'vue/server-renderer'; import base from '../components/mail/base.vue'; -import registration from '../components/mail/registration.vue'; -//import revalidation from '../components/mail/revalidation.vue'; +import Registration from '../components/mail/registration.vue'; +import ResetPassword from '../components/mail/reset-password.vue'; const config = useRuntimeConfig(); const [domain, selector, dkim] = config.mail.dkim.split(":"); export const templates: Record = { - "registration": { component: registration, subject: 'Bienvenue sur d[any] 😎' }, -// "revalidate-mail": { component: revalidation, subject: 'd[any]: Valider votre email' }, + "registration": { component: Registration, subject: 'Bienvenue sur d[any] 😎' }, + "reset-password": { component: ResetPassword, subject: 'Réinitialisation de votre mot de passe' }, }; -import 'nitropack/types'; import type Mail from 'nodemailer/lib/mailer'; -declare module 'nitropack/types' +interface MailPayload { - interface TaskPayload - { - type: 'mail' - to: string[] - template: string - data: Record - } + type: 'mail' + to: string[] + template: string + data: Record } const transport = nodemailer.createTransport({ @@ -57,7 +53,7 @@ export default defineTask({ throw new Error(`Données inconnues`); } - const payload = e.payload; + const payload = e.payload as MailPayload; const template = templates[payload.template]; if(!template) diff --git a/server/tasks/validation.ts b/server/tasks/validation.ts new file mode 100644 index 0000000..2877499 --- /dev/null +++ b/server/tasks/validation.ts @@ -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 }; + } + }, +}) \ No newline at end of file diff --git a/todo.md b/todo.md index 00871d8..1a28a2a 100644 --- a/todo.md +++ b/todo.md @@ -1,4 +1,5 @@ -- [ ] Rename auto des liens au changement de path +- [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