fix(api/webauthn): temporary disable mail before jsx-email lib upgrade

This commit is contained in:
Julien Oculi 2025-06-23 11:33:38 +02:00
parent d461e53a00
commit 1cc3ce65eb

View file

@ -1,178 +1,178 @@
import 'npm:iterator-polyfill' import 'npm:iterator-polyfill'
import { define } from '../../../utils.ts' import { define } from '../../../utils.ts'
// Polyfill AsyncIterator // Polyfill AsyncIterator
import { db } from ':src/db/mod.ts' // import { db } from ':src/db/mod.ts'
import { SessionStore } from ':src/session/mod.ts' // import { SessionStore } from ':src/session/mod.ts'
import { respondApi } from ':src/utils.ts' // import { respondApi } from ':src/utils.ts'
import { Contact, type Mail, send } from '@cohabit/mailer' // import { Contact, type Mail, send } from '@cohabit/mailer'
import { magicLinkTemplate } from '@cohabit/mailer/templates' // import { magicLinkTemplate } from '@cohabit/mailer/templates'
import { User } from '@cohabit/resources-manager/models' // import { User } from '@cohabit/resources-manager/models'
import { sleep } from '@jotsr/delayed' // import { sleep } from '@jotsr/delayed'
type MagicLinkInfos = { // type MagicLinkInfos = {
remoteId: string // remoteId: string
email: string // email: string
timestamp: number // timestamp: number
} // }
export async function getUserByMail(email: string): Promise<User | undefined> { // export async function getUserByMail(email: string): Promise<User | undefined> {
const [user] = await db.resource.user // const [user] = await db.resource.user
.list((user) => user.mail === email) // .list((user) => user.mail === email)
.take(1) // .take(1)
.toArray() // .toArray()
return user // return user
} // }
// export const handler = define.handlers({}) export const handler = define.handlers({})
export const handler = define.handlers({ // export const _handler = define.handlers({
async POST(ctx) { // async POST(ctx) {
const request = ctx.req // const request = ctx.req
const { email } = await request.json() as { email: string } // const { email } = await request.json() as { email: string }
// check email before continue // // check email before continue
if (!/\S+@\S+\.\S+/.test(email)) { // if (!/\S+@\S+\.\S+/.test(email)) {
return respondApi('error', new SyntaxError('empty or invalid email'), 400) // return respondApi('error', new SyntaxError('empty or invalid email'), 400)
} // }
const user = await getUserByMail(email) // const user = await getUserByMail(email)
// generate magic link // // generate magic link
const token = crypto.randomUUID() // const token = crypto.randomUUID()
if (ctx.state.session === undefined) { // if (ctx.state.session === undefined) {
return respondApi( // return respondApi(
'error', // 'error',
new Error('missing server session for current user'), // new Error('missing server session for current user'),
500, // 500,
) // )
} // }
const remoteAddr = ctx.info.remoteAddr // const remoteAddr = ctx.info.remoteAddr
if (!('hostname' in remoteAddr)) { // if (!('hostname' in remoteAddr)) {
return respondApi('error', new Error('connection is not using http'), 500) // return respondApi('error', new Error('connection is not using http'), 500)
} // }
const endpoint = // const endpoint =
`${ctx.url.origin}/api/magiclink?token=${token}&session=${ctx.state.session.uuid}&redirect=/profil` // `${ctx.url.origin}/api/magiclink?token=${token}&session=${ctx.state.session.uuid}&redirect=/profil`
// save token to session // // save token to session
ctx.state.session.flash<MagicLinkInfos>(`MAGIC_LINK__${token}`, { // ctx.state.session.flash<MagicLinkInfos>(`MAGIC_LINK__${token}`, {
email, // email,
remoteId: remoteId(request, { remoteAddr }), // remoteId: remoteId(request, { remoteAddr }),
timestamp: Date.now(), // timestamp: Date.now(),
}) // })
// send mail to user // // send mail to user
try { // try {
if (user) { // if (user) {
// Get user ip through proxy else from tcp connection // // Get user ip through proxy else from tcp connection
const ip = request.headers.get('X-FORWARDED-FOR') ?? remoteAddr.hostname // const ip = request.headers.get('X-FORWARDED-FOR') ?? remoteAddr.hostname
const device = request.headers.get('Sec-Ch-Ua-Platform') ?? undefined // const device = request.headers.get('Sec-Ch-Ua-Platform') ?? undefined
await sendMagicLink(user, { device, ip, endpoint }) // await sendMagicLink(user, { device, ip, endpoint })
} else { // } else {
//! perform wait to prevent time attacks // //! perform wait to prevent time attacks
await sleep(Math.random() * 5_000 + 2_000) //between 2s and 7s // await sleep(Math.random() * 5_000 + 2_000) //between 2s and 7s
} // }
return respondApi('success') // return respondApi('success')
} catch (error) { // } catch (error) {
console.error('MAGIC_LINK_SENDING', error) // console.error('MAGIC_LINK_SENDING', error)
return respondApi( // return respondApi(
'error', // 'error',
new Error(`unable to send mail to ${email}`), // new Error(`unable to send mail to ${email}`),
500, // 500,
) // )
} // }
}, // },
async GET(ctx) { // async GET(ctx) {
const request = ctx.req // const request = ctx.req
const token = ctx.url.searchParams.get('token') // const token = ctx.url.searchParams.get('token')
const redirect = ctx.url.searchParams.get('redirect') // const redirect = ctx.url.searchParams.get('redirect')
const sessionId = ctx.url.searchParams.get('session') // const sessionId = ctx.url.searchParams.get('session')
// no token or sessionId // // no token or sessionId
if (token === null || sessionId === null) { // if (token === null || sessionId === null) {
return respondApi('error', 'no token or session provided', 400) // return respondApi('error', 'no token or session provided', 400)
} // }
// set session if 3rd party cookies was blocked // // set session if 3rd party cookies was blocked
ctx.state.session = ctx.state.session ?? SessionStore.getSession(sessionId) // ctx.state.session = ctx.state.session ?? SessionStore.getSession(sessionId)
// no session available // // no session available
if (ctx.state.session === null || ctx.state.session === undefined) { // if (ctx.state.session === null || ctx.state.session === undefined) {
return respondApi('error', 'no session datas', 401) // return respondApi('error', 'no session datas', 401)
} // }
// wrong or timeout token // // wrong or timeout token
const entry = ctx.state.session.get<MagicLinkInfos>(`MAGIC_LINK__${token}`) // const entry = ctx.state.session.get<MagicLinkInfos>(`MAGIC_LINK__${token}`)
const lifespan = Date.now() - 10 * 60 * 1_000 // ten minutes // const lifespan = Date.now() - 10 * 60 * 1_000 // ten minutes
if (entry === undefined || entry.timestamp < lifespan) { // if (entry === undefined || entry.timestamp < lifespan) {
return respondApi('error', 'wrong token or timeout exceeded', 401) // return respondApi('error', 'wrong token or timeout exceeded', 401)
} // }
const remoteAddr = ctx.info.remoteAddr // const remoteAddr = ctx.info.remoteAddr
if (!('hostname' in remoteAddr)) { // if (!('hostname' in remoteAddr)) {
return respondApi('error', new Error('connection is not using http'), 500) // return respondApi('error', new Error('connection is not using http'), 500)
} // }
// check remote id (same user/machine that has query the token) // // check remote id (same user/machine that has query the token)
if (entry.remoteId === remoteId(request, { remoteAddr })) { // if (entry.remoteId === remoteId(request, { remoteAddr })) {
const user = await getUserByMail(entry.email) // const user = await getUserByMail(entry.email)
ctx.state.session.set('user', user) // ctx.state.session.set('user', user)
if (redirect) { // if (redirect) {
return Response.redirect(new URL(redirect, ctx.url.origin)) // return Response.redirect(new URL(redirect, ctx.url.origin))
} // }
return respondApi('success', user) // return respondApi('success', user)
} // }
return respondApi( // return respondApi(
'error', // 'error',
new Error( // new Error(
'invalid id, use the same device/ip to query token and verify token', // 'invalid id, use the same device/ip to query token and verify token',
), // ),
401, // 401,
) // )
}, // },
}) // })
function remoteId( // function remoteId(
{ headers }: { headers: Headers }, // { headers }: { headers: Headers },
{ remoteAddr }: { remoteAddr: Deno.NetAddr }, // { remoteAddr }: { remoteAddr: Deno.NetAddr },
): string { // ): string {
const forwardedAddress = headers.get('X-FORWARDED-FOR') // const forwardedAddress = headers.get('X-FORWARDED-FOR')
const forwardedProto = headers.get('X-FORWARDED-PROTO') // const forwardedProto = headers.get('X-FORWARDED-PROTO')
if (forwardedAddress && forwardedProto) { // if (forwardedAddress && forwardedProto) {
return `${forwardedProto}://${forwardedAddress}` // return `${forwardedProto}://${forwardedAddress}`
} // }
return `(${remoteAddr.transport}):${remoteAddr.hostname}:${remoteAddr.port}` // return `(${remoteAddr.transport}):${remoteAddr.hostname}:${remoteAddr.port}`
} // }
async function sendMagicLink( // async function sendMagicLink(
{ firstname, lastname, mail }: User, // { firstname, lastname, mail }: User,
{ device, ip, endpoint }: { device?: string; ip?: string; endpoint: string }, // { device, ip, endpoint }: { device?: string; ip?: string; endpoint: string },
): Promise<void> { // ): Promise<void> {
const message: Mail = { // const message: Mail = {
from: Contact.expand('contact'), // from: Contact.expand('contact'),
to: [Contact.fromString(`${firstname} ${lastname} <${mail}>`)], // to: [Contact.fromString(`${firstname} ${lastname} <${mail}>`)],
subject: 'Lien de connection pour FabLab Coh@bit', // subject: 'Lien de connection pour FabLab Coh@bit',
body: magicLinkTemplate.builder({ // body: magicLinkTemplate.builder({
device, // device,
ip, // ip,
endpoint, // endpoint,
})!, // })!,
options: { // options: {
cc: [], // cc: [],
cci: [], // cci: [],
attachments: [], // attachments: [],
}, // },
} // }
await send(message) // await send(message)
} // }