Skip to content

Commit

Permalink
Merge pull request #104 from david-loe/david-loe/issue101
Browse files Browse the repository at this point in the history
Add magiclogin token to email links
  • Loading branch information
david-loe authored Oct 5, 2024
2 parents 4fa6d3f + 9b5627d commit 4497be4
Show file tree
Hide file tree
Showing 6 changed files with 90 additions and 25 deletions.
39 changes: 28 additions & 11 deletions backend/authStrategies/magiclogin.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,20 @@
import { default as MagicLoginStrategy } from 'passport-magic-login'
import jwt from 'jsonwebtoken'
import MagicLoginStrategy from 'passport-magic-login'
import { escapeRegExp } from '../../common/scripts.js'
import { NotAllowedError } from '../controller/error.js'
import i18n from '../i18n.js'
import { sendMail } from '../mail/mail.js'
import User from '../models/user.js'

const magicLogin = new MagicLoginStrategy.default({
secret: process.env.MAGIC_LOGIN_SECRET,
callbackUrl: process.env.VITE_BACKEND_URL + '/auth/magiclogin/callback',
const secret = process.env.MAGIC_LOGIN_SECRET
const callbackUrl = process.env.VITE_BACKEND_URL + '/auth/magiclogin/callback'
const jwtOptions = {
expiresIn: 60 * 120 // in seconds -> 120min
}

export default new MagicLoginStrategy.default({
secret: secret,
callbackUrl: callbackUrl,
sendMagicLink: async (destination, href) => {
var user = await User.findOne({ 'fk.magiclogin': { $regex: new RegExp('^' + escapeRegExp(destination) + '$', 'i') } }).lean()
if (user) {
Expand All @@ -16,23 +23,33 @@ const magicLogin = new MagicLoginStrategy.default({
'Login abrechnung🧾',
i18n.t('mail.magiclogin.paragraph', { lng: user.settings.language }),
{ text: i18n.t('mail.magiclogin.buttonText', { lng: user.settings.language }), link: href },
''
'',
false
)
} else {
throw new NotAllowedError('No magiclogin user found for e-mail: ' + destination)
}
},
verify: async function (payload, callback) {
var user = await User.findOne({ 'fk.magiclogin': { $regex: new RegExp('^' + escapeRegExp(payload.destination) + '$', 'i') } }).lean()
if (user) {
var user = await User.findOne({ 'fk.magiclogin': { $regex: new RegExp('^' + escapeRegExp(payload.destination) + '$', 'i') } })
if (user && (await user.isActive())) {
callback(null, user, { redirect: payload.redirect })
} else {
callback(new NotAllowedError('No magiclogin user found for e-mail: ' + payload.destination))
}
},
jwtOptions: {
expiresIn: '30m'
}
jwtOptions: jwtOptions
})

export default magicLogin
export function genAuthenticatedLink(payload: { destination: string; redirect: string }) {
return new Promise<string>((resolve, reject) => {
const code = Math.floor(Math.random() * 90000) + 10000 + ''
jwt.sign({ ...payload, code }, secret, jwtOptions, (err, token) => {
if (err) {
reject(err)
} else {
resolve(`${callbackUrl}?token=${token}`)
}
})
})
}
17 changes: 16 additions & 1 deletion backend/controller/authController.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Request as ExRequest, Response as ExResponse, NextFunction } from 'express'
import jwt from 'jsonwebtoken'
import passport from 'passport'
import { Body, Controller, Delete, Get, Middlewares, Post, Query, Request, Response, Route, Security, SuccessResponse, Tags } from 'tsoa'
import { Base64, escapeRegExp } from '../../common/scripts.js'
Expand Down Expand Up @@ -38,7 +39,21 @@ const magicloginHandler = useMagicLogin
}
: NotImplementedMiddleware

const magicloginCallbackHandler = useMagicLogin ? passport.authenticate('magiclogin') : NotImplementedMiddleware
const magicloginCallbackHandler = useMagicLogin
? (req: ExRequest, res: ExResponse, next: NextFunction) => {
let redirect: any
if (req.query.token) {
const token = jwt.decode(req.query.token as string) as jwt.JwtPayload
const redirectPath = token.redirect
if (redirectPath && typeof redirectPath === 'string' && redirectPath.startsWith('/')) {
redirect = redirectPath
}
}
passport.authenticate('magiclogin', {
failureRedirect: process.env.VITE_FRONTEND_URL + '/login' + (redirect ? '?redirect=' + redirect : '')
})(req, res, next)
}
: NotImplementedMiddleware

@Tags('Authentication')
@Route('auth')
Expand Down
43 changes: 31 additions & 12 deletions backend/mail/mail.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,25 +10,44 @@ import {
reportIsHealthCareCost,
reportIsTravel
} from '../../common/types.js'
import { genAuthenticatedLink } from '../authStrategies/magiclogin.js'
import i18n from '../i18n.js'
import User from '../models/user.js'
import mailClient from './client.js'

export function sendMail(
export async function sendMail(
recipients: IUser[],
subject: string,
paragaph: string,
paragraph: string,
button: { text: string; link: string },
lastParagraph: string
lastParagraph: string,
authenticateLink = true
) {
if (mailClient == undefined || recipients.length === 0) {
return false
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)
})
}
_sendMail(recipients[i], subject, paragraph, recipientButton, lastParagraph, language)
}
const language = recipients[0].settings.language
var salutation = i18n.t('mail.hi', { lng: language })
if (recipients.length === 1) {
salutation = i18n.t('mail.hiX', { lng: language, X: recipients[0].name.givenName })
}

function _sendMail(
recipient: IUser,
subject: string,
paragraph: string,
button: { text: string; link: string },
lastParagraph: string,
language: Locale
) {
if (mailClient == undefined) {
return
}
const salutation = i18n.t('mail.hiX', { lng: language, X: recipient.name.givenName })
const regards = i18n.t('mail.regards', { lng: language })
const app = {
name: i18n.t('headlines.title', { lng: language }) + ' ' + i18n.t('headlines.emoji', { lng: language }),
Expand All @@ -38,7 +57,7 @@ export function sendMail(
const template = fs.readFileSync('./templates/mail.ejs', { encoding: 'utf-8' })
const renderedHTML = ejs.render(template, {
salutation,
paragaph,
paragraph,
button,
lastParagraph,
regards,
Expand All @@ -47,7 +66,7 @@ export function sendMail(
const plainText =
salutation +
'\n\n' +
paragaph +
paragraph +
'\n\n' +
button.text +
': ' +
Expand All @@ -63,7 +82,7 @@ export function sendMail(

mailClient.sendMail({
from: '"' + app.name + '" <' + process.env.MAIL_SENDER_ADDRESS + '>', // sender address
to: recipients.map((r) => r.email), // list of receivers
to: recipient.email, // list of receivers
subject: subject, // Subject line
text: plainText, // plain text body
html: renderedHTML // html body
Expand Down
12 changes: 12 additions & 0 deletions backend/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions backend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
"express": "^5.0.0",
"express-session": "^1.18.0",
"i18next": "^23.15.1",
"jsonwebtoken": "^9.0.2",
"mongoose": "^8.7.0",
"multer": "^1.4.5-lts.1",
"nodemailer": "^6.9.15",
Expand All @@ -30,6 +31,7 @@
"@types/ejs": "^3.1.5",
"@types/express": "^4.17.21",
"@types/express-session": "^1.18.0",
"@types/jsonwebtoken": "^9.0.7",
"@types/multer": "^1.4.12",
"@types/node": "^20.16.10",
"@types/nodemailer": "^6.4.16",
Expand Down
2 changes: 1 addition & 1 deletion backend/templates/mail.ejs
Original file line number Diff line number Diff line change
Expand Up @@ -349,7 +349,7 @@
<tr>
<td>
<p><%= salutation %></p>
<p><%= paragaph %></p>
<p><%= paragraph %></p>
<table role="presentation" border="0" cellpadding="0" cellspacing="0" class="btn btn-primary">
<tbody>
<tr>
Expand Down

0 comments on commit 4497be4

Please sign in to comment.