Skip to content
Snippets Groups Projects
Commit d26473c5 authored by Aaron Dötsch's avatar Aaron Dötsch
Browse files

Implement notification email handler

parent 2563f531
No related branches found
No related tags found
No related merge requests found
Showing
with 111 additions and 24 deletions
...@@ -2,3 +2,12 @@ DB_USERNAME="postgres" ...@@ -2,3 +2,12 @@ DB_USERNAME="postgres"
DB_PASSWORD="postgres" DB_PASSWORD="postgres"
DB_NAME="postgres" DB_NAME="postgres"
BODY_SIZE_LIMIT=10485760 # 10MiB BODY_SIZE_LIMIT=10485760 # 10MiB
MAIL_HOST="domain.tld"
MAIL_PORT=465
MAIL_USER="noreply@domain.tld"
MAIL_PASSWORD="secure-password"
MAIL_FROM_ADDRESS="noreply@domain.tld"
MAIL_FROM_NAME="Getränkekasse"
MAIL_TLS=true
MAIL_AUTH_METHOD="plain"
MAIL_REPLY_TO="getraenke@domain.tld" # optional, sets the reply-to header, useful if you don't read the mailbox of MAIL_USER
...@@ -29,6 +29,15 @@ services: ...@@ -29,6 +29,15 @@ services:
environment: environment:
BODY_SIZE_LIMIT: ${BODY_SIZE_LIMIT} BODY_SIZE_LIMIT: ${BODY_SIZE_LIMIT}
DATABASE_URL: postgresql://${DB_USERNAME}:${DB_PASSWORD}@postgres:5432/${DB_NAME} DATABASE_URL: postgresql://${DB_USERNAME}:${DB_PASSWORD}@postgres:5432/${DB_NAME}
MAIL_HOST: ${MAIL_HOST}
MAIL_PORT: ${MAIL_PORT}
MAIL_USER: ${MAIL_USER}
MAIL_PASSWORD: ${MAIL_PASSWORD}
MAIL_FROM_NAME: ${MAIL_FROM_NAME}
MAIL_FROM_ADDRESS: ${MAIL_FROM_ADDRESS}
MAIL_TLS: ${MAIL_TLS}
MAIL_AUTH_METHOD: ${MAIL_AUTH_METHOD}
MAIL_REPLY_TO: ${MAIL_REPLY_TO}
volumes: volumes:
- ./article-images:/app/article-images - ./article-images:/app/article-images
......
...@@ -12,6 +12,7 @@ ...@@ -12,6 +12,7 @@
"@sveltejs/adapter-node": "^1.2.1", "@sveltejs/adapter-node": "^1.2.1",
"cookie": "^0.5.0", "cookie": "^0.5.0",
"nanoid": "^4.0.1", "nanoid": "^4.0.1",
"nodemailer": "^6.9.4",
"prisma": "^4.11.0" "prisma": "^4.11.0"
}, },
"devDependencies": { "devDependencies": {
...@@ -1666,6 +1667,14 @@ ...@@ -1666,6 +1667,14 @@
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
"dev": true "dev": true
}, },
"node_modules/nodemailer": {
"version": "6.9.4",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.4.tgz",
"integrity": "sha512-CXjQvrQZV4+6X5wP6ZIgdehJamI63MFoYFGGPtHudWym9qaEHDNdPzaj5bfMCvxG1vhAileSWW90q7nL0N36mA==",
"engines": {
"node": ">=6.0.0"
}
},
"node_modules/once": { "node_modules/once": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
...@@ -3426,6 +3435,11 @@ ...@@ -3426,6 +3435,11 @@
"integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==",
"dev": true "dev": true
}, },
"nodemailer": {
"version": "6.9.4",
"resolved": "https://registry.npmjs.org/nodemailer/-/nodemailer-6.9.4.tgz",
"integrity": "sha512-CXjQvrQZV4+6X5wP6ZIgdehJamI63MFoYFGGPtHudWym9qaEHDNdPzaj5bfMCvxG1vhAileSWW90q7nL0N36mA=="
},
"once": { "once": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
......
...@@ -31,6 +31,7 @@ ...@@ -31,6 +31,7 @@
"@sveltejs/adapter-node": "^1.2.1", "@sveltejs/adapter-node": "^1.2.1",
"cookie": "^0.5.0", "cookie": "^0.5.0",
"nanoid": "^4.0.1", "nanoid": "^4.0.1",
"nodemailer": "^6.9.4",
"prisma": "^4.11.0" "prisma": "^4.11.0"
} }
} }
import { handleWebhook } from "./webhookHandler"; import { handleWebhook } from "../server/notifications/webhookHandler";
import { handleEmail } from "./emailHandler"; import { handleEmail } from "../server/notifications/emailHandler";
/** /**
* @type {{ * @type {{
......
import nodemailer from "nodemailer";
const transporter = nodemailer.createTransport({
host: process.env.MAIL_HOST,
port: parseInt(process.env.MAIL_PORT),
auth: {
user: process.env.MAIL_USER,
pass: process.env.MAIL_PASSWORD
},
secure: process.env.MAIL_TLS === "true",
authMethod: process.env.MAIL_AUTH_METHOD
});
/**
* @param {Object} mailOptions
* @param {string} mailOptions.to The email address to send the email to
* @param {string} mailOptions.subject The subject of the email
* @param {string} mailOptions.text The content of the email
* @param {boolean} [mailOptions.html=false] `true` if `text` is HTML, `false` if `text` is plain text
* @returns
*/
export async function sendMail({to, subject, text, html=false}) {
const content = html ? {html: text} : {text};
return transporter.sendMail({
subject,
to,
from: {
address: process.env.MAIL_FROM_ADDRESS,
name: process.env.MAIL_FROM_NAME
},
replyTo: process.env.MAIL_REPLY_TO,
...content
});
}
import { sendMail } from "../mail";
import { formatMessage, getMessageTitle } from "./formatter"; import { formatMessage, getMessageTitle } from "./formatter";
import type { HandlerReturnType, NotificationData, NotificationType, PossibleNotificationType } from "./types"; import type { HandlerReturnType, NotificationData, NotificationType, PossibleNotificationType } from "../../notifications/types";
export async function handleEmail<T extends PossibleNotificationType>(recipient: string, type: NotificationType<T>, data: NotificationData<T>): Promise<HandlerReturnType> { export async function handleEmail<T extends PossibleNotificationType>(recipient: string, type: NotificationType<T>, data: NotificationData<T>): Promise<HandlerReturnType> {
const emailTitle = getMessageTitle(type); return sendMail({
const emailBody = formatMessage(type, data); to: recipient,
return Promise.resolve({status: "success"}); // TODO actually send email subject: getMessageTitle(type),
text: formatMessage(type, data)
}).then(res=>{
if(res.accepted.length > 0) return { status: "success" };
else if(res.pending.length > 0) throw "Email is pending";
else if(res.rejected.length > 0) throw "Email was rejected";
else throw "Unknown error";
});
}; };
import type { NotificationData, NotificationType, PossibleNotificationType } from "./types"; import type { NotificationData, NotificationType, PossibleNotificationType } from "../../notifications/types";
export function getMessageTitle<T extends PossibleNotificationType>(type: NotificationType<T>): string { export function getMessageTitle<T extends PossibleNotificationType>(type: NotificationType<T>): string {
switch(type.name){ switch(type.name){
......
import { db } from "$lib/server/database"; import { db } from "$lib/server/database";
import { ChannelType } from "./channelTypes"; import { ChannelType } from "../../notifications/channelTypes";
import type { HandlerReturnType, NotificationChannel, NotificationData, NotificationType, PossibleNotificationType } from "./types"; import type { HandlerReturnType, NotificationChannel, NotificationData, NotificationType, PossibleNotificationType } from "../../notifications/types";
export async function sendNotification<T extends PossibleNotificationType>(user: { id: number, notificationChannels: NotificationChannel[] | undefined } | number, type: NotificationType<T>, data: NotificationData<T>): Promise<HandlerReturnType[]>{ export async function sendNotification<T extends PossibleNotificationType>(user: { id: number, notificationChannels: NotificationChannel[] | undefined } | number, type: NotificationType<T>, data: NotificationData<T>): Promise<HandlerReturnType[]>{
let notificationChannels: NotificationChannel[]; let notificationChannels: NotificationChannel[];
......
import { formatMessage } from "./formatter"; import { formatMessage } from "./formatter";
import type { HandlerReturnType, NotificationData, NotificationType, PossibleNotificationType } from "./types"; import type { HandlerReturnType, NotificationData, NotificationType, PossibleNotificationType } from "../../notifications/types";
export async function handleWebhook<T extends PossibleNotificationType>(recipient: string, type: NotificationType<T>, data: NotificationData<T>): Promise<HandlerReturnType> { export async function handleWebhook<T extends PossibleNotificationType>(recipient: string, type: NotificationType<T>, data: NotificationData<T>): Promise<HandlerReturnType> {
const body = JSON.stringify(Object.assign({}, data, {type: type.name, content: formatMessage(type, data)})); const body = JSON.stringify(Object.assign({}, data, {type: type.name, content: formatMessage(type, data)}));
......
...@@ -22,12 +22,12 @@ export async function getUsers(){ ...@@ -22,12 +22,12 @@ export async function getUsers(){
} }
export async function getUser(id, includeCards=false, includeNotificationChannels=false){ export async function getUser(id, includeCards=false, includeNotificationChannels=false){
let include = includeCards || includeNotificationChannels ? {} : undefined;
if(includeCards) include.cards = true;
if(includeNotificationChannels) include.notificationChannels = true;
return db.user.findUnique({ return db.user.findUnique({
where: { id }, where: { id },
include include: {
notificationChannels: includeNotificationChannels,
cards: includeCards
}
}); });
} }
......
...@@ -7,7 +7,11 @@ import { createNotificationChannel, updateNotificationChannel } from "../../api/ ...@@ -7,7 +7,11 @@ import { createNotificationChannel, updateNotificationChannel } from "../../api/
export const load = async ({ params }) => { export const load = async ({ params }) => {
const { id } = params; const { id } = params;
const user = await getUser(parseInt(id), true, true); const user = await getUser(parseInt(id), true, true);
return { user }; const channelTypes = {};
for(const [name, {key}] of Object.entries(ChannelType)){
channelTypes[name] = key;
}
return { user, channelTypes };
} }
export const actions = { export const actions = {
......
...@@ -40,4 +40,4 @@ import { addMessage, MessageType } from "$lib/messages"; ...@@ -40,4 +40,4 @@ import { addMessage, MessageType } from "$lib/messages";
</form> </form>
<h2>Benachrichtigungen</h2> <h2>Benachrichtigungen</h2>
<NotificationChannelList bind:notificationChannels /> <NotificationChannelList bind:notificationChannels channelTypes={data.channelTypes} />
<script> <script>
import { ChannelType } from "$lib/notifications/channelTypes";
import { NotificationType } from "$lib/notifications/notificationTypes"; import { NotificationType } from "$lib/notifications/notificationTypes";
import BitfieldSelector from "../../../../components/BitfieldSelector.svelte"; import BitfieldSelector from "../../../../components/BitfieldSelector.svelte";
export let id, channelType, notificationTypes, recipient, edit, deleteChannel; export let id, channelType, notificationTypes, recipient, edit, deleteChannel, channelTypes;
</script> </script>
{#if edit} {#if edit}
<tr> <tr>
<td> <td>
<select name="channelType" form="notif-form{id}" required> <select name="channelType" form="notif-form{id}" required>
{#each Object.entries(ChannelType) as [name, {key}]} {#each Object.entries(channelTypes) as [name, key]}
<option value={key} selected={key==channelType}>{name}</option> <option value={key} selected={key==channelType}>{name}</option>
{/each} {/each}
</select> </select>
...@@ -21,7 +20,7 @@ ...@@ -21,7 +20,7 @@
</tr> </tr>
{:else} {:else}
<tr> <tr>
<td>{Object.entries(ChannelType).find(([,type])=>type.key==channelType)?.[0]}</td> <td>{Object.entries(channelTypes).find(([,type])=>type==channelType)?.[0]}</td>
<td><span title={Object.values(NotificationType).filter(t=>t.key&notificationTypes).map(t=>t.name).join(", ")}>{notificationTypes}</span></td> <td><span title={Object.values(NotificationType).filter(t=>t.key&notificationTypes).map(t=>t.name).join(", ")}>{notificationTypes}</span></td>
<td>{recipient}</td> <td>{recipient}</td>
<td><button type="button" on:click={() => edit = true}>Bearbeiten</button><button on:click={deleteChannel}>Löschen</button></td> <td><button type="button" on:click={() => edit = true}>Bearbeiten</button><button on:click={deleteChannel}>Löschen</button></td>
......
<script> <script>
import { enhance } from "$app/forms"; import { enhance } from "$app/forms";
import { MessageType, addMessage } from "$lib/messages"; import { MessageType, addMessage } from "$lib/messages";
import { ChannelType } from "$lib/notifications/channelTypes";
import { NotificationType } from "$lib/notifications/notificationTypes"; import { NotificationType } from "$lib/notifications/notificationTypes";
import BitfieldSelector from "../../../../components/BitfieldSelector.svelte"; import BitfieldSelector from "../../../../components/BitfieldSelector.svelte";
import NotificationChannelItem from "./NotificationChannelItem.svelte"; import NotificationChannelItem from "./NotificationChannelItem.svelte";
...@@ -9,6 +8,8 @@ ...@@ -9,6 +8,8 @@
/** @type {import("$lib/notifications/types").NotificationChannel[]} */ /** @type {import("$lib/notifications/types").NotificationChannel[]} */
export let notificationChannels; export let notificationChannels;
let edit = notificationChannels.map(() => false); let edit = notificationChannels.map(() => false);
/** @type {Object<string, number>} */
export let channelTypes;
function deleteChannel(id){ function deleteChannel(id){
fetch("/admin/api/notificationchannel", { fetch("/admin/api/notificationchannel", {
...@@ -41,12 +42,12 @@ ...@@ -41,12 +42,12 @@
<th></th> <th></th>
</tr> </tr>
{#each notificationChannels as channel, i} {#each notificationChannels as channel, i}
<NotificationChannelItem {...channel} bind:edit={edit[i]} deleteChannel={()=>deleteChannel(channel.id)} /> <NotificationChannelItem {...channel} bind:edit={edit[i]} deleteChannel={()=>deleteChannel(channel.id)} {channelTypes} />
{/each} {/each}
<tr> <tr>
<td> <td>
<select name="channelType" form="notif-form-new" required> <select name="channelType" form="notif-form-new" required>
{#each Object.entries(ChannelType) as [name, {key}]} {#each Object.entries(channelTypes) as [name, key]}
<option value={key}>{name}</option> <option value={key}>{name}</option>
{/each} {/each}
</select> </select>
......
...@@ -6,6 +6,6 @@ export async function getUser(card) { ...@@ -6,6 +6,6 @@ export async function getUser(card) {
return Object.assign(usercard.user, {card: usercard.card, perms: usercard.perms}); return Object.assign(usercard.user, {card: usercard.card, perms: usercard.perms});
} }
export async function getUserById(id, includeCards=false) { export async function getUserById(id, includeCards=false, includeNotificationChannels=false) {
return db.user.findUnique({where: {id}, include: {cards: includeCards}}); return db.user.findUnique({where: {id}, include: {cards: includeCards, notificationChannels: includeNotificationChannels}});
} }
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment