verify-code.js
4.39 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
/**
* 验证码模板(纯函数):data → { subject, html, text }。
* 本模板不生成、不校验验证码——只渲染 auth 传入的 `code` 与 `ttlMin`。
*
* @typedef {Object} VerifyCodeData
* @property {string} code 验证码(auth 生成)
* @property {number} ttlMin 有效期(分钟)
* @property {'en'|'zh'|'ja'|'ko'|'es'|'pt'} locale
*/
import { EmailError } from '../map-error.js'
const COPY = {
en: {
subject: (code) => `VOKO verification code: ${code}`,
heading: 'Verify your VOKO email',
intro: 'Use the code below to continue. Do not share it with anyone.',
ttl: (min) => `This code expires in ${min} minute${min === 1 ? '' : 's'}.`,
ignore: 'If you did not request this, you can safely ignore this email.',
},
zh: {
subject: (code) => `VOKO 验证码:${code}`,
heading: '验证您的 VOKO 邮箱',
intro: '请使用下方验证码继续操作,请勿向任何人泄露。',
ttl: (min) => `该验证码将在 ${min} 分钟后失效。`,
ignore: '如果这不是您本人的操作,请忽略本邮件。',
},
ja: {
subject: (code) => `VOKO認証コード:${code}`,
heading: 'VOKOのメールアドレスを認証',
intro: '続行するには、以下のコードを使用してください。他の人と共有しないでください。',
ttl: (min) => `このコードは${min}分後に無効になります。`,
ignore: 'このリクエストに心当たりがない場合は、このメールを無視してください。',
},
ko: {
subject: (code) => `VOKO 인증 코드: ${code}`,
heading: 'VOKO 이메일 인증',
intro: '계속하려면 아래 코드를 사용하세요. 다른 사람과 공유하지 마세요.',
ttl: (min) => `이 코드는 ${min}분 후에 만료됩니다.`,
ignore: '본인이 요청하지 않았다면 이 이메일을 무시하세요.',
},
es: {
subject: (code) => `Código de verificación de VOKO: ${code}`,
heading: 'Verifica tu correo de VOKO',
intro: 'Usa el siguiente código para continuar. No lo compartas con nadie.',
ttl: (min) => `Este código caduca en ${min} minuto${min === 1 ? '' : 's'}.`,
ignore: 'Si no solicitaste este código, puedes ignorar este correo.',
},
pt: {
subject: (code) => `Código de verificação do VOKO: ${code}`,
heading: 'Verifique seu e-mail do VOKO',
intro: 'Use o código abaixo para continuar. Não o compartilhe com ninguém.',
ttl: (min) => `Este código expira em ${min} minuto${min === 1 ? '' : 's'}.`,
ignore: 'Se você não solicitou este código, ignore este e-mail.',
},
}
/**
* @param {VerifyCodeData} data
* @returns {{ subject: string, html: string, text: string }}
*/
export function render(data) {
if (!data || typeof data !== 'object') {
throw new EmailError({ provider: 'template', reason: 'badTemplate', retryable: false, message: 'verify-code: data required' })
}
const { code, ttlMin } = data
if (typeof code !== 'string' || code.trim() === '') {
throw new EmailError({ provider: 'template', reason: 'badTemplate', retryable: false, message: 'verify-code: missing field "code"' })
}
if (typeof ttlMin !== 'number' || !Number.isFinite(ttlMin) || ttlMin <= 0) {
throw new EmailError({ provider: 'template', reason: 'badTemplate', retryable: false, message: 'verify-code: missing or invalid field "ttlMin"' })
}
const c = COPY[data.locale]
if (!c) {
throw new EmailError({ provider: 'template', reason: 'badTemplate', retryable: false, message: `verify-code: unsupported locale: ${String(data.locale)}` })
}
const subject = c.subject(code)
const ttlLine = c.ttl(ttlMin)
const text = [c.heading, '', c.intro, '', code, '', ttlLine, '', c.ignore].join('\n')
const html = [
'<!doctype html><html><body style="font-family:Arial,Helvetica,sans-serif;color:#1a1a1a;">',
`<h2 style="margin:0 0 16px;">${esc(c.heading)}</h2>`,
`<p style="margin:0 0 16px;">${esc(c.intro)}</p>`,
`<p style="font-size:32px;font-weight:700;letter-spacing:6px;margin:0 0 16px;">${esc(code)}</p>`,
`<p style="margin:0 0 16px;color:#555;">${esc(ttlLine)}</p>`,
`<p style="margin:0;color:#999;font-size:12px;">${esc(c.ignore)}</p>`,
'</body></html>',
].join('')
return { subject, html, text }
}
function esc(s) {
return String(s)
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"')
}