Send registration email, add mail validation page, stabilize mail generation
This commit is contained in:
parent
4df9297d47
commit
4125cbb3a2
Binary file not shown.
BIN
db.sqlite-wal
BIN
db.sqlite-wal
Binary file not shown.
|
|
@ -1,5 +1,6 @@
|
||||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||||
import PluginVue from '@vitejs/plugin-vue';
|
import vuePlugin from 'rollup-plugin-vue'
|
||||||
|
import postcssPlugin from 'rollup-plugin-postcss'
|
||||||
|
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
compatibilityDate: '2024-04-03',
|
compatibilityDate: '2024-04-03',
|
||||||
|
|
@ -123,12 +124,21 @@ export default defineNuxtConfig({
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
nitro: {
|
nitro: {
|
||||||
|
alias: {
|
||||||
|
'public': '//public',
|
||||||
|
},
|
||||||
|
publicAssets: [{
|
||||||
|
baseURL: 'public',
|
||||||
|
dir: 'public',
|
||||||
|
}],
|
||||||
preset: 'bun',
|
preset: 'bun',
|
||||||
experimental: {
|
experimental: {
|
||||||
tasks: true,
|
tasks: true,
|
||||||
},
|
},
|
||||||
rollupConfig: {
|
rollupConfig: {
|
||||||
plugins: [ PluginVue() ],
|
plugins: [
|
||||||
|
vuePlugin({ include: /\.vue$/, target: 'node' })
|
||||||
|
]
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
runtimeConfig: {
|
runtimeConfig: {
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,8 @@
|
||||||
"remark-gfm": "^4.0.0",
|
"remark-gfm": "^4.0.0",
|
||||||
"remark-parse": "^11.0.0",
|
"remark-parse": "^11.0.0",
|
||||||
"remark-rehype": "^11.1.1",
|
"remark-rehype": "^11.1.1",
|
||||||
|
"rollup-plugin-postcss": "^4.0.2",
|
||||||
|
"rollup-plugin-vue": "^6.0.0",
|
||||||
"unified": "^11.0.5",
|
"unified": "^11.0.5",
|
||||||
"unist-util-visit": "^5.0.0",
|
"unist-util-visit": "^5.0.0",
|
||||||
"vue": "latest",
|
"vue": "latest",
|
||||||
|
|
|
||||||
|
|
@ -81,9 +81,6 @@ async function fetch()
|
||||||
<div v-if="job === 'mail'" class="flex justify-center items-center flex-col">
|
<div v-if="job === 'mail'" class="flex justify-center items-center flex-col">
|
||||||
<TextInput label="Destinataire" class="w-full" v-model="payload.to" />
|
<TextInput label="Destinataire" class="w-full" v-model="payload.to" />
|
||||||
<textarea v-model="payload.data" class="w-[640px] bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 outline-none m-2 px-2"></textarea>
|
<textarea v-model="payload.data" class="w-[640px] bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 outline-none m-2 px-2"></textarea>
|
||||||
<div class="bg-[#fff] text-[#000]"><MailBase>
|
|
||||||
<MailRegistration v-bind="JSON.parse(payload.data)" />
|
|
||||||
</MailBase></div>
|
|
||||||
</div>
|
</div>
|
||||||
<Button class="self-center" @click="() => !!job && fetch()" :loading="status === 'pending'">
|
<Button class="self-center" @click="() => !!job && fetch()" :loading="status === 'pending'">
|
||||||
<span>Executer</span>
|
<span>Executer</span>
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="flex flex-col gap-2 justify-start">
|
<div class="flex flex-col gap-2 justify-start">
|
||||||
<ProseH3>Utilisateur</ProseH3>
|
<ProseH3>Utilisateur</ProseH3>
|
||||||
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Validation du compte par mail<ProseTag>prioritaire</ProseTag></span></Label>
|
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" checked disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class="text-light-60 dark:text-dark-60 line-through">Validation du compte par mail<ProseTag>prioritaire</ProseTag></span></Label>
|
||||||
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Modification de profil</span></Label>
|
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Modification de profil</span></Label>
|
||||||
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Image de profil</span></Label>
|
<Label class="flex flex-row gap-2 items-center"><CheckboxRoot class="border border-light-35 dark:border-dark-35 w-6 h-6 flex justify-center items-center" disabled><CheckboxIndicator><Icon icon="radix-icons:check" /></CheckboxIndicator></CheckboxRoot><span class=" ">Image de profil</span></Label>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -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">
|
||||||
|
<ProseH2>Votre compte a été validé ! 🎉</ProseH2>
|
||||||
|
<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>
|
||||||
|
|
@ -11,19 +11,13 @@
|
||||||
<TextInput type="text" label="Nom d'utilisateur" autocomplete="username" v-model="state.username" class="w-full md:w-auto"/>
|
<TextInput type="text" label="Nom d'utilisateur" autocomplete="username" v-model="state.username" class="w-full md:w-auto"/>
|
||||||
<TextInput type="email" label="Email" autocomplete="email" v-model="state.email" class="w-full md:w-auto"/>
|
<TextInput type="email" label="Email" autocomplete="email" v-model="state.email" class="w-full md:w-auto"/>
|
||||||
<TextInput type="password" label="Mot de passe" autocomplete="new-password" v-model="state.password" class="w-full md:w-auto"/>
|
<TextInput type="password" label="Mot de passe" autocomplete="new-password" v-model="state.password" class="w-full md:w-auto"/>
|
||||||
<div class="flex 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">
|
<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="">Votre mot de passe doit respecter les critères de sécurité suivants
|
<span class="col-span-2">Prérequis de sécurité</span>
|
||||||
:</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" :class="{'text-light-red dark:text-dark-red': !checkedLength}">Entre 8 et 128
|
<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>
|
||||||
caractères</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" :class="{'text-light-red dark:text-dark-red': !checkedLowerUpper}">Au moins
|
<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>
|
||||||
une minuscule et une majuscule</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>
|
||||||
<span class="ps-4" :class="{'text-light-red dark:text-dark-red': !checkedDigit}">Au moins un
|
|
||||||
chiffre</span>
|
|
||||||
<span class="ps-4" :class="{'text-light-red dark:text-dark-red': !checkedSymbol}">Au moins un
|
|
||||||
caractère spécial parmi la liste suivante:
|
|
||||||
<pre class="text-wrap">! " # $ % & ' ( ) * + , - . / : ; < = > ? @ [ ] ^ _ ` { | } ~</pre>
|
|
||||||
</span>
|
|
||||||
</div>
|
</div>
|
||||||
<TextInput type="password" label="Confirmation du mot de passe" autocomplete="new-password" v-model="confirmPassword" class="w-full md:w-auto"/>
|
<TextInput type="password" label="Confirmation du mot de passe" autocomplete="new-password" v-model="confirmPassword" class="w-full md:w-auto"/>
|
||||||
<Button 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>
|
<Button 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>
|
||||||
|
|
@ -52,7 +46,8 @@ const { add: addToast, clear: clearToasts } = useToast();
|
||||||
const confirmPassword = ref("");
|
const confirmPassword = ref("");
|
||||||
|
|
||||||
const checkedLength = computed(() => state.password.length >= 8 && state.password.length <= 128);
|
const checkedLength = computed(() => state.password.length >= 8 && state.password.length <= 128);
|
||||||
const checkedLowerUpper = computed(() => state.password.toLowerCase() !== state.password && state.password.toUpperCase() !== state.password);
|
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 checkedDigit = computed(() => /[0-9]/.test(state.password));
|
||||||
const checkedSymbol = computed(() => " !\"#$%&'()*+,-./:;<=>?@[]^_`{|}~".split("").some(e => state.password.includes(e)));
|
const checkedSymbol = computed(() => " !\"#$%&'()*+,-./:;<=>?@[]^_`{|}~".split("").some(e => state.password.includes(e)));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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);
|
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);
|
setResponseStatus(e, 201);
|
||||||
return { success: true, session };
|
return { success: true, session };
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,20 +1,16 @@
|
||||||
<template>
|
<template>
|
||||||
<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;'>
|
||||||
<body style="min-width: 1000px;">
|
<div style="margin-left: auto; margin-right: auto; text-align: center;">
|
||||||
<div style="user-select: none; font-size: medium; font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; width: 70%; margin: auto;">
|
<a href="https://obsidian.peaceultime.com">
|
||||||
<div style="margin: auto;">
|
<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;" />
|
||||||
<a href="https://obsidian.peaceultime.com" style="display: inline-block; ">
|
<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>
|
||||||
<span style="font-size: 25px; font-weight: bolder;">d[any]</span>
|
</a>
|
||||||
<img src="cid:logo.obsidian.peaceultime.com" style="width: 64px; height: 64px; display: inline;" alt="Logo" width="64" height="64"/>
|
</div>
|
||||||
</a>
|
<div style="padding: 1rem;">
|
||||||
</div>
|
<slot></slot>
|
||||||
<div style="width: 70%; padding: 1em 0;">
|
</div>
|
||||||
<slot />
|
</div>
|
||||||
</div>
|
<div style="background-color: #171717;">
|
||||||
<footer style="color: #707070; font-style: italic; display: flex; justify-content: center; align-items: center; padding: 1em 0;">
|
<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>
|
||||||
<p>Copyright Peaceultime - d[any] - 2024</p>
|
</div>
|
||||||
</footer>
|
|
||||||
</div>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
@ -1,22 +1,26 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div style="max-width: 800px; margin-left: auto; margin-right: auto;">
|
||||||
<p style="font-size: 19px; font-variant: small-caps;">Bienvenue sur d[any], <span style="">{{ 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="display: flex; justify-content: center; align-items: center; padding: 1em 0;">
|
<div style="padding-top: 1rem; padding-bottom: 1rem; text-align: center;">
|
||||||
<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>
|
<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>
|
||||||
<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="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>
|
</div>
|
||||||
<span style="font-size: small; padding-left: 8px;">Ce lien est valable 1 heure.</span>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue';
|
||||||
|
import Bun from 'bun';
|
||||||
|
|
||||||
const { id, username, timestamp } = defineProps<{
|
const { id, username, timestamp } = defineProps<{
|
||||||
id: number
|
id: number
|
||||||
username: string
|
username: string
|
||||||
timestamp: number
|
timestamp: number
|
||||||
}>();
|
}>();
|
||||||
|
const hash = computed(() => Bun.hash(id.toString(), timestamp));
|
||||||
</script>
|
</script>
|
||||||
|
|
@ -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');
|
||||||
|
})
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
import nodemailer from 'nodemailer';
|
import nodemailer from 'nodemailer';
|
||||||
import { createSSRApp, h } from 'vue';
|
import { createSSRApp, h } from 'vue';
|
||||||
import { renderToString } from 'vue/server-renderer';
|
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 base from '../components/mail/base.vue';
|
||||||
import registration from '../components/mail/registration.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 'nitropack/types';
|
||||||
import type Registration from '../components/mail/registration.vue';
|
import type Mail from 'nodemailer/lib/mailer';
|
||||||
declare module 'nitropack/types'
|
declare module 'nitropack/types'
|
||||||
{
|
{
|
||||||
interface TaskPayload
|
interface TaskPayload
|
||||||
|
|
@ -65,22 +68,22 @@ export default defineTask({
|
||||||
throw new Error(`Modèle de mail ${payload.template} inconnu`);
|
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>',
|
from: 'd[any] - Ne pas répondre <no-reply@peaceultime.com>',
|
||||||
to: payload.to,
|
to: payload.to,
|
||||||
html: await render(template.component, payload.data),
|
html: await render(template.component, payload.data),
|
||||||
subject: template.subject,
|
subject: template.subject,
|
||||||
attachments: [{
|
|
||||||
filename: 'logo.svg',
|
|
||||||
path: '../../public/logo.dark.svg',
|
|
||||||
cid: 'logo.obsidian.peaceultime.com',
|
|
||||||
}]
|
|
||||||
};
|
};
|
||||||
|
console.timeEnd('Generating HTML');
|
||||||
|
|
||||||
if(mail.html === '')
|
if(mail.html === '')
|
||||||
return { result: false, error: new Error("Invalid content") };
|
return { result: false, error: new Error("Invalid content") };
|
||||||
|
|
||||||
|
console.time('Sending Mail');
|
||||||
const status = await transport.sendMail(mail);
|
const status = await transport.sendMail(mail);
|
||||||
|
console.timeEnd('Sending Mail');
|
||||||
|
|
||||||
if(status.rejected.length > 0)
|
if(status.rejected.length > 0)
|
||||||
{
|
{
|
||||||
|
|
@ -100,9 +103,11 @@ async function render(component: any, data: Record<string, any>): Promise<string
|
||||||
{
|
{
|
||||||
const app = createSSRApp({
|
const app = createSSRApp({
|
||||||
render(){
|
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>`);
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue