Add mail template, mail HTML generation and a few UI fixes
This commit is contained in:
parent
d71e8b7910
commit
4df9297d47
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<Label class="py-4 flex flex-row justify-center items-center">
|
<Label class="my-2 flex flex-1 items-center justify-between flex-col md:flex-row">
|
||||||
<span>{{ label }}</span>
|
<span class="pb-1 md:p-0">{{ label }}</span>
|
||||||
<SelectRoot v-model="model">
|
<SelectRoot v-model="model">
|
||||||
<SelectTrigger :disabled="disabled" class="mx-4 inline-flex min-w-[160px] items-center justify-between px-3 text-sm font-semibold leading-none h-8 gap-1
|
<SelectTrigger :disabled="disabled" class="mx-4 inline-flex min-w-[160px] items-center justify-between px-3 text-sm font-semibold leading-none h-8 gap-1
|
||||||
bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 outline-none data-[placeholder]:font-normal
|
bg-light-20 dark:bg-dark-20 border border-light-35 dark:border-dark-35 outline-none data-[placeholder]:font-normal
|
||||||
|
|
|
||||||
BIN
db.sqlite-shm
BIN
db.sqlite-shm
Binary file not shown.
BIN
db.sqlite-wal
BIN
db.sqlite-wal
Binary file not shown.
|
|
@ -8,7 +8,7 @@
|
||||||
<Icon icon="si:error-line" class="w-12 h-12 text-light-60 dark:text-dark-60"/>
|
<Icon icon="si:error-line" class="w-12 h-12 text-light-60 dark:text-dark-60"/>
|
||||||
<div class="text-3xl">Une erreur est survenue.</div>
|
<div class="text-3xl">Une erreur est survenue.</div>
|
||||||
</div>
|
</div>
|
||||||
<pre class="">Erreur {{ error?.statusCode }}: {{ error?.message }}</pre>
|
<pre class="text-center text-wrap">Erreur {{ error?.statusCode }}: {{ error?.message }}</pre>
|
||||||
<Button @click="handleError">Revenir en lieu sûr</Button>
|
<Button @click="handleError">Revenir en lieu sûr</Button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,6 @@
|
||||||
// https://nuxt.com/docs/api/configuration/nuxt-config
|
// https://nuxt.com/docs/api/configuration/nuxt-config
|
||||||
|
import PluginVue from '@vitejs/plugin-vue';
|
||||||
|
|
||||||
export default defineNuxtConfig({
|
export default defineNuxtConfig({
|
||||||
compatibilityDate: '2024-04-03',
|
compatibilityDate: '2024-04-03',
|
||||||
modules: [
|
modules: [
|
||||||
|
|
@ -114,11 +116,20 @@ export default defineNuxtConfig({
|
||||||
path: '~/components',
|
path: '~/components',
|
||||||
pathPrefix: false,
|
pathPrefix: false,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: '~/server/components',
|
||||||
|
pathPrefix: true,
|
||||||
|
global: true,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
nitro: {
|
nitro: {
|
||||||
|
preset: 'bun',
|
||||||
experimental: {
|
experimental: {
|
||||||
tasks: true,
|
tasks: true,
|
||||||
},
|
},
|
||||||
|
rollupConfig: {
|
||||||
|
plugins: [ PluginVue() ],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
runtimeConfig: {
|
runtimeConfig: {
|
||||||
session: {
|
session: {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
const mailSchema = z.object({
|
const mailSchema = z.object({
|
||||||
to: z.string().email(),
|
to: z.string().email(),
|
||||||
message: z.string(),
|
template: z.string(),
|
||||||
subject: z.string(),
|
data: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const schemaList: Record<string, z.ZodObject<any> | null> = {
|
const schemaList: Record<string, z.ZodObject<any> | null> = {
|
||||||
|
|
@ -21,7 +21,10 @@ definePageMeta({
|
||||||
const job = ref<string>('');
|
const job = ref<string>('');
|
||||||
|
|
||||||
const toaster = useToast();
|
const toaster = useToast();
|
||||||
const payload = reactive<Record<string, any>>({});
|
const payload = reactive<Record<string, any>>({
|
||||||
|
data: JSON.stringify({ username: "Peaceultime", id: 1, timestamp: Date.now() }),
|
||||||
|
to: 'clem31470@gmail.com',
|
||||||
|
});
|
||||||
const data = ref(), status = ref<'idle' | 'pending' | 'success' | 'error'>('idle'), success = ref(false), error = ref<Error | null>();
|
const data = ref(), status = ref<'idle' | 'pending' | 'success' | 'error'>('idle'), success = ref(false), error = ref<Error | null>();
|
||||||
async function fetch()
|
async function fetch()
|
||||||
{
|
{
|
||||||
|
|
@ -36,6 +39,7 @@ async function fetch()
|
||||||
|
|
||||||
if(schema)
|
if(schema)
|
||||||
{
|
{
|
||||||
|
console.log(payload);
|
||||||
const parsedPayload = schema.parse(payload);
|
const parsedPayload = schema.parse(payload);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -66,15 +70,20 @@ async function fetch()
|
||||||
</Head>
|
</Head>
|
||||||
<div class="flex flex-col justify-start items-center">
|
<div class="flex flex-col justify-start items-center">
|
||||||
<ProseH2>Administration</ProseH2>
|
<ProseH2>Administration</ProseH2>
|
||||||
<Select label="Job" v-model="job" @update:model-value="payload = {}">
|
<div class="flex flex-row w-full gap-8">
|
||||||
<SelectItem label="Récupérer les données d'Obsidian" value="pull" />
|
<Select label="Job" v-model="job">
|
||||||
<SelectItem label="Envoyer les données dans Obsidian" value="push" disabled />
|
<SelectItem label="Récupérer les données d'Obsidian" value="pull" />
|
||||||
<SelectItem label="Envoyer un mail de test" value="mail" />
|
<SelectItem label="Envoyer les données dans Obsidian" value="push" disabled />
|
||||||
</Select>
|
<SelectItem label="Envoyer un mail de test" value="mail" />
|
||||||
|
</Select>
|
||||||
|
<Select v-if="job === 'mail'" v-model="payload.template" label="Modèle" class="w-full" ><SelectItem label="Inscription" value="registration" /></Select>
|
||||||
|
</div>
|
||||||
<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" />
|
||||||
<TextInput label="Objet" class="w-full" v-model="payload.subject" />
|
<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>
|
||||||
<TextInput label="Message" class="w-full" v-model="payload.message" />
|
<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>
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,13 @@
|
||||||
import { hasPermissions } from "#shared/auth.util";
|
import { hasPermissions } from "#shared/auth.util";
|
||||||
|
|
||||||
|
declare module 'nitropack'
|
||||||
|
{
|
||||||
|
interface TaskPayload
|
||||||
|
{
|
||||||
|
type: string
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export default defineEventHandler(async (e) => {
|
export default defineEventHandler(async (e) => {
|
||||||
const session = await getUserSession(e);
|
const session = await getUserSession(e);
|
||||||
|
|
||||||
|
|
@ -16,6 +24,9 @@ export default defineEventHandler(async (e) => {
|
||||||
setResponseStatus(e, 400);
|
setResponseStatus(e, 400);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
payload.type = id;
|
||||||
|
payload.data = JSON.parse(payload.data);
|
||||||
|
|
||||||
const result = await runTask(id, {
|
const result = await runTask(id, {
|
||||||
payload: payload
|
payload: payload
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
<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>
|
||||||
|
</template>
|
||||||
|
|
@ -0,0 +1,22 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<p style="font-size: 19px; font-variant: small-caps;">Bienvenue sur d[any], <span style="">{{ 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>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
|
<span style="font-size: small; padding-left: 8px;">Ce lien est valable 1 heure.</span>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
const { id, username, timestamp } = defineProps<{
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
timestamp: number
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
@ -0,0 +1,20 @@
|
||||||
|
<template>
|
||||||
|
<div class="">
|
||||||
|
<img />
|
||||||
|
<p>Bienvenue sur d[any], <span>{{ username }}</span>.</p>
|
||||||
|
</div>
|
||||||
|
<p class="">Nous vous invitons à valider votre compte en cliquant <a :href="`https://obsidian.peaceultime.com/user/mail-validation?u=${id}&t=${timestamp}&h=${hash(id.toString(), timestamp)}`"><Button>ici</Button></a> afin de profiter de toutes les fonctionnalités de d[any]</p>
|
||||||
|
<p class="">Vous pouvez egalement copier le lien suivant pour valider votre compte: {{ `https://obsidian.peaceultime.com/user/mail-validation?u=${id}&t=${timestamp}&h=${hash(id.toString(), timestamp)}` }}</p>
|
||||||
|
<span>Ce lien est valable 1 heure.</span>
|
||||||
|
<footer></footer>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { hash } from 'bun';
|
||||||
|
|
||||||
|
const { id, username, timestamp } = defineProps<{
|
||||||
|
id: number
|
||||||
|
username: string
|
||||||
|
timestamp: number
|
||||||
|
}>();
|
||||||
|
</script>
|
||||||
|
|
@ -1,9 +1,34 @@
|
||||||
import nodemailer from 'nodemailer';
|
import nodemailer from 'nodemailer';
|
||||||
|
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';
|
||||||
|
|
||||||
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 }> = {
|
||||||
|
"registration": { component: registration, subject: 'Bienvenue sur d[any] 😎' },
|
||||||
|
"revalidate-mail": { component: revalidation, subject: 'd[any]: Valider votre email' },
|
||||||
|
};
|
||||||
|
|
||||||
|
import 'nitropack/types';
|
||||||
|
import type Registration from '../components/mail/registration.vue';
|
||||||
|
declare module 'nitropack/types'
|
||||||
|
{
|
||||||
|
interface TaskPayload
|
||||||
|
{
|
||||||
|
type: 'mail'
|
||||||
|
to: string[]
|
||||||
|
template: string
|
||||||
|
data: Record<string, any>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const transport = nodemailer.createTransport({
|
const transport = nodemailer.createTransport({
|
||||||
|
//@ts-ignore
|
||||||
pool: true,
|
pool: true,
|
||||||
host: config.mail.host,
|
host: config.mail.host,
|
||||||
port: config.mail.port,
|
port: config.mail.port,
|
||||||
|
|
@ -27,13 +52,35 @@ export default defineTask({
|
||||||
},
|
},
|
||||||
async run(e) {
|
async run(e) {
|
||||||
try {
|
try {
|
||||||
const payload: { to: string[], message: string, subject: string } = e.payload;
|
if(e.payload.type !== 'mail')
|
||||||
const status = await transport.sendMail({
|
{
|
||||||
from: 'Message automatique d[any] <no-reply@peaceultime.com>',
|
throw new Error(`Données inconnues`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const payload = e.payload;
|
||||||
|
const template = templates[payload.template];
|
||||||
|
|
||||||
|
if(!template)
|
||||||
|
{
|
||||||
|
throw new Error(`Modèle de mail ${payload.template} inconnu`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const mail = {
|
||||||
|
from: 'd[any] - Ne pas répondre <no-reply@peaceultime.com>',
|
||||||
to: payload.to,
|
to: payload.to,
|
||||||
text: payload.message,
|
html: await render(template.component, payload.data),
|
||||||
subject: payload.subject,
|
subject: template.subject,
|
||||||
});
|
attachments: [{
|
||||||
|
filename: 'logo.svg',
|
||||||
|
path: '../../public/logo.dark.svg',
|
||||||
|
cid: 'logo.obsidian.peaceultime.com',
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
|
||||||
|
if(mail.html === '')
|
||||||
|
return { result: false, error: new Error("Invalid content") };
|
||||||
|
|
||||||
|
const status = await transport.sendMail(mail);
|
||||||
|
|
||||||
if(status.rejected.length > 0)
|
if(status.rejected.length > 0)
|
||||||
{
|
{
|
||||||
|
|
@ -47,4 +94,15 @@ export default defineTask({
|
||||||
return { result: false, error: e };
|
return { result: false, error: e };
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
|
async function render(component: any, data: Record<string, any>): Promise<string>
|
||||||
|
{
|
||||||
|
const app = createSSRApp({
|
||||||
|
render(){
|
||||||
|
return h(base, null, { default: () => h(component, data, []) });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return await renderToString(app);
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue