Send registration email, add mail validation page, stabilize mail generation

This commit is contained in:
2024-11-26 15:22:57 +01:00
parent 4df9297d47
commit 4125cbb3a2
15 changed files with 147 additions and 53 deletions

View File

@@ -73,6 +73,19 @@ export default defineEventHandler(async (e): Promise<Return> => {
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);
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 };
}

View File

@@ -1,20 +1,16 @@
<template>
<html>
<body style="min-width: 1000px;">
<div style="user-select: none; font-size: medium; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; width: 70%; margin: auto;">
<div style="margin: auto;">
<a href="https://obsidian.peaceultime.com" style="display: inline-block; ">
<span style="font-size: 25px; font-weight: bolder;">d[any]</span>
<img src="cid:logo.obsidian.peaceultime.com" style="width: 64px; height: 64px; display: inline;" alt="Logo" width="64" height="64"/>
</a>
</div>
<div style="width: 70%; padding: 1em 0;">
<slot />
</div>
<footer style="color: #707070; font-style: italic; display: flex; justify-content: center; align-items: center; padding: 1em 0;">
<p>Copyright Peaceultime - d[any] - 2024</p>
</footer>
</div>
</body>
</html>
<div style='margin-left: auto; margin-right: auto; width: 75%; font-family: ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"; font-size: 1rem; line-height: 1.5rem; color: #171717;'>
<div style="margin-left: auto; margin-right: auto; text-align: center;">
<a href="https://obsidian.peaceultime.com">
<img src="https://obsidian.peaceultime.com/logo.light.svg" alt="Logo" title="d[any] logo" width="64" height="64" style="display: block; height: 4rem; width: 4rem; margin-left: auto; margin-right: auto;" />
<span style="margin-inline-end: 1rem; font-size: 1.5rem; line-height: 2rem; font-weight: 700; font-family: 'Lucida Sans', 'Lucida Sans Regular', 'Lucida Grande', 'Lucida Sans Unicode', Geneva, Verdana, sans-serif;">d[any]</span>
</a>
</div>
<div style="padding: 1rem;">
<slot></slot>
</div>
</div>
<div style="background-color: #171717;">
<p style="padding-top: 1rem; padding-bottom: 1rem; text-align: center; font-size: 0.75rem; line-height: 1rem; color: #fff;">Copyright Peaceultime - d[any] - 2024</p>
</div>
</template>

View File

@@ -1,22 +1,26 @@
<template>
<div>
<p style="font-size: 19px; font-variant: small-caps;">Bienvenue sur d[any], <span style="">{{ username }}</span>.</p>
<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;">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>
<div style="display: flex; justify-content: center; align-items: center; padding: 1em 0;">
<a :href="`https://obsidian.peaceultime.com/user/mail-validation?u=${id}&t=${timestamp}&h=${0}`" target="_blank" style="border: 1px solid #999; padding: 1px 4px; background-color: #eee; font-weight: lighter; font-size: large">Je valide mon compte</a>
<div style="padding-top: 1rem; padding-bottom: 1rem; text-align: center;">
<a :href="`https://obsidian.peaceultime.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>
<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 valider votre compte: </span>
<pre style="border-bottom: 1px solid #999; font-size: small; display: inline-block; user-select: all">{{ `https://obsidian.peaceultime.com/user/mail-validation?u=${id}&t=${timestamp}&h=${0}` }}</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://obsidian.peaceultime.com/user/mailvalidation?u=${id}&t=${timestamp}&h=${hash}` }}</pre>
</div>
<span style="font-size: small; padding-left: 8px;">Ce lien est valable 1 heure.</span>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue';
import Bun from 'bun';
const { id, username, timestamp } = defineProps<{
id: number
username: string
timestamp: number
}>();
const hash = computed(() => Bun.hash(id.toString(), timestamp));
</script>

View File

@@ -0,0 +1,54 @@
import { eq } from "drizzle-orm";
import { z } from "zod";
import useDatabase from "~/composables/useDatabase";
import { usersTable } from "~/db/schema";
const schema = z.object({
h: z.coerce.string(),
u: z.coerce.number(),
t: z.coerce.number(),
});
export default defineEventHandler(async (e) => {
const query = await getValidatedQuery(e, schema.safeParse);
if(!query.success)
throw query.error;
if(Bun.hash(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 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',
})
}
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();
sendRedirect(e, '/user/mailvalidated');
})

View File

@@ -1,6 +1,9 @@
import nodemailer from 'nodemailer';
import { createSSRApp, h } from 'vue';
import { renderToString } from 'vue/server-renderer';
import postcss from 'postcss';
import tailwindcss from 'tailwindcss';
import { join } from 'node:path';
import base from '../components/mail/base.vue';
import registration from '../components/mail/registration.vue';
@@ -15,7 +18,7 @@ export const templates: Record<string, { component: any, subject: string }> = {
};
import 'nitropack/types';
import type Registration from '../components/mail/registration.vue';
import type Mail from 'nodemailer/lib/mailer';
declare module 'nitropack/types'
{
interface TaskPayload
@@ -65,22 +68,22 @@ export default defineTask({
throw new Error(`Modèle de mail ${payload.template} inconnu`);
}
const mail = {
console.time('Generating HTML');
const mail: Mail.Options = {
from: 'd[any] - Ne pas répondre <no-reply@peaceultime.com>',
to: payload.to,
html: await render(template.component, payload.data),
subject: template.subject,
attachments: [{
filename: 'logo.svg',
path: '../../public/logo.dark.svg',
cid: 'logo.obsidian.peaceultime.com',
}]
};
console.timeEnd('Generating HTML');
if(mail.html === '')
return { result: false, error: new Error("Invalid content") };
console.time('Sending Mail');
const status = await transport.sendMail(mail);
console.timeEnd('Sending Mail');
if(status.rejected.length > 0)
{
@@ -100,9 +103,11 @@ async function render(component: any, data: Record<string, any>): Promise<string
{
const app = createSSRApp({
render(){
return h(base, null, { default: () => h(component, data, []) });
return h(base, null, { default: () => h(component, data, { default: () => null }) });
}
});
return await renderToString(app);
const html = await renderToString(app);
return (`<html><body><div>${html}</div></body></html>`);
}