@things-factory/auth-base 10.1.247 → 10.1.254

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist-server/controllers/account-mail.d.ts +29 -0
  2. package/dist-server/controllers/account-mail.js +28 -0
  3. package/dist-server/controllers/account-mail.js.map +1 -0
  4. package/dist-server/controllers/password-setup.js +2 -9
  5. package/dist-server/controllers/password-setup.js.map +1 -1
  6. package/dist-server/controllers/reset-password.js +2 -6
  7. package/dist-server/controllers/reset-password.js.map +1 -1
  8. package/dist-server/controllers/unlock-user.js +2 -7
  9. package/dist-server/controllers/unlock-user.js.map +1 -1
  10. package/dist-server/controllers/verification.js +2 -7
  11. package/dist-server/controllers/verification.js.map +1 -1
  12. package/dist-server/router/webauthn-router.js +18 -5
  13. package/dist-server/router/webauthn-router.js.map +1 -1
  14. package/dist-server/tsconfig.tsbuildinfo +1 -1
  15. package/package.json +5 -5
  16. package/tests/account-mail.test.ts +69 -0
  17. package/tests/mail-request-answers-db.test.ts +3 -0
  18. package/tests/passkey-signin-route-db.test.ts +139 -0
  19. package/tests/password-setup-db.test.ts +2 -0
  20. package/tests/verification-token-db.test.ts +3 -0
  21. package/translations/en.json +13 -1
  22. package/translations/es.json +13 -1
  23. package/translations/ja.json +13 -1
  24. package/translations/ko.json +13 -1
  25. package/translations/ms.json +13 -1
  26. package/translations/zh.json +13 -1
  27. package/dist-server/templates/account-unlock-email.d.ts +0 -5
  28. package/dist-server/templates/account-unlock-email.js +0 -69
  29. package/dist-server/templates/account-unlock-email.js.map +0 -1
  30. package/dist-server/templates/reset-password-email.d.ts +0 -11
  31. package/dist-server/templates/reset-password-email.js +0 -73
  32. package/dist-server/templates/reset-password-email.js.map +0 -1
  33. package/dist-server/templates/verification-email.d.ts +0 -5
  34. package/dist-server/templates/verification-email.js +0 -70
  35. package/dist-server/templates/verification-email.js.map +0 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/auth-base",
3
- "version": "10.1.247",
3
+ "version": "10.1.254",
4
4
  "main": "dist-server/index.js",
5
5
  "browser": "dist-client/index.js",
6
6
  "things-factory": true,
@@ -35,10 +35,10 @@
35
35
  "@reduxjs/toolkit": "^2.2.5",
36
36
  "@simplewebauthn/browser": "^13.0.0",
37
37
  "@simplewebauthn/server": "^13.0.0",
38
- "@things-factory/email-base": "^10.1.247",
38
+ "@things-factory/email-base": "^10.1.254",
39
39
  "@things-factory/env": "^10.1.149",
40
- "@things-factory/shell": "^10.1.247",
41
- "@things-factory/task-lease": "^10.1.247",
40
+ "@things-factory/shell": "^10.1.254",
41
+ "@things-factory/task-lease": "^10.1.254",
42
42
  "@things-factory/utils": "^10.1.179",
43
43
  "@types/webappsec-credential-management": "^0.6.9",
44
44
  "jsonwebtoken": "^9.0.0",
@@ -50,5 +50,5 @@
50
50
  "passport-jwt": "^4.0.0",
51
51
  "passport-local": "^1.0.0"
52
52
  },
53
- "gitHead": "3c672029b2ef4a09f3370d37e5d225392856a602"
53
+ "gitHead": "793e2fd617db5bb917ec892e64956fb690f7e7b0"
54
54
  }
@@ -0,0 +1,69 @@
1
+ /**
2
+ * The four account mails: each in the person's language, with its own subject, in the one mail every module sends -
3
+ * the application's brand at the head, never the old frame with a logo fetched from hatiolab.com.
4
+ */
5
+ import fs from 'fs'
6
+ import path from 'path'
7
+
8
+ import { DEFAULT_BRAND } from '@things-factory/email-base'
9
+ import { i18next, NAMESPACE, translatorFor } from '@things-factory/i18n-base/translator'
10
+
11
+ import { ACCOUNT_MAIL_KINDS, accountMail, accountMailWords } from '../server/controllers/account-mail'
12
+
13
+ const LANGUAGES = ['ko', 'en', 'ja', 'zh', 'ms', 'es']
14
+ const LINK = 'https://office.example.com/auth/reset-password#token'
15
+
16
+ /* A test runs outside an application, so the translator has not gathered this module's words; they are added here. */
17
+ beforeAll(() => {
18
+ for (const lng of LANGUAGES) {
19
+ const words = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../translations', `${lng}.json`), 'utf8'))
20
+ i18next.addResourceBundle(lng, NAMESPACE, words, true, true)
21
+ }
22
+ })
23
+
24
+ describe('the words of an account mail', () => {
25
+ test('every mail has its subject, body and button in every language - never a key', () => {
26
+ for (const lng of LANGUAGES) {
27
+ for (const kind of ACCOUNT_MAIL_KINDS) {
28
+ const words = accountMailWords(translatorFor(lng), kind, 'Brand X')
29
+
30
+ for (const [part, text] of Object.entries(words)) {
31
+ expect([lng, kind, part, text]).not.toEqual([lng, kind, part, expect.stringMatching(/^(title|text|button)\.account-mail-/)])
32
+ expect([lng, kind, part, text.trim()]).not.toEqual([lng, kind, part, ''])
33
+ }
34
+ expect(words.body).toContain('Brand X')
35
+ }
36
+ }
37
+ })
38
+
39
+ test('the four subjects differ in every language - a reset is not mistaken for an unlock', () => {
40
+ for (const lng of LANGUAGES) {
41
+ const subjects = ACCOUNT_MAIL_KINDS.map(kind => accountMailWords(translatorFor(lng), kind, 'Brand X').subject)
42
+ expect(new Set(subjects).size).toBe(ACCOUNT_MAIL_KINDS.length)
43
+ }
44
+ })
45
+ })
46
+
47
+ describe('an account mail', () => {
48
+ const context = (lng?: string) => ({ lng, i18next })
49
+
50
+ test('is written in the person’s language, else the language of the request', () => {
51
+ expect(accountMail({ kind: 'unlock-account', user: { locale: 'ko-KR' }, link: LINK, context: context('en-US') }).subject).toBe('계정 잠김')
52
+ expect(accountMail({ kind: 'unlock-account', user: { locale: null }, link: LINK, context: context('ja') }).subject).toBe(
53
+ translatorFor('ja')('title.account-mail-unlock-account')
54
+ )
55
+ })
56
+
57
+ test('is the one mail: the brand at the head, the subject in the band, the link as the button, no fetched image', () => {
58
+ const { subject, content } = accountMail({ kind: 'reset-password', user: { locale: 'ko' }, link: LINK, context: context() })
59
+
60
+ expect(subject).toBe('비밀번호 재설정')
61
+ expect(content).toContain('<html lang="ko">')
62
+ expect(content).toContain(`>${DEFAULT_BRAND.name}<`)
63
+ expect(content).toContain(`href="${LINK}"`)
64
+ expect(content).toContain('>비밀번호 재설정</font></a')
65
+ expect(content).toContain('Copyright © HatioLab Inc. All rights reserved.')
66
+ expect(content).not.toContain('<img')
67
+ expect(content).not.toContain('Hatio, Lab.')
68
+ })
69
+ })
@@ -2,6 +2,7 @@ const mockSentMail: any[] = []
2
2
  let mockMailFails = false
3
3
 
4
4
  jest.mock('@things-factory/email-base', () => ({
5
+ ...jest.requireActual('@things-factory/email-base'),
5
6
  sendEmail: async (mail: any) => {
6
7
  if (mockMailFails) throw new Error('smtp down')
7
8
  mockSentMail.push(mail)
@@ -10,6 +11,7 @@ jest.mock('@things-factory/email-base', () => ({
10
11
 
11
12
  import { closeAuthDatabase, Domain, getRepository, openAuthDatabase, resetAuthDatabase } from './db'
12
13
  import { loadCompiled } from './compiled'
14
+ import { i18next } from '@things-factory/i18n-base/translator'
13
15
 
14
16
  const { User } = loadCompiled('service/user/user')
15
17
  const { authPublicProcessRouter } = loadCompiled('router/auth-public-process-router')
@@ -35,6 +37,7 @@ function handler(path: string) {
35
37
  async function post(path: string, body: any) {
36
38
  const context: any = {
37
39
  t: (key: string) => key,
40
+ i18next,
38
41
  lng: 'en-US',
39
42
  headers: {},
40
43
  header: {},
@@ -0,0 +1,139 @@
1
+ /**
2
+ * The passkey sign-in route, through `authenticate401Middleware`, the way the sign-in screen reaches it.
3
+ *
4
+ * The screen used to fetch it. A fetch says `Accept: *\/*`, the middleware takes that for a page request,
5
+ * so a locked account got the whole `auth-activate` page back as the body - and the screen put that body
6
+ * in a toast. Now the screen posts a form, as the password does, and every refusal is answered with the
7
+ * page its code leads to: the activate page for a locked or unconfirmed account, the sign-in page with
8
+ * its one line for the rest.
9
+ */
10
+
11
+ import { closeAuthDatabase, Domain, getRepository, openAuthDatabase, resetAuthDatabase } from './db'
12
+ import { loadCompiled } from './compiled'
13
+ import { SoftwareAuthenticator } from './passkey-authenticator'
14
+
15
+ const { User } = loadCompiled('service/user/user')
16
+ const { authenticate401Middleware } = loadCompiled('middlewares/authenticate-401-middleware')
17
+ const { webAuthnGlobalPublicRouter } = loadCompiled('router/webauthn-router')
18
+ const CODES = loadCompiled('constants/error-code')
19
+ const passkey = loadCompiled('controllers/passkey')
20
+
21
+ const SITE = { rpID: 'localhost', origin: 'http://localhost:3000' }
22
+ const NAVIGATION = 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8'
23
+
24
+ let domain: any
25
+ let kim: any
26
+
27
+ const signinRoute = webAuthnGlobalPublicRouter.stack.find(
28
+ (layer: any) => layer.path === '/auth/signin-webauthn' && layer.methods.includes('POST')
29
+ )
30
+
31
+ /** What the browser gets back for the form the screen posts. */
32
+ async function post(body: (challenge: string) => any) {
33
+ const { challenge } = await passkey.signinOptions(SITE)
34
+ const answer: any = { status: 200, cookies: {} }
35
+
36
+ const context: any = {
37
+ t: (key: string) => key,
38
+ method: 'POST',
39
+ path: '/auth/signin-webauthn',
40
+ originalUrl: '/auth/signin-webauthn',
41
+ header: { accept: NAVIGATION },
42
+ hostname: SITE.rpID,
43
+ origin: SITE.origin,
44
+ secure: false,
45
+ query: {},
46
+ session: { challenge },
47
+ state: { domain },
48
+ request: { body: body(challenge) },
49
+ cookies: { set: (key: string, value: string) => (answer.cookies[key] = value) },
50
+ render: async (view: string, options: any) => (answer.page = { view, ...options }),
51
+ redirect: (url: string) => (answer.redirect = url)
52
+ }
53
+
54
+ await authenticate401Middleware(context, () => signinRoute.stack[0](context, async () => {}))
55
+
56
+ return { ...answer, status: context.status, body: context.body }
57
+ }
58
+
59
+ async function registered(user: any) {
60
+ const authenticator = new SoftwareAuthenticator()
61
+ const { challenge } = await passkey.registrationOptions(user, 'Operato', SITE)
62
+ await passkey.registerPasskey(user, authenticator.register(challenge, SITE), 'macOS · Chrome', challenge, SITE)
63
+ return authenticator
64
+ }
65
+
66
+ const formWith = (authenticator: SoftwareAuthenticator) => (challenge: string) => ({
67
+ response: JSON.stringify(authenticator.assert(challenge, SITE)),
68
+ redirectTo: '/office'
69
+ })
70
+
71
+ beforeAll(async () => {
72
+ await openAuthDatabase()
73
+ })
74
+
75
+ afterAll(async () => {
76
+ await closeAuthDatabase()
77
+ })
78
+
79
+ beforeEach(async () => {
80
+ await resetAuthDatabase()
81
+ domain = await getRepository(Domain).save({ name: 'factory', subdomain: 'factory' })
82
+
83
+ const salt = User.generateSalt()
84
+ kim = await getRepository(User).save({
85
+ username: 'kim',
86
+ email: 'kim@example.com',
87
+ name: 'kim',
88
+ salt,
89
+ password: User.encode('Passw0rd!', salt),
90
+ status: 'activated',
91
+ domains: [domain]
92
+ })
93
+ })
94
+
95
+ it('signs in and goes on to check-in, with the token in the cookie', async () => {
96
+ const answer = await post(formWith(await registered(kim)))
97
+
98
+ expect(answer.redirect).toBe('/auth/checkin/factory?redirect_to=%2Foffice')
99
+ expect(Object.values(answer.cookies)).toHaveLength(1)
100
+ })
101
+
102
+ describe.each([
103
+ ['locked', CODES.USER_LOCKED],
104
+ ['inactive', CODES.USER_NOT_ACTIVATED]
105
+ ])('a %s account', (status, errorCode) => {
106
+ it('lands on the activate page with its code and its line, the way a password does', async () => {
107
+ const authenticator = await registered(kim)
108
+ await getRepository(User).update({ id: kim.id }, { status, lockedAt: new Date() })
109
+
110
+ const answer = await post(formWith(authenticator))
111
+
112
+ expect(answer.status).toBe(401)
113
+ expect(answer.page.pageElement).toBe('auth-activate')
114
+ expect(answer.page.data).toMatchObject({ errorCode, message: `error.${errorCode}`, username: 'kim', redirectTo: '/office' })
115
+ expect(answer.cookies).toEqual({})
116
+ })
117
+ })
118
+
119
+ describe.each([
120
+ ['deleted', CODES.USER_DELETED],
121
+ ['password_reset_required', CODES.PASSWORD_SETUP_PENDING]
122
+ ])('a %s account', (status, errorCode) => {
123
+ it('stays on the sign-in page with its one line', async () => {
124
+ const authenticator = await registered(kim)
125
+ await getRepository(User).update({ id: kim.id }, { status })
126
+
127
+ const answer = await post(formWith(authenticator))
128
+
129
+ expect(answer.page.pageElement).toBe('auth-signin')
130
+ expect(answer.page.data.message).toBe(`error.${errorCode}`)
131
+ })
132
+ })
133
+
134
+ it('an answer that is not JSON is a failed verification, said in one line on the sign-in page', async () => {
135
+ const answer = await post(() => ({ response: '<not json>', redirectTo: '/office' }))
136
+
137
+ expect(answer.page.pageElement).toBe('auth-signin')
138
+ expect(answer.page.data.message).toBe(`error.${CODES.AUTHN_VERIFICATION_FAILED}`)
139
+ })
@@ -9,6 +9,7 @@
9
9
 
10
10
  import { authDataSource, closeAuthDatabase, Domain, getRepository, openAuthDatabase, resetAuthDatabase } from './db'
11
11
  import { loadCompiled } from './compiled'
12
+ import { i18next } from '@things-factory/i18n-base/translator'
12
13
 
13
14
  const jwt = require('jsonwebtoken')
14
15
 
@@ -29,6 +30,7 @@ function context(tx?: any) {
29
30
  return {
30
31
  state: { domain: here, tx },
31
32
  t: (key: string) => key,
33
+ i18next,
32
34
  headers: {},
33
35
  request: { protocol: 'https', host: 'acme.example.com' }
34
36
  } as any
@@ -1,6 +1,7 @@
1
1
  const sentMail: any[] = []
2
2
 
3
3
  jest.mock('@things-factory/email-base', () => ({
4
+ ...jest.requireActual('@things-factory/email-base'),
4
5
  sendEmail: async (mail: any) => {
5
6
  sentMail.push(mail)
6
7
  }
@@ -8,6 +9,7 @@ jest.mock('@things-factory/email-base', () => ({
8
9
 
9
10
  import { closeAuthDatabase, Domain, getRepository, openAuthDatabase, resetAuthDatabase } from './db'
10
11
  import { loadCompiled } from './compiled'
12
+ import { i18next } from '@things-factory/i18n-base/translator'
11
13
  import {
12
14
  hashVerificationToken,
13
15
  isVerificationTokenLive,
@@ -45,6 +47,7 @@ const HOUR = 60 * 60 * 1000
45
47
 
46
48
  const context: any = {
47
49
  t: (key: string) => key,
50
+ i18next,
48
51
  lng: 'en-US',
49
52
  headers: {},
50
53
  request: { protocol: 'https', host: 'acme.example.com' }
@@ -271,5 +271,17 @@
271
271
  "error.identity confirmation required": "confirm it is you to see this",
272
272
  "error.passkey-not-found": "There is no passkey of yours with this id. Reload the list.",
273
273
  "error.passkey-name-too-long": "A passkey name takes up to {max} characters; this one has {length}.",
274
- "error.domain not specified": "This account cannot enter the $t(term.domain) {subdomain}."
274
+ "error.domain not specified": "This account cannot enter the $t(term.domain) {subdomain}.",
275
+ "title.account-mail-verify-email": "Confirm your email address",
276
+ "text.account-mail-verify-email": "To finish signing up for {brand}, confirm that this email address is yours. Press the button below and you are done.\nThe link works for 48 hours.\nIf you did not sign up, you can ignore this mail.",
277
+ "button.account-mail-verify-email": "Confirm email",
278
+ "title.account-mail-reset-password": "Reset your password",
279
+ "text.account-mail-reset-password": "We received a request to reset the password of your {brand} account. Press the button below to choose a new one.\nThe link works once, for 1 hour.\nIf you did not ask for this, you can ignore this mail - your password stays as it is.",
280
+ "button.account-mail-reset-password": "Reset password",
281
+ "title.account-mail-set-password": "Set your password",
282
+ "text.account-mail-set-password": "An administrator sent you a link to set the password of your {brand} account. Press the button below to choose one.\nThe link works once, for 48 hours, and the page asks for this email address.",
283
+ "button.account-mail-set-password": "Set password",
284
+ "title.account-mail-unlock-account": "Your account is locked",
285
+ "text.account-mail-unlock-account": "Your {brand} account was locked after too many wrong passwords. Press the button below and choose a new password to unlock it.\nThe link works once, for 1 hour.\nIf it was not you signing in, someone else may have tried your password - choose a new one with this link.",
286
+ "button.account-mail-unlock-account": "Unlock account"
275
287
  }
@@ -271,5 +271,17 @@
271
271
  "error.identity confirmation required": "confirme que es usted para ver esto",
272
272
  "error.passkey-not-found": "No tiene ninguna llave de acceso con este id. Vuelva a cargar la lista.",
273
273
  "error.passkey-name-too-long": "El nombre de una llave de acceso admite hasta {max} caracteres; este tiene {length}.",
274
- "error.domain not specified": "Esta cuenta no puede entrar en el $t(term.domain) {subdomain}."
274
+ "error.domain not specified": "Esta cuenta no puede entrar en el $t(term.domain) {subdomain}.",
275
+ "title.account-mail-verify-email": "Confirma tu correo electrónico",
276
+ "text.account-mail-verify-email": "Para terminar el registro en {brand}, confirma que esta dirección de correo es tuya. Pulsa el botón de abajo y listo.\nEl enlace funciona durante 48 horas.\nSi no te registraste, puedes ignorar este correo.",
277
+ "button.account-mail-verify-email": "Confirmar correo",
278
+ "title.account-mail-reset-password": "Restablecer contraseña",
279
+ "text.account-mail-reset-password": "Recibimos una solicitud para restablecer la contraseña de tu cuenta de {brand}. Pulsa el botón de abajo para elegir una nueva.\nEl enlace funciona una vez, durante 1 hora.\nSi no lo pediste, puedes ignorar este correo: tu contraseña no cambia.",
280
+ "button.account-mail-reset-password": "Restablecer contraseña",
281
+ "title.account-mail-set-password": "Establece tu contraseña",
282
+ "text.account-mail-set-password": "Un administrador te envió un enlace para establecer la contraseña de tu cuenta de {brand}. Pulsa el botón de abajo para elegirla.\nEl enlace funciona una vez, durante 48 horas, y la página pide esta dirección de correo.",
283
+ "button.account-mail-set-password": "Establecer contraseña",
284
+ "title.account-mail-unlock-account": "Tu cuenta está bloqueada",
285
+ "text.account-mail-unlock-account": "Tu cuenta de {brand} se bloqueó tras demasiadas contraseñas incorrectas. Pulsa el botón de abajo y elige una nueva contraseña para desbloquearla.\nEl enlace funciona una vez, durante 1 hora.\nSi no eras tú quien iniciaba sesión, alguien pudo haber probado tu contraseña: elige una nueva con este enlace.",
286
+ "button.account-mail-unlock-account": "Desbloquear cuenta"
275
287
  }
@@ -271,5 +271,17 @@
271
271
  "error.identity confirmation required": "本人確認が必要です。",
272
272
  "error.passkey-not-found": "あなたのパスキーにこのパスキーはありません。一覧を再読み込みしてください。",
273
273
  "error.passkey-name-too-long": "パスキー名は{max}文字までです。今は{length}文字です。",
274
- "error.domain not specified": "このアカウントで入れる$t(term.domain)ではありません: {subdomain}"
274
+ "error.domain not specified": "このアカウントで入れる$t(term.domain)ではありません: {subdomain}",
275
+ "title.account-mail-verify-email": "メールアドレスの確認",
276
+ "text.account-mail-verify-email": "{brand} の登録を完了するには、このメールアドレスがご本人のものか確認してください。下のボタンを押すと確認が完了します。\nリンクは48時間有効です。\n登録していない場合は、このメールを無視してください。",
277
+ "button.account-mail-verify-email": "メールを確認",
278
+ "title.account-mail-reset-password": "パスワードの再設定",
279
+ "text.account-mail-reset-password": "{brand} アカウントのパスワード再設定の依頼を受け付けました。下のボタンから新しいパスワードを設定してください。\nリンクは1時間、1回だけ使えます。\n依頼していない場合は、このメールを無視してください。パスワードは変わりません。",
280
+ "button.account-mail-reset-password": "パスワードを再設定",
281
+ "title.account-mail-set-password": "パスワードの設定",
282
+ "text.account-mail-set-password": "管理者が {brand} アカウントのパスワード設定リンクを送りました。下のボタンからパスワードを設定してください。\nリンクは48時間、1回だけ使えます。開いたページでこのメールアドレスを入力します。",
283
+ "button.account-mail-set-password": "パスワードを設定",
284
+ "title.account-mail-unlock-account": "アカウントがロックされました",
285
+ "text.account-mail-unlock-account": "パスワードを何度も間違えたため、{brand} アカウントがロックされました。下のボタンから新しいパスワードを設定するとロックが解除されます。\nリンクは1時間、1回だけ使えます。\nご自身でサインインしていない場合、他の人がパスワードを試した可能性があります。このリンクで新しいパスワードを設定してください。",
286
+ "button.account-mail-unlock-account": "ロックを解除"
275
287
  }
@@ -271,5 +271,17 @@
271
271
  "error.identity confirmation required": "본인 확인이 필요합니다.",
272
272
  "error.passkey-not-found": "내 패스키 가운데 이 패스키가 없습니다. 목록을 새로 불러오세요.",
273
273
  "error.passkey-name-too-long": "패스키 이름은 {max}자까지입니다. 지금 {length}자입니다.",
274
- "error.domain not specified": "이 계정으로 들어갈 수 있는 $t(term.domain)이 아닙니다: {subdomain}"
274
+ "error.domain not specified": "이 계정으로 들어갈 수 있는 $t(term.domain)이 아닙니다: {subdomain}",
275
+ "title.account-mail-verify-email": "이메일 주소 확인",
276
+ "text.account-mail-verify-email": "{brand} 가입을 마치려면 이 이메일 주소가 맞는지 확인해 주세요. 아래 단추를 누르면 확인이 끝납니다.\n링크는 48시간 동안 쓸 수 있습니다.\n가입한 적이 없다면 이 메일은 무시해도 됩니다.",
277
+ "button.account-mail-verify-email": "이메일 확인",
278
+ "title.account-mail-reset-password": "비밀번호 재설정",
279
+ "text.account-mail-reset-password": "{brand} 계정의 비밀번호 재설정 요청을 받았습니다. 아래 단추를 눌러 새 비밀번호를 정하세요.\n링크는 1시간 동안 한 번 쓸 수 있습니다.\n요청한 적이 없다면 이 메일은 무시해도 됩니다. 비밀번호는 바뀌지 않습니다.",
280
+ "button.account-mail-reset-password": "비밀번호 재설정",
281
+ "title.account-mail-set-password": "비밀번호 설정",
282
+ "text.account-mail-set-password": "관리자가 {brand} 계정의 비밀번호 설정 링크를 보냈습니다. 아래 단추를 눌러 비밀번호를 정하세요.\n링크는 48시간 동안 한 번 쓸 수 있고, 열리는 페이지에서 이 이메일 주소를 묻습니다.",
283
+ "button.account-mail-set-password": "비밀번호 설정",
284
+ "title.account-mail-unlock-account": "계정 잠김",
285
+ "text.account-mail-unlock-account": "비밀번호를 여러 번 잘못 입력해 {brand} 계정이 잠겼습니다. 아래 단추를 눌러 새 비밀번호를 정하면 잠금이 풀립니다.\n링크는 1시간 동안 한 번 쓸 수 있습니다.\n직접 로그인하지 않았다면 다른 사람이 비밀번호를 시도한 것일 수 있습니다. 이 링크로 새 비밀번호를 정하세요.",
286
+ "button.account-mail-unlock-account": "잠금 풀기"
275
287
  }
@@ -271,5 +271,17 @@
271
271
  "error.identity confirmation required": "sahkan ini anda untuk melihat ini",
272
272
  "error.passkey-not-found": "Tiada kunci laluan anda dengan id ini. Muat semula senarai.",
273
273
  "error.passkey-name-too-long": "Nama kunci laluan boleh sehingga {max} aksara; nama ini ada {length}.",
274
- "error.domain not specified": "Akaun ini tidak boleh memasuki $t(term.domain) {subdomain}."
274
+ "error.domain not specified": "Akaun ini tidak boleh memasuki $t(term.domain) {subdomain}.",
275
+ "title.account-mail-verify-email": "Sahkan alamat e-mel anda",
276
+ "text.account-mail-verify-email": "Untuk melengkapkan pendaftaran {brand}, sahkan bahawa alamat e-mel ini milik anda. Tekan butang di bawah dan selesai.\nPautan sah selama 48 jam.\nJika anda tidak mendaftar, abaikan e-mel ini.",
277
+ "button.account-mail-verify-email": "Sahkan e-mel",
278
+ "title.account-mail-reset-password": "Tetapkan semula kata laluan",
279
+ "text.account-mail-reset-password": "Kami menerima permintaan untuk menetapkan semula kata laluan akaun {brand} anda. Tekan butang di bawah untuk memilih kata laluan baharu.\nPautan boleh digunakan sekali, selama 1 jam.\nJika anda tidak memintanya, abaikan e-mel ini - kata laluan anda tidak berubah.",
280
+ "button.account-mail-reset-password": "Tetapkan semula kata laluan",
281
+ "title.account-mail-set-password": "Tetapkan kata laluan anda",
282
+ "text.account-mail-set-password": "Pentadbir menghantar pautan untuk menetapkan kata laluan akaun {brand} anda. Tekan butang di bawah untuk memilihnya.\nPautan boleh digunakan sekali, selama 48 jam, dan halaman itu akan meminta alamat e-mel ini.",
283
+ "button.account-mail-set-password": "Tetapkan kata laluan",
284
+ "title.account-mail-unlock-account": "Akaun anda dikunci",
285
+ "text.account-mail-unlock-account": "Akaun {brand} anda dikunci selepas terlalu banyak kata laluan salah. Tekan butang di bawah dan pilih kata laluan baharu untuk membukanya.\nPautan boleh digunakan sekali, selama 1 jam.\nJika bukan anda yang log masuk, orang lain mungkin telah mencuba kata laluan anda - pilih kata laluan baharu dengan pautan ini.",
286
+ "button.account-mail-unlock-account": "Buka kunci akaun"
275
287
  }
@@ -271,5 +271,17 @@
271
271
  "error.identity confirmation required": "需要确认身份。",
272
272
  "error.passkey-not-found": "您的通行密钥中没有这一个。请重新加载列表。",
273
273
  "error.passkey-name-too-long": "通行密钥名称最多 {max} 个字符,现在是 {length} 个。",
274
- "error.domain not specified": "此账户无法进入该$t(term.domain): {subdomain}"
274
+ "error.domain not specified": "此账户无法进入该$t(term.domain): {subdomain}",
275
+ "title.account-mail-verify-email": "确认邮箱地址",
276
+ "text.account-mail-verify-email": "要完成 {brand} 的注册,请确认此邮箱地址属于您。点击下方按钮即可完成确认。\n链接 48 小时内有效。\n如果您没有注册,请忽略此邮件。",
277
+ "button.account-mail-verify-email": "确认邮箱",
278
+ "title.account-mail-reset-password": "重置密码",
279
+ "text.account-mail-reset-password": "我们收到了重置您 {brand} 账户密码的请求。请点击下方按钮设置新密码。\n链接 1 小时内有效,只能使用一次。\n如果不是您本人的请求,请忽略此邮件,密码不会改变。",
280
+ "button.account-mail-reset-password": "重置密码",
281
+ "title.account-mail-set-password": "设置密码",
282
+ "text.account-mail-set-password": "管理员向您发送了 {brand} 账户的密码设置链接。请点击下方按钮设置密码。\n链接 48 小时内有效,只能使用一次,打开的页面会要求输入此邮箱地址。",
283
+ "button.account-mail-set-password": "设置密码",
284
+ "title.account-mail-unlock-account": "账户已锁定",
285
+ "text.account-mail-unlock-account": "由于多次输错密码,您的 {brand} 账户已被锁定。点击下方按钮设置新密码即可解锁。\n链接 1 小时内有效,只能使用一次。\n如果不是您本人登录,可能有他人尝试了您的密码,请用此链接设置新密码。",
286
+ "button.account-mail-unlock-account": "解锁账户"
275
287
  }
@@ -1,5 +0,0 @@
1
- export declare function getUnlockUserEmailForm({ username, name, resetUrl }: {
2
- username: any;
3
- name: any;
4
- resetUrl: any;
5
- }): string;
@@ -1,69 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getUnlockUserEmailForm = getUnlockUserEmailForm;
4
- function getUnlockUserEmailForm({ username, name, resetUrl }) {
5
- return `
6
- <html lang="en">
7
- <head>
8
- <meta charset="utf-8" />
9
-
10
- <title>reset password</title>
11
- <meta name="description" content="Password Reset" />
12
- <meta name="author" content="hatiolab" />
13
- <meta name="google" content="notranslate"/>
14
- </head>
15
-
16
- <body>
17
- <div style="background-color:#f6f6f6">
18
- <!--header begin-->
19
- <div style="background-color:#fff;padding:0 10px;border-top: 2px solid #394e64;">
20
- <a href="#" target="_blank"
21
- ><img
22
- src="http://www.hatiolab.com/assets/img/logo-operato.png"
23
- style="max-height:50px"
24
- /></a>
25
- </div>
26
- <!--header end-->
27
-
28
- <!--title begin-->
29
- <div
30
- style="background-color:#22a6a7;padding:12px 10px 10px 10px;min-height:50px;"
31
- >
32
- <img
33
- src="http://www.hatiolab.com/assets/img/icon-mail.png"
34
- style="float:left;margin:0 10px 0 40px"
35
- />
36
- <span style="display:block;color:#fff;font-size:20px"
37
- >Hi ${name}!</span
38
- >
39
- <span style="display:block;color:#fff;font-size:34px;font-weight:bold"
40
- >Unlock Account</span
41
- >
42
- </div>
43
- <!--title end-->
44
-
45
- <!--body begin-->
46
- <p style="padding:10px 20px;line-height:1.5;font-size:16px">
47
- Click the button below to unlock account and reset password.
48
- <br />
49
- <a
50
- href="${resetUrl}"
51
- style="display:inline-block;margin:10px 5px 5px 0;border-radius:7px;background-color:#22a6a7;padding:7px 15px;color:#fff;font-size:18px;text-decoration:none;text-transform:capitalize;"
52
- >unlock account</a
53
- >
54
- </p>
55
- <!--body end-->
56
-
57
- <!--footer begin-->
58
- <div
59
- style="background-color:#3d5874;padding:7px 20px 5px 20px;font-size:12px;color:#efefef"
60
- >
61
- © Hatio, Lab. Inc. All rights reserved.
62
- </div>
63
- <!--footer end-->
64
- </div>
65
- </body>
66
- </html>
67
- `;
68
- }
69
- //# sourceMappingURL=account-unlock-email.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"account-unlock-email.js","sourceRoot":"","sources":["../../server/templates/account-unlock-email.ts"],"names":[],"mappings":";;AAAA,wDAgEC;AAhED,SAAgB,sBAAsB,CAAC,EAAE,QAAQ,EAAE,IAAI,EAAE,QAAQ,EAAE;IACjE,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAgCS,IAAI;;;;;;;;;;;;;oBAaF,QAAQ;;;;;;;;;;;;;;;;;GAiBzB,CAAA;AACH,CAAC","sourcesContent":["export function getUnlockUserEmailForm({ username, name, resetUrl }) {\n return `\n <html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n \n <title>reset password</title>\n <meta name=\"description\" content=\"Password Reset\" />\n <meta name=\"author\" content=\"hatiolab\" />\n <meta name=\"google\" content=\"notranslate\"/>\n </head>\n\n <body>\n <div style=\"background-color:#f6f6f6\">\n <!--header begin-->\n <div style=\"background-color:#fff;padding:0 10px;border-top: 2px solid #394e64;\">\n <a href=\"#\" target=\"_blank\"\n ><img\n src=\"http://www.hatiolab.com/assets/img/logo-operato.png\"\n style=\"max-height:50px\"\n /></a>\n </div>\n <!--header end-->\n\n <!--title begin-->\n <div\n style=\"background-color:#22a6a7;padding:12px 10px 10px 10px;min-height:50px;\"\n >\n <img\n src=\"http://www.hatiolab.com/assets/img/icon-mail.png\"\n style=\"float:left;margin:0 10px 0 40px\"\n />\n <span style=\"display:block;color:#fff;font-size:20px\"\n >Hi ${name}!</span\n >\n <span style=\"display:block;color:#fff;font-size:34px;font-weight:bold\"\n >Unlock Account</span\n >\n </div>\n <!--title end-->\n\n <!--body begin-->\n <p style=\"padding:10px 20px;line-height:1.5;font-size:16px\">\n Click the button below to unlock account and reset password.\n <br />\n <a\n href=\"${resetUrl}\"\n style=\"display:inline-block;margin:10px 5px 5px 0;border-radius:7px;background-color:#22a6a7;padding:7px 15px;color:#fff;font-size:18px;text-decoration:none;text-transform:capitalize;\"\n >unlock account</a\n >\n </p>\n <!--body end-->\n\n <!--footer begin-->\n <div\n style=\"background-color:#3d5874;padding:7px 20px 5px 20px;font-size:12px;color:#efefef\"\n >\n © Hatio, Lab. Inc. All rights reserved.\n </div>\n <!--footer end-->\n </div>\n </body>\n </html>\n `\n}\n"]}
@@ -1,11 +0,0 @@
1
- /**
2
- * The mail that carries a link to the password page. A person who forgot theirs gets it (`Reset password`), and so
3
- * does a person whose administrator issued a password setup link (ADR-0161) - one page, one mail, two headings.
4
- */
5
- export declare function getResetPasswordEmailForm({ name, resetUrl, heading, action, note }: {
6
- name: string;
7
- resetUrl: string;
8
- heading?: string;
9
- action?: string;
10
- note?: string;
11
- }): string;
@@ -1,73 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.getResetPasswordEmailForm = getResetPasswordEmailForm;
4
- /**
5
- * The mail that carries a link to the password page. A person who forgot theirs gets it (`Reset password`), and so
6
- * does a person whose administrator issued a password setup link (ADR-0161) - one page, one mail, two headings.
7
- */
8
- function getResetPasswordEmailForm({ name, resetUrl, heading = 'Reset password', action = 'reset password', note = '' }) {
9
- return `
10
- <html lang="en">
11
- <head>
12
- <meta charset="utf-8" />
13
-
14
- <title>reset password</title>
15
- <meta name="description" content="Password Reset" />
16
- <meta name="author" content="hatiolab" />
17
- <meta name="google" content="notranslate"/>
18
- </head>
19
-
20
- <body>
21
- <div style="background-color:#f6f6f6">
22
- <!--header begin-->
23
- <div style="background-color:#fff;padding:0 10px;border-top: 2px solid #394e64;">
24
- <a href="#" target="_blank"
25
- ><img
26
- src="http://www.hatiolab.com/assets/img/logo-operato.png"
27
- style="max-height:50px"
28
- /></a>
29
- </div>
30
- <!--header end-->
31
-
32
- <!--title begin-->
33
- <div
34
- style="background-color:#22a6a7;padding:12px 10px 10px 10px;min-height:50px;"
35
- >
36
- <img
37
- src="http://www.hatiolab.com/assets/img/icon-mail.png"
38
- style="float:left;margin:0 10px 0 40px"
39
- />
40
- <span style="display:block;color:#fff;font-size:20px"
41
- >Hi ${name}!</span
42
- >
43
- <span style="display:block;color:#fff;font-size:34px;font-weight:bold"
44
- >${heading}</span
45
- >
46
- </div>
47
- <!--title end-->
48
-
49
- <!--body begin-->
50
- <p style="padding:10px 20px;line-height:1.5;font-size:16px">
51
- Click the button below to ${action}.${note ? ` ${note}` : ''}
52
- <br />
53
- <a
54
- href="${resetUrl}"
55
- style="display:inline-block;margin:10px 5px 5px 0;border-radius:7px;background-color:#22a6a7;padding:7px 15px;color:#fff;font-size:18px;text-decoration:none;text-transform:capitalize;"
56
- >${action}</a
57
- >
58
- </p>
59
- <!--body end-->
60
-
61
- <!--footer begin-->
62
- <div
63
- style="background-color:#3d5874;padding:7px 20px 5px 20px;font-size:12px;color:#efefef"
64
- >
65
- © Hatio, Lab. Inc. All rights reserved.
66
- </div>
67
- <!--footer end-->
68
- </div>
69
- </body>
70
- </html>
71
- `;
72
- }
73
- //# sourceMappingURL=reset-password-email.js.map
@@ -1 +0,0 @@
1
- {"version":3,"file":"reset-password-email.js","sourceRoot":"","sources":["../../server/templates/reset-password-email.ts"],"names":[],"mappings":";;AAIA,8DA4EC;AAhFD;;;GAGG;AACH,SAAgB,yBAAyB,CAAC,EACxC,IAAI,EACJ,QAAQ,EACR,OAAO,GAAG,gBAAgB,EAC1B,MAAM,GAAG,gBAAgB,EACzB,IAAI,GAAG,EAAE,EAOV;IACC,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;kBAgCS,IAAI;;;eAGP,OAAO;;;;;;;sCAOgB,MAAM,IAAI,IAAI,CAAC,CAAC,CAAC,IAAI,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE;;;oBAGlD,QAAQ;;eAEb,MAAM;;;;;;;;;;;;;;;GAelB,CAAA;AACH,CAAC","sourcesContent":["/**\n * The mail that carries a link to the password page. A person who forgot theirs gets it (`Reset password`), and so\n * does a person whose administrator issued a password setup link (ADR-0161) - one page, one mail, two headings.\n */\nexport function getResetPasswordEmailForm({\n name,\n resetUrl,\n heading = 'Reset password',\n action = 'reset password',\n note = ''\n}: {\n name: string\n resetUrl: string\n heading?: string\n action?: string\n note?: string\n}) {\n return `\n <html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n \n <title>reset password</title>\n <meta name=\"description\" content=\"Password Reset\" />\n <meta name=\"author\" content=\"hatiolab\" />\n <meta name=\"google\" content=\"notranslate\"/>\n </head>\n\n <body>\n <div style=\"background-color:#f6f6f6\">\n <!--header begin-->\n <div style=\"background-color:#fff;padding:0 10px;border-top: 2px solid #394e64;\">\n <a href=\"#\" target=\"_blank\"\n ><img\n src=\"http://www.hatiolab.com/assets/img/logo-operato.png\"\n style=\"max-height:50px\"\n /></a>\n </div>\n <!--header end-->\n\n <!--title begin-->\n <div\n style=\"background-color:#22a6a7;padding:12px 10px 10px 10px;min-height:50px;\"\n >\n <img\n src=\"http://www.hatiolab.com/assets/img/icon-mail.png\"\n style=\"float:left;margin:0 10px 0 40px\"\n />\n <span style=\"display:block;color:#fff;font-size:20px\"\n >Hi ${name}!</span\n >\n <span style=\"display:block;color:#fff;font-size:34px;font-weight:bold\"\n >${heading}</span\n >\n </div>\n <!--title end-->\n\n <!--body begin-->\n <p style=\"padding:10px 20px;line-height:1.5;font-size:16px\">\n Click the button below to ${action}.${note ? ` ${note}` : ''}\n <br />\n <a\n href=\"${resetUrl}\"\n style=\"display:inline-block;margin:10px 5px 5px 0;border-radius:7px;background-color:#22a6a7;padding:7px 15px;color:#fff;font-size:18px;text-decoration:none;text-transform:capitalize;\"\n >${action}</a\n >\n </p>\n <!--body end-->\n\n <!--footer begin-->\n <div\n style=\"background-color:#3d5874;padding:7px 20px 5px 20px;font-size:12px;color:#efefef\"\n >\n © Hatio, Lab. Inc. All rights reserved.\n </div>\n <!--footer end-->\n </div>\n </body>\n </html>\n `\n}\n"]}
@@ -1,5 +0,0 @@
1
- export declare function getVerificationEmailForm({ username, name, verifyUrl }: {
2
- username: any;
3
- name: any;
4
- verifyUrl: any;
5
- }): string;