Skip to content

Commit

Permalink
Merge pull request #121 from david-loe/david-loe/issue117
Browse files Browse the repository at this point in the history
feat: send reports to organisation email
  • Loading branch information
david-loe authored Nov 21, 2024
2 parents 430600b + 46a8102 commit d4cce41
Show file tree
Hide file tree
Showing 34 changed files with 216 additions and 92 deletions.
1 change: 1 addition & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -35,5 +35,6 @@ RATE_LIMIT=
# TRUE | number | string (https://expressjs.com/en/guide/behind-proxies.html) use BACKEND_URL/ip to validate
TRUST_PROXY=

# ⚠️Deprecated⚠️ Use send via mail under connection settings
# If set to 'TRUE', all reports will be saved to `/reports` in the backend container. Uncomment the corresponding backend volume in `docker-compose.yml` to get reports on host maschine
BACKEND_SAVE_REPORTS_ON_DISK=FALSE
2 changes: 1 addition & 1 deletion backend/authStrategies/magiclogin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export default new MagicLoginStrategy.default({
i18n.t('mail.magiclogin.subject', { lng: user.settings.language }),
i18n.t('mail.magiclogin.paragraph', { lng: user.settings.language }),
{ text: i18n.t('mail.magiclogin.buttonText', { lng: user.settings.language }), link: href },
'',
undefined,
false
)
} else {
Expand Down
3 changes: 2 additions & 1 deletion backend/controller/expenseReportController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { sendNotificationMail } from '../mail/mail.js'
import ExpenseReport, { ExpenseReportDoc } from '../models/expenseReport.js'
import User from '../models/user.js'
import { generateExpenseReportReport } from '../pdf/expenseReport.js'
import { writeToDiskFilePath } from '../pdf/helper.js'
import { sendViaMail, writeToDiskFilePath } from '../pdf/helper.js'
import { Controller, GetterQuery, SetterBody } from './controller.js'
import { AuthorizationError, NotFoundError } from './error.js'
import { IdDocument, MoneyPost } from './types.js'
Expand Down Expand Up @@ -273,6 +273,7 @@ export class ExpenseReportExamineController extends Controller {

const cb = async (expenseReport: IExpenseReport) => {
sendNotificationMail(expenseReport)
sendViaMail(expenseReport)
if (process.env.BACKEND_SAVE_REPORTS_ON_DISK.toLowerCase() === 'true') {
await writeToDisk(
await writeToDiskFilePath(expenseReport),
Expand Down
4 changes: 3 additions & 1 deletion backend/controller/healthCareCostController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import HealthCareCost, { HealthCareCostDoc } from '../models/healthCareCost.js'
import Organisation from '../models/organisation.js'
import User from '../models/user.js'
import { generateHealthCareCostReport } from '../pdf/healthCareCost.js'
import { writeToDiskFilePath } from '../pdf/helper.js'
import { sendViaMail, writeToDiskFilePath } from '../pdf/helper.js'
import { Controller, GetterQuery, SetterBody } from './controller.js'
import { AuthorizationError, NotAllowedError } from './error.js'
import { IdDocument, MoneyPlusPost } from './types.js'
Expand Down Expand Up @@ -251,6 +251,7 @@ export class HealthCareCostExamineController extends Controller {

const cb = async (healthCareCost: IHealthCareCost) => {
sendNotificationMail(healthCareCost)
sendViaMail(healthCareCost)
if (process.env.BACKEND_SAVE_REPORTS_ON_DISK.toLowerCase() === 'true') {
await writeToDisk(
await writeToDiskFilePath(healthCareCost),
Expand Down Expand Up @@ -371,6 +372,7 @@ export class HealthCareCostConfirmController extends Controller {

const cb = async (healthCareCost: IHealthCareCost) => {
sendNotificationMail(healthCareCost)
sendViaMail(healthCareCost)
if (process.env.BACKEND_SAVE_REPORTS_ON_DISK.toLowerCase() === 'true') {
await writeToDisk(
await writeToDiskFilePath(healthCareCost),
Expand Down
14 changes: 7 additions & 7 deletions backend/controller/travelController.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { sendNotificationMail } from '../mail/mail.js'
import Travel, { TravelDoc } from '../models/travel.js'
import User from '../models/user.js'
import { generateAdvanceReport } from '../pdf/advance.js'
import { writeToDiskFilePath } from '../pdf/helper.js'
import { sendViaMail, writeToDiskFilePath } from '../pdf/helper.js'
import { generateTravelReport } from '../pdf/travel.js'
import { Controller, GetterQuery, SetterBody } from './controller.js'
import { AuthorizationError, NotAllowedError } from './error.js'
Expand Down Expand Up @@ -278,12 +278,11 @@ export class TravelApproveController extends Controller {
}
const cb = async (travel: ITravel) => {
sendNotificationMail(travel)
if (
travel.advance.amount !== null &&
travel.advance.amount > 0 &&
process.env.BACKEND_SAVE_REPORTS_ON_DISK.toLowerCase() === 'true'
) {
await writeToDisk(await writeToDiskFilePath(travel), await generateAdvanceReport(travel, i18n.language as Locale))
if (travel.advance.amount !== null && travel.advance.amount > 0) {
sendViaMail(travel)
if (process.env.BACKEND_SAVE_REPORTS_ON_DISK.toLowerCase() === 'true') {
await writeToDisk(await writeToDiskFilePath(travel), await generateAdvanceReport(travel, i18n.language as Locale))
}
}
}

Expand Down Expand Up @@ -448,6 +447,7 @@ export class TravelExamineController extends Controller {

const cb = async (travel: ITravel) => {
sendNotificationMail(travel)
sendViaMail(travel)
if (process.env.BACKEND_SAVE_REPORTS_ON_DISK.toLowerCase() === 'true') {
await writeToDisk(await writeToDiskFilePath(travel), await generateTravelReport(travel, i18n.language as Locale))
}
Expand Down
4 changes: 2 additions & 2 deletions backend/controller/uploadController.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,13 @@
import ejs from 'ejs'
import { Request as ExRequest, Response as ExResponse, NextFunction } from 'express'
import fs from 'node:fs/promises'
import { Body, Consumes, Controller, Get, Middlewares, Post, Produces, Query, Request, Route, SuccessResponse } from 'tsoa'
import { _id } from '../../common/types.js'
import { getSettings } from '../db.js'
import { documentFileHandler, fileHandler } from '../helper.js'
import i18n from '../i18n.js'
import Token from '../models/token.js'
import User from '../models/user.js'
import { getUploadTemplate } from '../templates/cache.js'
import { AuthorizationError, NotFoundError } from './error.js'
import { File } from './types.js'

Expand All @@ -33,7 +33,7 @@ export class UploadController extends Controller {
): Promise<void> {
const settings = await getSettings()
const user = await User.findOne({ _id: userId }).lean()
const template = await fs.readFile('./templates/upload.ejs', { encoding: 'utf-8' })
const template = await getUploadTemplate()
const url = new URL(process.env.VITE_BACKEND_URL + '/upload/new')
url.searchParams.append('userId', userId)
url.searchParams.append('tokenId', tokenId)
Expand Down
4 changes: 4 additions & 0 deletions backend/data/connectionSettings.development.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,5 +22,9 @@
"user": "username",
"password": "password",
"senderAddress": "[email protected]"
},
"PDFReportsViaEmail": {
"sendPDFReportsToOrganisationEmail": false,
"locale": "de"
}
}
7 changes: 2 additions & 5 deletions backend/db.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,9 @@ export async function initDB() {
}

if (process.env.NODE_ENV === 'development') {
await initer(ConnectionSettings, 'connectionSettings', [connectionSettingsDevelopment], true)
await initer(ConnectionSettings, 'connectionSettings', [connectionSettingsDevelopment as Partial<IConnectionSettings>], true)
} else {
const emtpyConnectionSettings: Partial<IConnectionSettings> = {
auth: {}
}
await initer(ConnectionSettings, 'connectionSettings', [emtpyConnectionSettings])
await initer(ConnectionSettings, 'connectionSettings', [{} as Partial<IConnectionSettings>])
}

await initer(DisplaySettings, 'displaySettings', [displaySettings as Partial<IDisplaySettings>])
Expand Down
42 changes: 22 additions & 20 deletions backend/mail/mail.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import ejs from 'ejs'
import fs from 'fs'
import nodemailer from 'nodemailer'
import {
ExpenseReportSimple,
Expand All @@ -16,8 +15,9 @@ import { genAuthenticatedLink } from '../helper.js'
import i18n, { formatter } from '../i18n.js'
import User from '../models/user.js'
import { mapSmtpConfig } from '../settingsValidator.js'
import { getMailTemplate } from '../templates/cache.js'

async function getClient() {
export async function getClient() {
const connectionSettings = await getConnectionSettings()
if (connectionSettings.smtp?.host) {
return nodemailer.createTransport(mapSmtpConfig(connectionSettings.smtp))
Expand All @@ -30,30 +30,35 @@ export async function sendMail(
recipients: IUser[],
subject: string,
paragraph: string,
button: { text: string; link: string },
lastParagraph: string,
button?: { text: string; link: string },
lastParagraph?: string,
authenticateLink = true
) {
const mailPromises = []
for (let i = 0; i < recipients.length; i++) {
const language = recipients[i].settings.language
const recipientButton = { ...button }
if (authenticateLink && recipients[i].fk.magiclogin && recipientButton.link.startsWith(process.env.VITE_FRONTEND_URL)) {
recipientButton.link = await genAuthenticatedLink({
destination: recipients[i].fk.magiclogin!,
redirect: recipientButton.link.substring(process.env.VITE_FRONTEND_URL.length)
})
let recipientButton: { text: string; link: string } | undefined = undefined
if (button) {
recipientButton = { ...button }
if (authenticateLink && recipients[i].fk.magiclogin && recipientButton.link.startsWith(process.env.VITE_FRONTEND_URL)) {
recipientButton.link = await genAuthenticatedLink({
destination: recipients[i].fk.magiclogin!,
redirect: recipientButton.link.substring(process.env.VITE_FRONTEND_URL.length)
})
}
}
_sendMail(recipients[i], subject, paragraph, recipientButton, lastParagraph, language)
mailPromises.push(_sendMail(recipients[i], subject, paragraph, language, recipientButton, lastParagraph))
}
return await Promise.allSettled(mailPromises)
}

async function _sendMail(
recipient: IUser,
subject: string,
paragraph: string,
button: { text: string; link: string },
lastParagraph: string,
language: Locale
language: Locale,
button?: { text: string; link: string },
lastParagraph?: string
) {
const mailClient = await getClient()
const salutation = i18n.t('mail.hiX', { lng: language, X: recipient.name.givenName })
Expand All @@ -63,7 +68,7 @@ async function _sendMail(
url: process.env.VITE_FRONTEND_URL
}

const template = fs.readFileSync('./templates/mail.ejs', { encoding: 'utf-8' })
const template = await getMailTemplate()
const renderedHTML = ejs.render(template, {
salutation,
paragraph,
Expand All @@ -77,10 +82,7 @@ async function _sendMail(
'\n\n' +
paragraph +
'\n\n' +
button.text +
': ' +
button.link +
'\n\n' +
(button ? button.text + ': ' + button.link + '\n\n' : '') +
lastParagraph +
'\n\n' +
regards +
Expand All @@ -89,7 +91,7 @@ async function _sendMail(
': ' +
app.url

mailClient.sendMail({
return await mailClient.sendMail({
from: '"' + app.name + '" <' + mailClient.options.from + '>', // sender address
to: recipient.email, // list of receivers
subject: subject, // Subject line
Expand Down
10 changes: 10 additions & 0 deletions backend/migrations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,16 @@ export async function checkForMigrations() {
}
await mongoose.connection.collection('displaysettings').updateOne({}, { $set: displaySettingsFromEnv })
}
if (semver.lte(migrateFrom, '1.4.1')) {
console.log('Apply migration from v1.4.1: Add PDFReportsViaEmail Settings')
const displaySettings = await mongoose.connection.collection('displaysettings').findOne({})
await mongoose.connection
.collection('connectionsettings')
.updateOne(
{},
{ $set: { PDFReportsViaEmail: { sendPDFReportsToOrganisationEmail: false, locale: displaySettings?.locale?.default || 'de' } } }
)
}
if (settings) {
settings.migrateFrom = undefined
await settings.save()
Expand Down
16 changes: 13 additions & 3 deletions backend/models/connectionSettings.ts
Original file line number Diff line number Diff line change
@@ -1,12 +1,21 @@
import { HydratedDocument, model, Schema } from 'mongoose'
import { ConnectionSettings, emailRegex } from '../../common/types.js'
import { ConnectionSettings, defaultLocale, emailRegex, locales } from '../../common/types.js'
import { verifyLdapauthConfig, verifySmtpConfig } from '../settingsValidator.js'

function requiredIf(ifPath: string) {
return [{ required: [ifPath, 'not_in', [null, '']] }, { nullable: [ifPath, 'in', [null, '']] }]
return [{ required: [ifPath, 'not_in', [null, '', false]] }, { nullable: [ifPath, 'in', [null, '', false]] }]
}

export const connectionSettingsSchema = new Schema<ConnectionSettings>({
PDFReportsViaEmail: {
type: {
sendPDFReportsToOrganisationEmail: { type: Boolean, default: false, required: true },
locale: { type: String, enum: locales, required: true, default: defaultLocale }
},
required: true,
default: () => ({}),
label: 'PDF via Email'
},
smtp: {
type: {
host: { type: String, trim: true, required: true, label: 'Host', rules: requiredIf('smtp.user') },
Expand Down Expand Up @@ -93,7 +102,8 @@ export const connectionSettingsSchema = new Schema<ConnectionSettings>({
label: 'LDAP'
}
},
required: true
required: true,
default: () => ({})
}
})

Expand Down
9 changes: 4 additions & 5 deletions backend/models/displaySettings.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { model, Schema } from 'mongoose'
import { DisplaySettings, Locale, locales } from '../../common/types.js'
import { defaultLocale, DisplaySettings, Locale, locales } from '../../common/types.js'

const overwrites = {} as { [key in Locale]: { type: typeof Schema.Types.Mixed; required: true; default: () => object } }
for (const locale of locales) {
Expand All @@ -26,12 +26,11 @@ export const displaySettingsSchema = new Schema<DisplaySettings>(
},
locale: {
type: {
default: { type: String, enum: locales, required: true },
fallback: { type: String, enum: locales, required: true },
default: { type: String, enum: locales, required: true, default: defaultLocale },
fallback: { type: String, enum: locales, required: true, default: defaultLocale },
overwrite: { type: overwrites, required: true, description: 'description.keyFullIdentifier' }
},
required: true,
label: 'Sprache'
required: true
}
},
{ minimize: false, toObject: { minimize: false }, toJSON: { minimize: false } }
Expand Down
9 changes: 5 additions & 4 deletions backend/models/organisation.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
import { Document, Query, Schema, model } from 'mongoose'
import { Organisation } from '../../common/types.js'
import { Document, model, Query, Schema } from 'mongoose'
import { emailRegex, Organisation } from '../../common/types.js'

export const organisationSchema = new Schema<Organisation>({
name: { type: String, trim: true, required: true },
subfolderPath: { type: String, trim: true, default: '' },
reportEmail: { type: String, validate: emailRegex },
website: { type: String },
bankDetails: { type: String },
companyNumber: { type: String, trim: true },
logo: { type: Schema.Types.ObjectId, ref: 'DocumentFile' },
website: { type: String }
subfolderPath: { type: String, trim: true, default: '' }
})

function populate(doc: Document) {
Expand Down
4 changes: 2 additions & 2 deletions backend/pdf/advance.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import fs from 'fs'
import fs from 'fs/promises'
import pdf_fontkit from 'pdf-fontkit'
import pdf_lib from 'pdf-lib'
import { Locale, Money, TravelSimple } from '../../common/types.js'
Expand All @@ -9,7 +9,7 @@ export async function generateAdvanceReport(travel: TravelSimple, language: Loca
formatter.setLocale(language)
const pdfDoc = await pdf_lib.PDFDocument.create()
pdfDoc.registerFontkit(pdf_fontkit)
const fontBytes = fs.readFileSync('../common/fonts/NotoSans-Regular.ttf')
const fontBytes = await fs.readFile('../common/fonts/NotoSans-Regular.ttf')
const font = await pdfDoc.embedFont(fontBytes, { subset: true })
const edge = 36
const fontSize = 11
Expand Down
4 changes: 2 additions & 2 deletions backend/pdf/expenseReport.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import fs from 'fs'
import fs from 'fs/promises'
import pdf_fontkit from 'pdf-fontkit'
import pdf_lib from 'pdf-lib'
import { addUp } from '../../common/scripts.js'
Expand All @@ -21,7 +21,7 @@ export async function generateExpenseReportReport(expenseReport: ExpenseReport,
formatter.setLocale(language)
const pdfDoc = await pdf_lib.PDFDocument.create()
pdfDoc.registerFontkit(pdf_fontkit)
const fontBytes = fs.readFileSync('../common/fonts/NotoSans-Regular.ttf')
const fontBytes = await fs.readFile('../common/fonts/NotoSans-Regular.ttf')
const font = await pdfDoc.embedFont(fontBytes, { subset: true })
const edge = 36
const fontSize = 11
Expand Down
4 changes: 2 additions & 2 deletions backend/pdf/healthCareCost.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import fs from 'fs'
import fs from 'fs/promises'
import pdf_fontkit from 'pdf-fontkit'
import pdf_lib from 'pdf-lib'
import { addUp } from '../../common/scripts.js'
Expand All @@ -21,7 +21,7 @@ export async function generateHealthCareCostReport(healthCareCost: HealthCareCos
formatter.setLocale(language)
const pdfDoc = await pdf_lib.PDFDocument.create()
pdfDoc.registerFontkit(pdf_fontkit)
const fontBytes = fs.readFileSync('../common/fonts/NotoSans-Regular.ttf')
const fontBytes = await fs.readFile('../common/fonts/NotoSans-Regular.ttf')
const font = await pdfDoc.embedFont(fontBytes, { subset: true })
const edge = 36
const fontSize = 11
Expand Down
Loading

0 comments on commit d4cce41

Please sign in to comment.