@nxgt/janus-mail 0.6.0 → 0.7.1
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.
- package/README.md +55 -18
- package/dist/expiring-mails.d.ts +2 -2
- package/dist/expiring-mails.d.ts.map +1 -1
- package/dist/generated/mail.d.ts +5 -0
- package/dist/generated/mail.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +22 -3
- package/dist/index.js.map +6 -6
- package/dist/links.d.ts.map +1 -1
- package/dist/options.d.ts.map +1 -1
- package/dist/template-types.d.ts +8 -2
- package/dist/template-types.d.ts.map +1 -1
- package/dist/templates.d.ts +1 -1
- package/dist/templates.d.ts.map +1 -1
- package/dist/types.d.ts +18 -1
- package/dist/types.d.ts.map +1 -1
- package/docs/README.md +2 -2
- package/docs/guide/building.md +1 -1
- package/docs/guide/locales.md +7 -6
- package/docs/guide/sending.md +49 -11
- package/docs/guide/templates.md +10 -8
- package/docs/roadmap.md +11 -2
- package/docs/troubleshooting.md +62 -15
- package/mails/en/magic-link.html +51 -0
- package/mails/en/magic-link.txt +21 -0
- package/mails/fr/magic-link.html +51 -0
- package/mails/fr/magic-link.txt +21 -0
- package/mails/mail-manifest.json +24 -0
- package/package.json +4 -4
package/dist/types.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,OAAO,KAAK,EAAE,KAAK,EAAE,UAAU,EAAE,WAAW,EAAE,MAAM,aAAa,CAAC;AAClE,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3E,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC3D,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,kBAAkB,CAAC;AAE3D,YAAY,EAAE,eAAe,EAAE,MAAM,qBAAqB,CAAC;AAC3D,YAAY,EACX,iBAAiB,EACjB,qBAAqB,EACrB,kBAAkB,EAClB,kBAAkB,GAClB,MAAM,kBAAkB,CAAC;AAE1B;;;;;GAKG;AACH,MAAM,WAAW,SAAS;IACzB,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,QAAQ,CAAC,MAAM,CAAC,EAAE,aAAa,CAAC;CAChC;AAED;;;;;;;;;;GAUG;AACH,MAAM,WAAW,cAAc;IAC9B,6HAA6H;IAC7H,QAAQ,CAAC,WAAW,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IAChD,mEAAmE;IACnE,QAAQ,CAAC,aAAa,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,MAAM,CAAC;IAClD;;;;OAIG;IACH,QAAQ,CAAC,aAAa,EAAE,MAAM,MAAM,CAAC;IACrC;;;;OAIG;IACH,QAAQ,CAAC,UAAU,EAAE,MAAM,MAAM,CAAC;IAClC;;;;;OAKG;IACH,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"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"}
|
package/docs/README.md
CHANGED
|
@@ -23,11 +23,11 @@ They mean the same here. These pages add:
|
|
|
23
23
|
|
|
24
24
|
| Word | Means | Not |
|
|
25
25
|
| --- | --- | --- |
|
|
26
|
-
| **e-mail** | One of the
|
|
26
|
+
| **e-mail** | One of the ten this package sends: `verify-email`, `reset-password`, `sign-in-code`, `magic-link`, `password-changed`, `email-changed`, `two-factor-enabled`, `two-factor-disabled`, `recovery-code-used`, `welcome` — by the name of its built file | "mail", "message" — a *message* is what a mailer sends, addressed |
|
|
27
27
|
| **template** | The function that renders one e-mail: `(variables & { locale }) => Rendered`. Named in camelCase, after the method that sends it: `verifyEmail`, `signInCode` | "renderer" — the renderer is `@nxgt/mail`'s, and reads the build |
|
|
28
28
|
| **default template** | A template of `janusTemplates()`: the prebuilt e-mail, filled by the renderer | "preset" — a preset is `@nxgt/mail-presets`' source, which the build compiles |
|
|
29
29
|
| **override** | A template of yours, passed in `templates`, used instead of the default | "custom template" |
|
|
30
|
-
| **issued** | What a flow answered: an `IssuedToken` from `verifyEmail.send` or `
|
|
30
|
+
| **issued** | What a flow answered: an `IssuedToken` from `verifyEmail.send`, `resetPassword.request` or `magicLink.request`, an `IssuedCode` from `signInCode.request` | "result", "payload" |
|
|
31
31
|
| **recipient** | Who an e-mail is for besides the address: a `Recipient`, `{ name, locale? }` | "user" — a user is a record of `@nxgt/janus`; "to" is the address |
|
|
32
32
|
| **notice** | An e-mail that tells, and asks for nothing: `passwordChanged`, `emailChanged`, `twoFactorEnabled`, `twoFactorDisabled`, `recoveryCodeUsed`. It links to `links.secureAccount()` — `recoveryCodeUsed` to `links.recoveryCodes()` when given | "alert", "notification" |
|
|
33
33
|
| **welcome** | The e-mail sent once a user was created, on `@nxgt/janus`'s `user.created`: `welcome`. It links to `links.getStarted()` — not a notice, since it warns of nothing | "onboarding", "greeting" |
|
package/docs/guide/building.md
CHANGED
|
@@ -55,7 +55,7 @@ packages/janus-mail/
|
|
|
55
55
|
`src/generated/mail.ts` (`rendererTypes`), from the build.
|
|
56
56
|
3. `scripts/build-mail.ts` reads the manifest and **fails unless its
|
|
57
57
|
format is one every `@nxgt/mail` the peer admits reads** — see below —
|
|
58
|
-
and **unless exactly the
|
|
58
|
+
and **unless exactly the ten e-mails were built**: an eleventh from a new
|
|
59
59
|
`@nxgt/mail-presets`, or one missing, stops the build.
|
|
60
60
|
4. It writes `src/generated/locales.ts` from the manifest's locales, and
|
|
61
61
|
`src/generated/codes-left.ts` — below — each only when its content
|
package/docs/guide/locales.md
CHANGED
|
@@ -81,6 +81,7 @@ const templates: JanusMailTemplates<Locale> = {
|
|
|
81
81
|
verifyEmail: ({ name, link, expiresIn, locale }) => render('verify-email', locale, { name, link, expiresIn }),
|
|
82
82
|
resetPassword: ({ name, link, expiresIn, locale }) => render('reset-password', locale, { name, link, expiresIn }),
|
|
83
83
|
signInCode: ({ code, expiresIn, locale }) => render('sign-in-code', locale, { code, expiresIn }),
|
|
84
|
+
magicLink: ({ link, expiresIn, locale }) => render('magic-link', locale, { link, expiresIn }),
|
|
84
85
|
passwordChanged: ({ name, link, locale }) => render('password-changed', locale, { name, link }),
|
|
85
86
|
emailChanged: ({ name, link, newEmail, locale }) => render('email-changed', locale, { name, link, newEmail }),
|
|
86
87
|
twoFactorEnabled: ({ name, link, locale }) => render('two-factor-enabled', locale, { name, link }),
|
|
@@ -100,22 +101,22 @@ mail.locales; // readonly ('en' | 'fr' | 'de')[]
|
|
|
100
101
|
plural of the codes left in `en` and `fr` only, so a German recipient's
|
|
101
102
|
`mail.recoveryCodeUsed` takes the sentence as text — "Sie haben noch 3
|
|
102
103
|
Wiederherstellungscodes." — and a count is a `TypeError` there.
|
|
103
|
-
`recoveryCodeUsed` is the ninth template, since 0.5.0
|
|
104
|
-
|
|
105
|
-
`fr
|
|
104
|
+
`recoveryCodeUsed` is the ninth template, since 0.5.0, and `magicLink` the
|
|
105
|
+
tenth, since 0.7.0: a `templates` written for an earlier version no longer
|
|
106
|
+
compiles with a locale beyond `en` and `fr` — add the one it lacks. A
|
|
106
107
|
renderer over a Maizzle build of your own, with
|
|
107
108
|
`@nxgt/mail-presets` and a `locales/de.json`, is the closest to the defaults.
|
|
108
109
|
With only some templates, the call does not compile — the error lands on
|
|
109
110
|
`templates`:
|
|
110
111
|
|
|
111
112
|
```text
|
|
112
|
-
TS2740: Type '{ verifyEmail: () => Rendered; }' is missing the following properties from type 'JanusMailTemplates<"en" | "fr" | "de">': resetPassword, signInCode,
|
|
113
|
+
TS2740: Type '{ verifyEmail: () => Rendered; }' is missing the following properties from type 'JanusMailTemplates<"en" | "fr" | "de">': welcome, resetPassword, signInCode, magicLink, and 5 more.
|
|
113
114
|
```
|
|
114
115
|
|
|
115
116
|
and in JavaScript it throws when `janusMail()` is called:
|
|
116
117
|
|
|
117
118
|
```text
|
|
118
|
-
TypeError: janusMail: the default templates are built in en and fr only — with another locale in locales, pass every template in templates; resetPassword, signInCode, passwordChanged, emailChanged, twoFactorEnabled, twoFactorDisabled, recoveryCodeUsed, welcome missing
|
|
119
|
+
TypeError: janusMail: the default templates are built in en and fr only — with another locale in locales, pass every template in templates; resetPassword, signInCode, magicLink, passwordChanged, emailChanged, twoFactorEnabled, twoFactorDisabled, recoveryCodeUsed, welcome missing
|
|
119
120
|
```
|
|
120
121
|
|
|
121
122
|
A default template is typed for `'en' | 'fr'`, so reusing one in a wider set
|
|
@@ -128,4 +129,4 @@ Built-in locales for more languages are on the [roadmap](../roadmap.md).
|
|
|
128
129
|
|
|
129
130
|
- `@nxgt/mail`'s [locales guide](https://github.com/softistx/nxgt-mail/blob/develop/packages/mail/docs/guide/locales.md)
|
|
130
131
|
— `pickLocale` and `parseAcceptLanguage` in full.
|
|
131
|
-
- [Templates](templates.md) — writing the
|
|
132
|
+
- [Templates](templates.md) — writing the ten for a new locale.
|
package/docs/guide/sending.md
CHANGED
|
@@ -48,6 +48,7 @@ time one is sent, and kept.
|
|
|
48
48
|
| `links.resetPassword` | `(token) => string` | required | The page that sets a new password, given the one-time token |
|
|
49
49
|
| `links.secureAccount` | `() => string` | required | Where a user who made no change secures their account: the notices link to it |
|
|
50
50
|
| `links.getStarted` | `() => string` | required | Where a new user starts — your home page, or your sign-in page for an account someone else created: the welcome's **Get started** button links to it |
|
|
51
|
+
| `links.magicLink` | `(token) => string` | none: `magicLink` is a `TypeError` without it | The page a sign-in link opens, given the one-time token. That page spends nothing: its button posts the token to the route that calls `auth.magicLink.confirm` |
|
|
51
52
|
| `links.recoveryCodes` | `() => string` | `links.secureAccount` | Where a user regenerates their recovery codes: the recovery code notice links to it, from its **Secure my account** button |
|
|
52
53
|
| `locales` | `readonly L[]` | `['en', 'fr']` | The locales sent in — see [Locales](locales.md) |
|
|
53
54
|
| `fallbackLocale` | one of `locales` | `'en'`, else the first of `locales` | The locale when the recipient wants none of `locales` |
|
|
@@ -114,6 +115,36 @@ challenge is the visitor's secret, and neither the default template nor an
|
|
|
114
115
|
override is ever given it. `to` is optional — the e-mail greets nobody by
|
|
115
116
|
name — and only its `locale` is read.
|
|
116
117
|
|
|
118
|
+
### `magicLink(issued, to?, options?)`
|
|
119
|
+
|
|
120
|
+
```ts
|
|
121
|
+
const issued = await auth.magicLink.request(email); // IssuedToken & { user } | null
|
|
122
|
+
if (issued !== null) await mail.magicLink(issued, { locale: issued.user.locale });
|
|
123
|
+
return new Response(null, { status: 202 }); // the same answer either way
|
|
124
|
+
```
|
|
125
|
+
|
|
126
|
+
`request` answers `null` for an address nobody holds, and for an inactive
|
|
127
|
+
user; the compiler refuses `null` here. Sent to `issued.email`, with a
|
|
128
|
+
**Sign in** button to `links.magicLink(issued.token)` and how long it lasts —
|
|
129
|
+
"This link expires in 15 minutes." `to` is optional — the e-mail greets
|
|
130
|
+
nobody by name — and only its `locale` is read.
|
|
131
|
+
|
|
132
|
+
- **`links.magicLink` is required to send it.** It is optional in
|
|
133
|
+
`janusMail()`, for an application that sends no sign-in link, and without
|
|
134
|
+
it `magicLink` throws a `TypeError` —
|
|
135
|
+
`janusMail.magicLink: links.magicLink is missing — …` — before anything is
|
|
136
|
+
rendered. There is no fallback: only your page can carry the token.
|
|
137
|
+
- **Link to a page, never to the route that signs in.** Mail scanners open
|
|
138
|
+
every link in an e-mail; one that signs in on `GET` signs in for the
|
|
139
|
+
scanner, and the user's click answers `TOKEN_SPENT`. The page's button
|
|
140
|
+
posts the token to the route calling `auth.magicLink.confirm` —
|
|
141
|
+
[`@nxgt/janus`'s sign-in link guide](https://github.com/softistx/nxgt-janus/blob/develop/packages/janus/docs/guide/magic-link.md)
|
|
142
|
+
has it, and [`@nxgt/janus-hono`'s routes](https://github.com/softistx/nxgt-janus/blob/develop/packages/janus-hono/docs/guide/routes.md#a-link-sent-by-e-mail)
|
|
143
|
+
the same on Hono.
|
|
144
|
+
- **A sign-in code is not a link.** Its answer has a `challenge`, the
|
|
145
|
+
visitor's secret, and no `token`: the compiler refuses it here, and
|
|
146
|
+
JavaScript gets `janusMail.magicLink: token must be a string`.
|
|
147
|
+
|
|
117
148
|
### `passwordChanged(to)`
|
|
118
149
|
|
|
119
150
|
```ts
|
|
@@ -130,8 +161,10 @@ const auth = janus({
|
|
|
130
161
|
|
|
131
162
|
A notice: the password of the account at `to.email` changed, with a link to
|
|
132
163
|
`links.secureAccount()` for a user who did not change it. Send it on the
|
|
133
|
-
`user.passwordChanged` event, which `changePassword` and `setPassword` send
|
|
134
|
-
|
|
164
|
+
`user.passwordChanged` event, which `changePassword` and `setPassword` send —
|
|
165
|
+
and, from `@nxgt/janus` 0.16, `signInCode.confirm` and `magicLink.confirm`
|
|
166
|
+
when their first proof of the e-mail dropped a password someone may have set
|
|
167
|
+
without holding the inbox. A reset sends `user.passwordReset` instead, never both: to tell a reset too,
|
|
135
168
|
send the same notice on that type — `event.type === 'user.passwordChanged'
|
|
136
169
|
|| event.type === 'user.passwordReset'`. The event names the user by id
|
|
137
170
|
alone, so read the name, the locale and the address from the user.
|
|
@@ -179,17 +212,21 @@ const auth = janus({
|
|
|
179
212
|
});
|
|
180
213
|
```
|
|
181
214
|
|
|
182
|
-
Two notices: two-factor authentication was turned on, or off, for the
|
|
183
|
-
|
|
184
|
-
|
|
215
|
+
Two notices: two-factor authentication was turned on, or off, for the account
|
|
216
|
+
at `to.email`, each with a link to `links.secureAccount()` — the user's
|
|
217
|
+
security settings — for a user who did not make the change. Send
|
|
185
218
|
`twoFactorEnabled` on the `user.secondFactorEnabled` event, which
|
|
186
219
|
`auth.secondFactor.activate` sends once the factor is active, and
|
|
187
220
|
`twoFactorDisabled` on `user.secondFactorDisabled`, which
|
|
188
221
|
`auth.secondFactor.disable` sends only when it removed an active factor: a
|
|
189
|
-
`disable` on a user who had none tells nobody anything.
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
222
|
+
`disable` on a user who had none tells nobody anything. From `@nxgt/janus`
|
|
223
|
+
0.16, `signInCode.confirm` and `magicLink.confirm` send it too when their
|
|
224
|
+
first proof of the e-mail removed a factor someone may have enrolled without
|
|
225
|
+
holding the inbox — after `user.passwordChanged`, so the inbox's owner gets
|
|
226
|
+
both notices. The event names the user by id alone, so read the name, the
|
|
227
|
+
locale and the address from the user. Calling them right after `activate` or
|
|
228
|
+
`disable` answered works as well — without the events, check `hasSecondFactor`
|
|
229
|
+
before the `disable`.
|
|
193
230
|
|
|
194
231
|
### `welcome(to)`
|
|
195
232
|
|
|
@@ -276,8 +313,8 @@ when opening the session then fails.
|
|
|
276
313
|
|
|
277
314
|
## The expiry
|
|
278
315
|
|
|
279
|
-
`verifyEmail`, `resetPassword` and `
|
|
280
|
-
code lasts — "This link expires in 1 hour.", "Ce code expire dans
|
|
316
|
+
`verifyEmail`, `resetPassword`, `signInCode` and `magicLink` say how long
|
|
317
|
+
the link or the code lasts — "This link expires in 1 hour.", "Ce code expire dans
|
|
281
318
|
10 minutes." The text is `expiresIn`, one of the template's variables,
|
|
282
319
|
derived at send time from the flow's `issued.expiresAt`:
|
|
283
320
|
|
|
@@ -325,6 +362,7 @@ a `TypeError`, and nothing reaches the mailer: revive the date with
|
|
|
325
362
|
| `verifyEmail` | `issued.email` | what the visitor typed |
|
|
326
363
|
| `resetPassword` | `issued.email` | the `email` passed to `request` |
|
|
327
364
|
| `signInCode` | `issued.email` | the `email` passed to `request` |
|
|
365
|
+
| `magicLink` | `issued.email` | the `email` passed to `request` |
|
|
328
366
|
| `passwordChanged` | `to.email` | — |
|
|
329
367
|
| `emailChanged` | `to.formerEmail` | `to.newEmail` |
|
|
330
368
|
| `twoFactorEnabled`, `twoFactorDisabled` | `to.email` | — |
|
package/docs/guide/templates.md
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
# Templates
|
|
2
2
|
|
|
3
3
|
This page is for changing what an e-mail says or looks like: what a template
|
|
4
|
-
is given and answers, replacing some of the
|
|
4
|
+
is given and answers, replacing some of the ten, using the defaults on their
|
|
5
5
|
own, and what the defaults say.
|
|
6
6
|
|
|
7
7
|
```ts
|
|
@@ -62,6 +62,7 @@ e-mail.
|
|
|
62
62
|
| `verifyEmail` | `brand`, `name`, `link`, `expiresIn` | `mail.verifyEmail(issued, to, options?)` |
|
|
63
63
|
| `resetPassword` | `brand`, `name`, `link`, `expiresIn` | `mail.resetPassword(issued, to, options?)` |
|
|
64
64
|
| `signInCode` | `brand`, `code`, `expiresIn` | `mail.signInCode(issued, to?, options?)` |
|
|
65
|
+
| `magicLink` | `brand`, `link`, `expiresIn` | `mail.magicLink(issued, to?, options?)` |
|
|
65
66
|
| `passwordChanged` | `brand`, `name`, `link` | `mail.passwordChanged(to)` |
|
|
66
67
|
| `emailChanged` | `brand`, `name`, `link`, `newEmail` | `mail.emailChanged(to)` |
|
|
67
68
|
| `twoFactorEnabled` | `brand`, `name`, `link` | `mail.twoFactorEnabled(to)` |
|
|
@@ -124,11 +125,11 @@ methods live on its prototype and are refused — see
|
|
|
124
125
|
[troubleshooting](../troubleshooting.md#janusmail-templatesname-is-not-an-own-enumerable-property--pass-a-plain-object-as--name-variables--rendered-).
|
|
125
126
|
|
|
126
127
|
`templates` is `Partial` while `locales` stays within `en` and `fr`: give
|
|
127
|
-
any of the
|
|
128
|
-
another locale, it takes **all
|
|
128
|
+
any of the ten, and the defaults render the rest. Once `locales` holds
|
|
129
|
+
another locale, it takes **all ten** — see
|
|
129
130
|
[Adding a locale](locales.md#adding-a-locale).
|
|
130
131
|
|
|
131
|
-
`mail.templates` answers the
|
|
132
|
+
`mail.templates` answers the ten in use — yours, and the defaults for the
|
|
132
133
|
rest — frozen:
|
|
133
134
|
|
|
134
135
|
```ts
|
|
@@ -150,7 +151,7 @@ const { subject, html, text } = await defaults.resetPassword({
|
|
|
150
151
|
});
|
|
151
152
|
```
|
|
152
153
|
|
|
153
|
-
It answers the
|
|
154
|
+
It answers the ten defaults, typed for `'en' | 'fr'`, without a mailer:
|
|
154
155
|
to preview an e-mail, to wrap one — add a line to the default text, say — or
|
|
155
156
|
to send one through something other than `janusMail()`. A default reused for
|
|
156
157
|
a wider set of locales is a compile error: it could not render the one it
|
|
@@ -183,6 +184,7 @@ The defaults are [`@nxgt/mail-presets`](https://www.npmjs.com/package/@nxgt/mail
|
|
|
183
184
|
| `verifyEmail` | `verify-email` | Confirm your e-mail address | Confirmez votre adresse e-mail |
|
|
184
185
|
| `resetPassword` | `reset-password` | Reset your password | Réinitialisez votre mot de passe |
|
|
185
186
|
| `signInCode` | `sign-in-code` | Your sign-in code: `042817` | Votre code de connexion : `042817` |
|
|
187
|
+
| `magicLink` | `magic-link` | Your sign-in link | Votre lien de connexion |
|
|
186
188
|
| `passwordChanged` | `password-changed` | Your password was changed | Votre mot de passe a été modifié |
|
|
187
189
|
| `emailChanged` | `email-changed` | Your e-mail address was changed | Votre adresse e-mail a été modifiée |
|
|
188
190
|
| `twoFactorEnabled` | `two-factor-enabled` | Two-factor authentication was turned on | L'authentification à deux facteurs a été activée |
|
|
@@ -191,7 +193,7 @@ The defaults are [`@nxgt/mail-presets`](https://www.npmjs.com/package/@nxgt/mail
|
|
|
191
193
|
| `welcome` | `welcome` | Welcome, `Ada` | Bienvenue, `Ada` |
|
|
192
194
|
|
|
193
195
|
Each has a header and a footer with the brand, a heading, a greeting by name
|
|
194
|
-
(but `signInCode`), a button with its link and the link again in text for a
|
|
196
|
+
(but `signInCode` and `magicLink`), a button with its link and the link again in text for a
|
|
195
197
|
client that shows no button, and a text part. The other four notices add a warning:
|
|
196
198
|
*if this was not you, secure your account now*. `recoveryCodeUsed` says it
|
|
197
199
|
too, after a warning banner — *a recovery code was used on your account at
|
|
@@ -200,8 +202,8 @@ too, after a warning banner — *a recovery code was used on your account at
|
|
|
200
202
|
its heading — "Welcome to Acme" — and the recipient's name in its subject,
|
|
201
203
|
filled at send time like the body; its button says **Get started**.
|
|
202
204
|
|
|
203
|
-
`verifyEmail`, `resetPassword` and `
|
|
204
|
-
or the code lasts: "This link expires in 1 hour.", "Ce code expire dans
|
|
205
|
+
`verifyEmail`, `resetPassword`, `signInCode` and `magicLink` also say how
|
|
206
|
+
long the link or the code lasts: "This link expires in 1 hour.", "Ce code expire dans
|
|
205
207
|
10 minutes." The text part keeps each paragraph on one line.
|
|
206
208
|
|
|
207
209
|
**The HTML follows dark mode.** Each HTML part declares
|
package/docs/roadmap.md
CHANGED
|
@@ -9,9 +9,9 @@ Nothing between releases.
|
|
|
9
9
|
|
|
10
10
|
## Next
|
|
11
11
|
|
|
12
|
-
- **The other presets** — `
|
|
12
|
+
- **The other presets** — `new-sign-in` and `invitation` from
|
|
13
13
|
`@nxgt/mail-presets`, each once `@nxgt/janus` has a flow that sends it: a
|
|
14
|
-
sign-in
|
|
14
|
+
sign-in from a new device, an invitation to a user type.
|
|
15
15
|
- **The other presets of `@nxgt/mail-presets` 0.4.0** — `account-deleted`,
|
|
16
16
|
after a user is deleted, once `@nxgt/janus` has a deletion a link can
|
|
17
17
|
undo; and `invitation-accepted`, once it has invitations.
|
|
@@ -47,6 +47,15 @@ Nothing between releases.
|
|
|
47
47
|
|
|
48
48
|
Newest first; from the first release on, the package's CHANGELOG holds every one.
|
|
49
49
|
|
|
50
|
+
- **The sign-in link, v0.7.0.** `magicLink(issued, to?, options?)`, built
|
|
51
|
+
from `@nxgt/mail-presets` 1.0.1's `magic-link` in English and French:
|
|
52
|
+
send what `@nxgt/janus` 0.15's `auth.magicLink.request(email)` answered,
|
|
53
|
+
to `issued.email`, its button built by the new, optional
|
|
54
|
+
`links.magicLink(token)` and its expiry read from `issued.expiresAt` —
|
|
55
|
+
"This link expires in 15 minutes." Without `links.magicLink`, the send is
|
|
56
|
+
a `TypeError`. Ten e-mails now, and ten templates to pass for a locale
|
|
57
|
+
beyond `en` and `fr`.
|
|
58
|
+
|
|
50
59
|
- **The change notices on their events, v0.6.0.** `passwordChanged` and
|
|
51
60
|
`emailChanged` are sent from `@nxgt/janus` 0.14's `user.passwordChanged`
|
|
52
61
|
and `user.emailChanged` events, as the two-factor notices are, rather than
|
package/docs/troubleshooting.md
CHANGED
|
@@ -28,18 +28,20 @@ How the messages are shaped:
|
|
|
28
28
|
- [`janusMail: links must be an object, as { verifyEmail, resetPassword, secureAccount, getStarted }`](#janusmail-links-must-be-an-object-as--verifyemail-resetpassword-secureaccount-getstarted-)
|
|
29
29
|
- [`janusMail: links.<name> must be a function`](#janusmail-linksname-must-be-a-function)
|
|
30
30
|
- [`janusMail: links.recoveryCodes must be a function, or left out`](#janusmail-linksrecoverycodes-must-be-a-function-or-left-out)
|
|
31
|
+
- [`janusMail: links.magicLink must be a function, or left out`](#janusmail-linksmagiclink-must-be-a-function-or-left-out)
|
|
31
32
|
- [`janusMail: locales must list at least one locale, as ['en', 'fr']`](#janusmail-locales-must-list-at-least-one-locale-as-en-fr)
|
|
32
33
|
- [`janusMail: locales must be BCP 47 language tags, as 'fr-CA'`](#janusmail-locales-must-be-bcp-47-language-tags-as-fr-ca)
|
|
33
34
|
- [`janusMail: locales holds the same locale twice`](#janusmail-locales-holds-the-same-locale-twice)
|
|
34
35
|
- [`janusMail: fallbackLocale must be one of locales`](#janusmail-fallbacklocale-must-be-one-of-locales)
|
|
35
36
|
- [`janusMail: templates must be an object of functions, as { verifyEmail: (variables) => rendered }`](#janusmail-templates-must-be-an-object-of-functions-as--verifyemail-variables--rendered-)
|
|
36
|
-
- [`janusMail: templates has no template <name> — name one of verifyEmail, resetPassword, signInCode, passwordChanged, emailChanged, twoFactorEnabled, twoFactorDisabled, recoveryCodeUsed, welcome`](#janusmail-templates-has-no-template-name--name-one-of-verifyemail-resetpassword-signincode-passwordchanged-emailchanged-twofactorenabled-twofactordisabled-recoverycodeused-welcome)
|
|
37
|
+
- [`janusMail: templates has no template <name> — name one of verifyEmail, resetPassword, signInCode, magicLink, passwordChanged, emailChanged, twoFactorEnabled, twoFactorDisabled, recoveryCodeUsed, welcome`](#janusmail-templates-has-no-template-name--name-one-of-verifyemail-resetpassword-signincode-magiclink-passwordchanged-emailchanged-twofactorenabled-twofactordisabled-recoverycodeused-welcome)
|
|
37
38
|
- [`janusMail: templates.<name> must be a function`](#janusmail-templatesname-must-be-a-function)
|
|
38
39
|
- [`janusMail: templates.<name> is not an own enumerable property — pass a plain object, as { <name>: (variables) => rendered }`](#janusmail-templatesname-is-not-an-own-enumerable-property--pass-a-plain-object-as--name-variables--rendered-)
|
|
39
40
|
- [`janusMail: clock must be a Clock — an object with a now function`](#janusmail-clock-must-be-a-clock--an-object-with-a-now-function)
|
|
40
41
|
- [`janusMail: the default templates are built in en and fr only — with another locale in locales, pass every template in templates; <names> missing`](#janusmail-the-default-templates-are-built-in-en-and-fr-only--with-another-locale-in-locales-pass-every-template-in-templates-names-missing)
|
|
41
42
|
|
|
42
43
|
**Sending**
|
|
44
|
+
- [`janusMail.magicLink: links.magicLink is missing — pass it to janusMail({ links }) to send sign-in links`](#janusmailmagiclink-linksmagiclink-is-missing--pass-it-to-janusmail-links--to-send-sign-in-links)
|
|
43
45
|
- [`janusMail.<method>: <field> must be a string`](#janusmailmethod-field-must-be-a-string)
|
|
44
46
|
- [`janusMail.<method>: expiresAt must be a Date`](#janusmailmethod-expiresat-must-be-a-date)
|
|
45
47
|
- [`janusMail.recoveryCodeUsed: recoveryCodesLeft must be a count — a whole number, 0 or more — or the sentence to show`](#janusmailrecoverycodeused-recoverycodesleft-must-be-a-count--a-whole-number-0-or-more--or-the-sentence-to-show)
|
|
@@ -65,6 +67,7 @@ How the messages are shaped:
|
|
|
65
67
|
- [`TS2345: … Types of property 'expiresAt' are incompatible. Type 'string' is not assignable to type 'Date'.`](#ts2345--types-of-property-expiresat-are-incompatible-type-string-is-not-assignable-to-type-date)
|
|
66
68
|
- [`TS2739: Type '{ … }' is missing the following properties from type 'JanusMailTemplates<…>'`](#ts2739-type----is-missing-the-following-properties-from-type-janusmailtemplates)
|
|
67
69
|
- [`TS2741: Property 'recoveryCodeUsed' is missing in type '{ … }' but required in type 'JanusMailTemplates<…>'.`](#ts2741-property-recoverycodeused-is-missing-in-type----but-required-in-type-janusmailtemplates)
|
|
70
|
+
- [`TS2741: Property 'magicLink' is missing in type '{ … }' but required in type 'JanusMailTemplates<…>'.`](#ts2741-property-magiclink-is-missing-in-type----but-required-in-type-janusmailtemplates)
|
|
68
71
|
- [`TS2322: Type '"de"' is not assignable to type '"en" | "fr"'.`](#ts2322-type-de-is-not-assignable-to-type-en--fr)
|
|
69
72
|
- [`TS2741: Property 'getStarted' is missing in type '{ … }' but required in type 'JanusMailLinks'.`](#ts2741-property-getstarted-is-missing-in-type----but-required-in-type-janusmaillinks)
|
|
70
73
|
- [`TS2322: Type 'number | null' is not assignable to type 'string | number'.`](#ts2322-type-number--null-is-not-assignable-to-type-string--number)
|
|
@@ -114,7 +117,8 @@ janusMail({ mailer, from, brand: 'Acme', links });
|
|
|
114
117
|
### `janusMail: links must be an object, as { verifyEmail, resetPassword, secureAccount, getStarted }`
|
|
115
118
|
|
|
116
119
|
No `links`. Four are required: the notices link to `secureAccount`, and
|
|
117
|
-
the welcome to `getStarted`.
|
|
120
|
+
the welcome to `getStarted`. Two more, `recoveryCodes` and `magicLink`, are
|
|
121
|
+
optional.
|
|
118
122
|
|
|
119
123
|
### `janusMail: links.<name> must be a function`
|
|
120
124
|
|
|
@@ -148,6 +152,18 @@ regenerates their codes:
|
|
|
148
152
|
links: { …, recoveryCodes: () => 'https://acme.example/account/recovery-codes' },
|
|
149
153
|
```
|
|
150
154
|
|
|
155
|
+
### `janusMail: links.magicLink must be a function, or left out`
|
|
156
|
+
|
|
157
|
+
`links.magicLink` given as a URL, or as anything but a function. It is
|
|
158
|
+
optional — an application that sends no sign-in link leaves it out — but
|
|
159
|
+
when given it takes the link's one-time token and answers **a page of
|
|
160
|
+
yours that spends nothing**, whose button posts the token to the route
|
|
161
|
+
calling `auth.magicLink.confirm`:
|
|
162
|
+
|
|
163
|
+
```ts
|
|
164
|
+
links: { …, magicLink: (token) => `https://acme.example/sign-in/link?token=${token}` },
|
|
165
|
+
```
|
|
166
|
+
|
|
151
167
|
### `janusMail: locales must list at least one locale, as ['en', 'fr']`
|
|
152
168
|
|
|
153
169
|
`locales: []`, `locales: 'en'`, or a list holding something that is not a
|
|
@@ -180,9 +196,9 @@ A `fallbackLocale` the list does not hold — `'de'` with the default
|
|
|
180
196
|
|
|
181
197
|
`templates` given as a list or a function. Key each template by its name.
|
|
182
198
|
|
|
183
|
-
### `janusMail: templates has no template <name> — name one of verifyEmail, resetPassword, signInCode, passwordChanged, emailChanged, twoFactorEnabled, twoFactorDisabled, recoveryCodeUsed, welcome`
|
|
199
|
+
### `janusMail: templates has no template <name> — name one of verifyEmail, resetPassword, signInCode, magicLink, passwordChanged, emailChanged, twoFactorEnabled, twoFactorDisabled, recoveryCodeUsed, welcome`
|
|
184
200
|
|
|
185
|
-
A key that is not one of the
|
|
201
|
+
A key that is not one of the ten — `newSignIn`, or the e-mail's file name
|
|
186
202
|
`'verify-email'` rather than the template's, `verifyEmail`. Other e-mails are
|
|
187
203
|
on the [roadmap](roadmap.md); send them with `@nxgt/mail` directly meanwhile.
|
|
188
204
|
|
|
@@ -216,7 +232,7 @@ as it is: fields are own properties.
|
|
|
216
232
|
### `janusMail: the default templates are built in en and fr only — with another locale in locales, pass every template in templates; <names> missing`
|
|
217
233
|
|
|
218
234
|
`locales` holds a locale beyond `en` and `fr`, and `templates` leaves some
|
|
219
|
-
out: the defaults could not render them in that locale. Pass all
|
|
235
|
+
out: the defaults could not render them in that locale. Pass all ten — see
|
|
220
236
|
[Adding a locale](guide/locales.md#adding-a-locale) — or drop the locale.
|
|
221
237
|
The message lists the templates missing.
|
|
222
238
|
|
|
@@ -234,6 +250,23 @@ janusMail({ mailer, from, brand: 'Acme', links, clock });
|
|
|
234
250
|
|
|
235
251
|
## Sending
|
|
236
252
|
|
|
253
|
+
### `janusMail.magicLink: links.magicLink is missing — pass it to janusMail({ links }) to send sign-in links`
|
|
254
|
+
|
|
255
|
+
A `TypeError` from `mail.magicLink`, before anything is rendered or sent:
|
|
256
|
+
`janusMail()` was given no `links.magicLink`. It is optional, so
|
|
257
|
+
`janusMail()` accepts `links` without it — written before 0.7, or for an
|
|
258
|
+
application that sent no sign-in link — and there is no page to fall back
|
|
259
|
+
on: only yours can carry the token. Add it:
|
|
260
|
+
|
|
261
|
+
```ts
|
|
262
|
+
const mail = janusMail({
|
|
263
|
+
…,
|
|
264
|
+
links: { …, magicLink: (token) => `https://acme.example/sign-in/link?token=${token}` },
|
|
265
|
+
});
|
|
266
|
+
```
|
|
267
|
+
|
|
268
|
+
Sending again fails again: it is a bug, not an outage.
|
|
269
|
+
|
|
237
270
|
### `janusMail.<method>: <field> must be a string`
|
|
238
271
|
|
|
239
272
|
A `TypeError` from a call, naming the method and the field it could not read
|
|
@@ -247,6 +280,10 @@ const issued = await auth.resetPassword.request(email);
|
|
|
247
280
|
if (issued !== null) await mail.resetPassword(issued, { name: issued.user.name });
|
|
248
281
|
```
|
|
249
282
|
|
|
283
|
+
`janusMail.magicLink: token must be a string` is a sign-in code's answer
|
|
284
|
+
given to `magicLink` — it has a `challenge`, never mailed, and no token:
|
|
285
|
+
send it with `signInCode`, or ask `auth.magicLink.request` for a link.
|
|
286
|
+
|
|
250
287
|
`issued` needs `token` and `email` (`code` and `email` for `signInCode`), and
|
|
251
288
|
the recipient its `name` — a user whose schema has no name passes one
|
|
252
289
|
anyway: `{ name: user.email }`. `janusMail.verifyEmail: expiresIn must be a
|
|
@@ -285,8 +322,8 @@ formatted in the recipient's locale and time zone —
|
|
|
285
322
|
|
|
286
323
|
### `janusMail.<method>: expiresAt must be a Date`
|
|
287
324
|
|
|
288
|
-
A `TypeError` from `verifyEmail`, `resetPassword
|
|
289
|
-
says how long its link or code lasts, from `issued.expiresAt`, and that is
|
|
325
|
+
A `TypeError` from `verifyEmail`, `resetPassword`, `signInCode` or
|
|
326
|
+
`magicLink`: the e-mail says how long its link or code lasts, from `issued.expiresAt`, and that is
|
|
290
327
|
not a valid `Date`. The usual cause is a flow's answer that went through
|
|
291
328
|
JSON — a job queue, a cache — where a `Date` becomes a string. Nothing is
|
|
292
329
|
sent. Revive it when the job runs, or say the expiry yourself:
|
|
@@ -326,8 +363,8 @@ await mail.recoveryCodeUsed(to, { when, recoveryCodesLeft: `Sie haben noch ${lef
|
|
|
326
363
|
|
|
327
364
|
### `janusMail.<method>: clock.now() must answer a Date`
|
|
328
365
|
|
|
329
|
-
A `TypeError` from `verifyEmail`, `resetPassword
|
|
330
|
-
`clock` given to `janusMail()` has a `now` that answered something else — a
|
|
366
|
+
A `TypeError` from `verifyEmail`, `resetPassword`, `signInCode` or
|
|
367
|
+
`magicLink`: the `clock` given to `janusMail()` has a `now` that answered something else — a
|
|
331
368
|
number from `Date.now()`, a string, an invalid `Date`. Nothing is sent.
|
|
332
369
|
`clock` is `@nxgt/janus`'s `Clock`: pass the one `janus({ clock })` was
|
|
333
370
|
given, or `{ now: () => new Date() }`.
|
|
@@ -384,7 +421,7 @@ A `TypeError` from a call, before anything is rendered or sent
|
|
|
384
421
|
must answer a string`, `janusMail.emailChanged: links.secureAccount() must
|
|
385
422
|
answer a string`, `janusMail.welcome: links.getStarted() must answer a
|
|
386
423
|
string`, `janusMail.recoveryCodeUsed: links.recoveryCodes() must answer a
|
|
387
|
-
string`. The `links` function answered something other than a
|
|
424
|
+
string`, `janusMail.magicLink: links.magicLink(token) must answer a string`. The `links` function answered something other than a
|
|
388
425
|
string: a `URL` object, `undefined`, or a promise — an `async` function.
|
|
389
426
|
Answer `url.href`, and compute the link synchronously:
|
|
390
427
|
|
|
@@ -460,7 +497,7 @@ avoids the read altogether. (In this repository: run `bun run build`.)
|
|
|
460
497
|
|
|
461
498
|
The default templates read `mails/` with `node:fs`, through
|
|
462
499
|
`@nxgt/mail/renderer`: Node, Bun or Deno only. On an edge runtime, pass all
|
|
463
|
-
|
|
500
|
+
ten templates yourself; an inlined build is on the [roadmap](roadmap.md).
|
|
464
501
|
|
|
465
502
|
### An e-mail stays light in dark mode
|
|
466
503
|
|
|
@@ -576,7 +613,9 @@ if (issued !== null) await mail.resetPassword(issued, { name: issued.user.name }
|
|
|
576
613
|
return new Response(null, { status: 202 });
|
|
577
614
|
```
|
|
578
615
|
|
|
579
|
-
|
|
616
|
+
`auth.magicLink.request(email)`'s answer, passed to `mail.magicLink`
|
|
617
|
+
unchecked, reads the same. The same holds for `signInCode.request`, whose
|
|
618
|
+
answer passed unchecked reads
|
|
580
619
|
`Argument of type 'IssuedCode<…> | null' is not assignable to parameter of
|
|
581
620
|
type 'Pick<IssuedCode<unknown>, "email" | "code" | "expiresAt">'`.
|
|
582
621
|
|
|
@@ -591,19 +630,27 @@ whole.
|
|
|
591
630
|
### `TS2739: Type '{ … }' is missing the following properties from type 'JanusMailTemplates<…>'`
|
|
592
631
|
|
|
593
632
|
`locales` holds a locale beyond `en` and `fr`, and `templates` does not give
|
|
594
|
-
all
|
|
595
|
-
`…, and
|
|
633
|
+
all ten. With five or more missing, the code is `TS2740` and the list ends
|
|
634
|
+
`…, and 5 more.` See [Adding a locale](guide/locales.md#adding-a-locale).
|
|
596
635
|
|
|
597
636
|
### `TS2741: Property 'recoveryCodeUsed' is missing in type '{ … }' but required in type 'JanusMailTemplates<…>'.`
|
|
598
637
|
|
|
599
638
|
`templates` written for 0.4, with a locale beyond `en` and `fr`: since
|
|
600
|
-
0.5.0
|
|
639
|
+
0.5.0 `recoveryCodeUsed` is one of the templates, and with such a locale every one is yours.
|
|
601
640
|
Add `recoveryCodeUsed` — it is given `brand`, `name`, `when`,
|
|
602
641
|
`recoveryCodesLeft` (a sentence) and `link` — even if you never send it; in
|
|
603
642
|
JavaScript the same `templates` is
|
|
604
643
|
[`janusMail: the default templates are built in en and fr only — …; recoveryCodeUsed missing`](#janusmail-the-default-templates-are-built-in-en-and-fr-only--with-another-locale-in-locales-pass-every-template-in-templates-names-missing).
|
|
605
644
|
See [Templates](guide/templates.md#what-each-template-is-given).
|
|
606
645
|
|
|
646
|
+
### `TS2741: Property 'magicLink' is missing in type '{ … }' but required in type 'JanusMailTemplates<…>'.`
|
|
647
|
+
|
|
648
|
+
`templates` written before 0.7, with a locale beyond `en` and `fr`: since
|
|
649
|
+
0.7.0 there are ten templates, and with such a locale every one is yours.
|
|
650
|
+
Add `magicLink` — it is given `brand`, `link` and `expiresIn` — even if you
|
|
651
|
+
never send it; in JavaScript the same `templates` is
|
|
652
|
+
[`janusMail: the default templates are built in en and fr only — …; magicLink missing`](#janusmail-the-default-templates-are-built-in-en-and-fr-only--with-another-locale-in-locales-pass-every-template-in-templates-names-missing).
|
|
653
|
+
|
|
607
654
|
### `TS2322: Type '"de"' is not assignable to type '"en" | "fr"'.`
|
|
608
655
|
|
|
609
656
|
A `fallbackLocale` outside `locales`. Add it to `locales` — with every
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
<!DOCTYPE html>
|
|
2
|
+
<html lang="en" dir="ltr" xmlns:v="urn:schemas-microsoft-com:vml" xmlns:o="urn:schemas-microsoft-com:office:office" style="color-scheme:light dark;">
|
|
3
|
+
<head>
|
|
4
|
+
<meta charset="utf-8">
|
|
5
|
+
<meta name="x-apple-disable-message-reformatting">
|
|
6
|
+
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
7
|
+
<meta name="color-scheme" content="light dark">
|
|
8
|
+
<meta name="supported-color-schemes" content="light dark">
|
|
9
|
+
<style>[data-ogsc] .nx-dark-bg-paper{background-color:#040a19!important;}[data-ogsb] .nx-dark-bg-paper{background-color:#040a19!important;}[data-ogsc] .nx-dark-bg-card{background-color:#0f172b!important;background-color:lab(7.78673% 1.82345 -15.0537)!important;}[data-ogsb] .nx-dark-bg-card{background-color:#0f172b!important;background-color:lab(7.78673% 1.82345 -15.0537)!important;}[data-ogsc] .nx-dark-bg-background{background-color:#020918!important;background-color:lab(2.35721% .367686
|
|
10
|
+
-8.51797)!important;}[data-ogsb] .nx-dark-bg-background{background-color:#020918!important;background-color:lab(2.35721% .367686 -8.51797)!important;}[data-ogsc] .nx-dark-text-foreground{color:#f8fafc!important;color:lab(98.1434% -.369519 -1.05966)!important;}[data-ogsb] .nx-dark-text-foreground{color:#f8fafc!important;color:lab(98.1434% -.369519 -1.05966)!important;}[data-ogsc] .nx-dark-text-foreground-25{color:#404551!important;}[data-ogsb] .nx-dark-text-foreground-25{color:#404551!important;}
|
|
11
|
+
[data-ogsc] .nx-dark-text-card-foreground{color:#f8fafc!important;color:lab(98.1434% -.369519 -1.05966)!important;}[data-ogsb] .nx-dark-text-card-foreground{color:#f8fafc!important;color:lab(98.1434% -.369519 -1.05966)!important;}[data-ogsc] .nx-dark-border-border{border-color:#1c212f!important;}[data-ogsb] .nx-dark-border-border{border-color:#1c212f!important;}[data-ogsc] .nx-dark-bg-muted{background-color:#1e293b!important;}[data-ogsb] .nx-dark-bg-muted{background-color:#1e293b!important;}
|
|
12
|
+
[data-ogsc] .nx-dark-text-muted-foreground{color:#cbd5e1!important;}[data-ogsb] .nx-dark-text-muted-foreground{color:#cbd5e1!important;}[data-ogsc] .nx-dark-border-muted-foreground{border-color:#cbd5e1!important;}[data-ogsb] .nx-dark-border-muted-foreground{border-color:#cbd5e1!important;}[data-ogsc] .nx-dark-border-muted{border-color:#1e293b!important;}[data-ogsb] .nx-dark-border-muted{border-color:#1e293b!important;}[data-ogsc] .nx-dark-bg-primary{background-color:#fafafa!important;}
|
|
13
|
+
[data-ogsb] .nx-dark-bg-primary{background-color:#fafafa!important;}[data-ogsc] .nx-dark-text-primary{color:#fafafa!important;}[data-ogsb] .nx-dark-text-primary{color:#fafafa!important;}[data-ogsc] .nx-dark-text-primary-foreground{color:#18181b!important;}[data-ogsb] .nx-dark-text-primary-foreground{color:#18181b!important;}[data-ogsc] .nx-dark-text-info{color:#93c5fd!important;}[data-ogsb] .nx-dark-text-info{color:#93c5fd!important;}[data-ogsc] .nx-dark-border-primary{border-color:#fafafa
|
|
14
|
+
!important;}[data-ogsb] .nx-dark-border-primary{border-color:#fafafa!important;}[data-ogsc] .nx-dark-bg-primary-15{background-color:#272d3a!important;}[data-ogsb] .nx-dark-bg-primary-15{background-color:#272d3a!important;}[data-ogsc] .nx-dark-bg-primary-20{background-color:#343945!important;}[data-ogsb] .nx-dark-bg-primary-20{background-color:#343945!important;}[data-ogsc] .nx-dark-border-primary-40{border-color:#656972!important;}[data-ogsb] .nx-dark-border-primary-40{border-color:#656972
|
|
15
|
+
!important;}[data-ogsc] .nx-dark-border-primary-50{border-color:#7e8189!important;}[data-ogsb] .nx-dark-border-primary-50{border-color:#7e8189!important;}[data-ogsc] .nx-light-only{display:none!important;}[data-ogsb] .nx-light-only{display:none!important;}[data-ogsc] .nx-dark-only{display:inline-block!important;}[data-ogsb] .nx-dark-only{display:inline-block!important;}
|
|
16
|
+
@media (prefers-color-scheme:dark){html{--lightningcss-light:;--lightningcss-dark:initial;}.nx-dark-bg-paper{background-color:#040a19!important;}.nx-dark-bg-card{background-color:#0f172b!important;background-color:lab(7.78673% 1.82345 -15.0537)!important;}.nx-dark-text-foreground{color:#f8fafc!important;color:lab(98.1434% -.369519 -1.05966)!important;}.nx-dark-text-card-foreground{color:#f8fafc!important;color:lab(98.1434% -.369519 -1.05966)!important;}.nx-dark-border-border{border-color:#1c212f
|
|
17
|
+
!important;}.nx-dark-text-muted-foreground{color:#cbd5e1!important;}.nx-dark-bg-primary{background-color:#fafafa!important;}.nx-dark-text-primary-foreground{color:#18181b!important;}.nx-dark-text-info{color:#93c5fd!important;} }
|
|
18
|
+
</style>
|
|
19
|
+
</head>
|
|
20
|
+
<body xml:lang="en" dir="ltr" style="margin:0;padding:0;width:100%;height:100%;word-break:break-word;background-color:#f4f4f4;" class="nx-dark-bg-paper"><div style="display:none;">One click to sign in. ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏
|
|
21
|
+
 ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏
|
|
22
|
+
 ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏
|
|
23
|
+
 ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏
|
|
24
|
+
 ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏
|
|
25
|
+
 ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏
|
|
26
|
+
 ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏
|
|
27
|
+
 ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏  ͏
|
|
28
|
+
 ͏  ͏  ͏  ͏ </div><span style="display:none">
|
|
29
|
+
<!--[if mso]><xml><o:OfficeDocumentSettings><o:PixelsPerInch>96</o:PixelsPerInch></o:OfficeDocumentSettings><w:WordDocument><w:DontUseAdvancedTypographyReadingMail/></w:WordDocument></xml>
|
|
30
|
+
<![endif]--></span><div role="article" aria-roledescription="email" lang="en" dir="ltr" style="font-size:medium;font-size:max(16px,1rem)">
|
|
31
|
+
<table class="nx-dark-bg-paper" role="presentation" cellpadding="0" cellspacing="0" dir="ltr" style="width:100%;background-color:#f4f4f4;font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont, 'Segoe UI',Roboto, 'Noto Sans',Ubuntu,Cantarell, 'Helvetica Neue',sans-serif;"><tr><td align="center" style="padding:32px 16px;">
|
|
32
|
+
<!--[if mso]>
|
|
33
|
+
<table role="none" cellpadding="0" cellspacing="0" style="width:600px" align="center"><tr><td>
|
|
34
|
+
<![endif]--><div style="margin:0 auto;max-width:600px;">
|
|
35
|
+
<table role="presentation" cellpadding="0" cellspacing="0" dir="ltr" style="width:100%;"><tr><td style="padding-bottom:24px;text-align:center;"><span class="nx-dark-text-foreground" style="font-size:18px;line-height:28px;font-weight:600;color:#020918;color:lab(2.35721% .367686 -8.51797);">{{ brand }}</span>
|
|
36
|
+
</td></tr><tr><td class="nx-dark-border-border nx-dark-bg-card nx-dark-text-card-foreground" style="border-radius:14px;border-color:#e2e8f0;border-style:solid;border-width:1px;border-color:lab(91.7353% -.998765 -4.76968);background-color:#fff;background-color:lab(100% 0 0);padding:32px;font-size:14px;line-height:20px;color:#020618;color:lab(1.76974% 1.32743 -9.28855);box-shadow:0 0 rgba(0,0,0,0),0 0 rgba(0,0,0,0),0 0 rgba(0,0,0,0),0 0 rgba(0,0,0,0),0 1px 3px rgba(0,0,0,.1),0 1px 2px -1px rgba(0,
|
|
37
|
+
0,0,.1);"><h1 class="nx-dark-text-foreground" style="margin:0 0 16px;font-size:24px;line-height:32px;font-weight:600;color:#020918;color:lab(2.35721% .367686 -8.51797);">Sign in to {{ brand }}</h1><p class="nx-dark-text-foreground" style="margin:0 0 16px;font-size:14px;line-height:20px;color:#020918;color:lab(2.35721% .367686 -8.51797);">Click the button below to sign in. The link works once.</p><div><a style="display:inline-block;border-radius:9999px;background-color:#27272a;padding:10px 16px;
|
|
38
|
+
font-family:ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont, 'Segoe UI',Roboto, 'Noto Sans',Ubuntu,Cantarell, 'Helvetica Neue',sans-serif;font-size:14px;line-height:16px;font-weight:500;color:#f8fafc;color:lab(98.1434% -.369519 -1.05966);text-decoration:none;" href="{{ link }}" class="nx-dark-bg-primary nx-dark-text-primary-foreground">
|
|
39
|
+
<!--[if mso]><i style="mso-font-width:114%;mso-text-raise:19px;" hidden> </i>
|
|
40
|
+
<![endif]--> <span style="mso-text-raise:10px;">Sign in</span>
|
|
41
|
+
<!--[if mso]><i style="mso-font-width:114%;" hidden> ​</i>
|
|
42
|
+
<![endif]--></a></div><div role="separator" style="line-height:24px;">‍</div><p class="nx-dark-text-foreground" style="margin:0 0 16px;font-size:12px;line-height:16px;color:#020918;color:lab(2.35721% .367686 -8.51797);">This link expires in {{ expiresIn }}.</p><p class="nx-dark-text-foreground" style="margin:0 0 16px;font-size:12px;line-height:16px;color:#5f718a;">If the button does not work, open this link: <a href="{{ link }}" class="nx-dark-text-info" style="color:#1d4ed8;text-decoration:
|
|
43
|
+
underline;">{{ link }}</a></p><p class="nx-dark-text-muted-foreground" style="margin:0 0 16px;font-size:10px;color:#5f718a;">If you did not ask for this, you can ignore this e-mail.</p>
|
|
44
|
+
</td></tr><tr><td class="nx-dark-text-muted-foreground" style="padding-left:24px;padding-right:24px;padding-top:24px;text-align:center;font-size:12px;line-height:16px;color:#5f718a;"><p style="margin:0 0 8px;">You received this e-mail because you have an account with {{ brand }}.</p><p style="margin:0;">{{ brand }}</p>
|
|
45
|
+
</td></tr></table></div>
|
|
46
|
+
<!--[if mso]>
|
|
47
|
+
</td></tr></table>
|
|
48
|
+
<![endif]-->
|
|
49
|
+
</td></tr></table></div>
|
|
50
|
+
</body>
|
|
51
|
+
</html>
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
One click to sign in.
|
|
2
|
+
|
|
3
|
+
{{ brand }}
|
|
4
|
+
|
|
5
|
+
Sign in to {{ brand }}
|
|
6
|
+
|
|
7
|
+
Click the button below to sign in. The link works once.
|
|
8
|
+
|
|
9
|
+
Sign in
|
|
10
|
+
|
|
11
|
+
{{ link }}
|
|
12
|
+
|
|
13
|
+
This link expires in {{ expiresIn }}.
|
|
14
|
+
|
|
15
|
+
If the button does not work, open this link: {{ link }}
|
|
16
|
+
|
|
17
|
+
If you did not ask for this, you can ignore this e-mail.
|
|
18
|
+
|
|
19
|
+
You received this e-mail because you have an account with {{ brand }}.
|
|
20
|
+
|
|
21
|
+
{{ brand }}
|