@nxgt/janus 0.5.0 → 0.7.0
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 +116 -23
- package/dist/auth/config.d.ts +3 -0
- package/dist/auth/config.d.ts.map +1 -1
- package/dist/auth/context.d.ts +12 -0
- package/dist/auth/context.d.ts.map +1 -1
- package/dist/auth/email-flows.d.ts +11 -0
- package/dist/auth/email-flows.d.ts.map +1 -0
- package/dist/auth/index.d.ts +1 -1
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/one-time.d.ts +66 -6
- package/dist/auth/one-time.d.ts.map +1 -1
- package/dist/auth/password-written.d.ts +24 -0
- package/dist/auth/password-written.d.ts.map +1 -0
- package/dist/auth/port/assert-stores.d.ts.map +1 -1
- package/dist/auth/port/types.d.ts +23 -1
- package/dist/auth/port/types.d.ts.map +1 -1
- package/dist/auth/second-factor/challenge.d.ts +0 -6
- package/dist/auth/second-factor/challenge.d.ts.map +1 -1
- package/dist/auth/second-factor/factor.d.ts +1 -4
- package/dist/auth/second-factor/factor.d.ts.map +1 -1
- package/dist/auth/second-factor/flows.d.ts +7 -3
- package/dist/auth/second-factor/flows.d.ts.map +1 -1
- package/dist/auth/second-factor/lifecycle.d.ts.map +1 -1
- package/dist/auth/sign-in-code.d.ts +20 -0
- package/dist/auth/sign-in-code.d.ts.map +1 -0
- package/dist/auth/types.d.ts +56 -1
- package/dist/auth/types.d.ts.map +1 -1
- package/dist/auth/users.d.ts +2 -2
- package/dist/auth/users.d.ts.map +1 -1
- package/dist/chunks/{index-gbwn2tts.js → index-c0jajeda.js} +12 -2
- package/dist/chunks/{index-gbwn2tts.js.map → index-c0jajeda.js.map} +3 -3
- package/dist/conformance/cases/outage.d.ts.map +1 -1
- package/dist/conformance/cases/tokens.d.ts.map +1 -1
- package/dist/conformance/index.js +73 -2
- package/dist/conformance/index.js.map +4 -4
- package/dist/index.js +253 -116
- package/dist/index.js.map +15 -12
- package/docs/README.md +2 -1
- package/docs/guide/adapters.md +56 -3
- package/docs/guide/email-flows.md +11 -1
- package/docs/guide/errors.md +3 -3
- package/docs/guide/second-factor.md +31 -4
- package/docs/guide/sign-in-code.md +489 -0
- package/docs/guide/users.md +5 -4
- package/docs/guide/vocabulary.md +7 -6
- package/docs/roadmap.md +28 -13
- package/docs/troubleshooting.md +193 -28
- package/package.json +1 -1
package/docs/troubleshooting.md
CHANGED
|
@@ -74,6 +74,13 @@ How the messages are shaped:
|
|
|
74
74
|
- [`<call>: the <type> type does not sign in with a password, so it has no second factor`](#call-the-type-type-does-not-sign-in-with-a-password-so-it-has-no-second-factor)
|
|
75
75
|
- `TOKEN_*`, `USER_INACTIVE` and `VERSION_CONFLICT` from `secondFactor.confirm`: in their entries above.
|
|
76
76
|
|
|
77
|
+
**Sign-in codes**
|
|
78
|
+
- [`TOKEN_STALE` — `<call>: the code was sent to an e-mail the user no longer has`](#token_stale--call-the-code-was-sent-to-an-e-mail-the-user-no-longer-has)
|
|
79
|
+
- [The code from an earlier e-mail is refused with `CODE_INVALID`](#the-code-from-an-earlier-e-mail-is-refused-with-code_invalid)
|
|
80
|
+
- [`signInCode.request` answers `null` for a user who exists](#signincoderequest-answers-null-for-a-user-who-exists)
|
|
81
|
+
- [`TS2339: Property 'signInCode' does not exist on type 'TypeApi<…>'.`](#ts2339-property-signincode-does-not-exist-on-type-typeapi)
|
|
82
|
+
- `CODE_INVALID`, `TOKEN_UNKNOWN`, `TOKEN_SPENT`, `TOKEN_EXPIRED`, `USER_INACTIVE` and `VERSION_CONFLICT` from `signInCode.confirm`, and `TS2339` on its `token`: in their entries above.
|
|
83
|
+
|
|
77
84
|
**Permissions**
|
|
78
85
|
- [`PERMISSION_DEPTH` — `can: checking <type>#<permission> crossed more than <n> relations without an answer`](#permission_depth--can-checking-typepermission-crossed-more-than-n-relations-without-an-answer)
|
|
79
86
|
- [`permissions: this model was not made by defineModel() …`](#permissions-this-model-was-not-made-by-definemodel--pass-what-definemodel-answered)
|
|
@@ -225,9 +232,11 @@ Also: `janus: store.<slot> is missing`, `janus: store must be an object with use
|
|
|
225
232
|
|
|
226
233
|
**When:** `janus({...})`, from JavaScript or with a store typed loosely. TypeScript refuses a partial store at compile time and names the method.
|
|
227
234
|
**Why:** `store` is `{ users, sessions, tokens }`, and each slot must answer every method of the port. `deleteExpiredSessions` is the one optional method: absent, or a function.
|
|
228
|
-
An adapter written
|
|
229
|
-
`store.tokens has no method countAttempt`
|
|
230
|
-
|
|
235
|
+
An adapter written for an earlier `@nxgt/janus` reports the method a later
|
|
236
|
+
release added to the port: `store.tokens has no method countAttempt` for one
|
|
237
|
+
written against 0.3 (the method came in 0.4), and
|
|
238
|
+
`store.tokens has no method spendUserTokens` for one written against 0.6 (the
|
|
239
|
+
method came in 0.7).
|
|
231
240
|
|
|
232
241
|
**Fix:** pass the three stores, whole:
|
|
233
242
|
|
|
@@ -237,17 +246,17 @@ import { createMemoryStores, janus } from '@nxgt/janus';
|
|
|
237
246
|
janus({ ..., store: createMemoryStores() });
|
|
238
247
|
```
|
|
239
248
|
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
`@nxgt/janus-redis` 0.2:
|
|
249
|
+
Upgrade the published adapter to the release that implements both —
|
|
250
|
+
`@nxgt/janus-drizzle` 0.3, `@nxgt/janus-mongo` 0.4, `@nxgt/janus-redis` 0.3:
|
|
243
251
|
|
|
244
252
|
```bash
|
|
245
|
-
bun add @nxgt/janus@^0.
|
|
253
|
+
bun add @nxgt/janus@^0.7 @nxgt/janus-drizzle@^0.3 # or @nxgt/janus-mongo@^0.4, @nxgt/janus-redis@^0.3
|
|
246
254
|
```
|
|
247
255
|
|
|
248
|
-
Your own adapter implements
|
|
249
|
-
[`TokenStore.countAttempt`](guide/adapters.md#tokenstorecountattempt)
|
|
250
|
-
|
|
256
|
+
Your own adapter implements them as
|
|
257
|
+
[`TokenStore.countAttempt`](guide/adapters.md#tokenstorecountattempt) and
|
|
258
|
+
[`TokenStore.spendUserTokens`](guide/adapters.md#tokenstorespendusertokens)
|
|
259
|
+
set out, then runs the conformance suite.
|
|
251
260
|
|
|
252
261
|
### `janus: relations must be a relation store — relations.deleteEntity is missing`
|
|
253
262
|
|
|
@@ -291,7 +300,7 @@ Also `janus: "<name>" cannot name a user type — janus() answers a method of th
|
|
|
291
300
|
|
|
292
301
|
### `janus: session.lifespan: "<value>" is not a duration; write a number followed by ms, s, m, h or d — for example "15m" or "720h"`
|
|
293
302
|
|
|
294
|
-
The same for `session.renewAfter`, `tokens.verifyEmail`, `tokens.resetPassword` and `secondFactor.challenge`. Also `<option>: a duration must be above zero` and `<option>: a duration in milliseconds must be a finite number above zero`.
|
|
303
|
+
The same for `session.renewAfter`, `tokens.verifyEmail`, `tokens.resetPassword`, `tokens.signInCode` and `secondFactor.challenge`. Also `<option>: a duration must be above zero` and `<option>: a duration in milliseconds must be a finite number above zero`.
|
|
295
304
|
|
|
296
305
|
**When:** `janus({...})`.
|
|
297
306
|
**Why:** a duration is a number of milliseconds, or a number followed by one unit. `'30 m'` compiles — TypeScript's `${number}` accepts the space — and is refused here.
|
|
@@ -466,7 +475,7 @@ if (error instanceof UserInvalidError) {
|
|
|
466
475
|
Also `changePassword: the current password does not match`.
|
|
467
476
|
|
|
468
477
|
**When:** `signIn`, `changePassword`.
|
|
469
|
-
**Why:** no user holds the login, the user has no password, or the password is wrong — **one code for the three**. `error.reason` (`unknownLogin`, `noPassword`, `wrongPassword`) tells them apart for your logs and your rate limiter. A login holding a NUL character or a lone surrogate is `unknownLogin`: no user can hold one.
|
|
478
|
+
**Why:** no user holds the login, the user has no password, or the password is wrong — **one code for the three**. Also a sign-in that verified a password written over while it ran (`reason: 'wrongPassword'`): its session is revoked, or its challenge spent, before the refusal. `error.reason` (`unknownLogin`, `noPassword`, `wrongPassword`) tells them apart for your logs and your rate limiter. A login holding a NUL character or a lone surrogate is `unknownLogin`: no user can hold one.
|
|
470
479
|
**Fix:** answer 401 with the same body whatever the reason:
|
|
471
480
|
|
|
472
481
|
```ts
|
|
@@ -492,8 +501,8 @@ janus({ ..., hasher: scryptHasher(), verifiers: [bcryptVerifier] }); // a Passwo
|
|
|
492
501
|
|
|
493
502
|
### `USER_INACTIVE` — `<call>: the user is inactive`
|
|
494
503
|
|
|
495
|
-
**When:** `signIn`, with the **right** password, for a user set inactive. Also `secondFactor.confirm`, for a user set inactive after `signIn` asked for a code.
|
|
496
|
-
**Why:** an inactive user keeps their record and password, and every sign-in is refused. It is checked after the password, so only somebody who knows the password learns the user is inactive. On `
|
|
504
|
+
**When:** `signIn`, with the **right** password, for a user set inactive. Also `secondFactor.confirm`, for a user set inactive after `signIn` asked for a code, and `signInCode.confirm`, for a user set inactive after the code was sent.
|
|
505
|
+
**Why:** an inactive user keeps their record and password, and every sign-in is refused. It is checked after the password, so only somebody who knows the password learns the user is inactive — and after the code, so only somebody who read the e-mail does: `signInCode.request` answers `null` for an inactive user, as for nobody. On either `confirm`, the challenge is spent: reactivating the user does not revive it.
|
|
497
506
|
**Fix:** answer 403, or reactivate, then sign in again: `await auth.setActive(user, true)`.
|
|
498
507
|
|
|
499
508
|
### `TOKEN_UNKNOWN`, `TOKEN_SPENT`, `TOKEN_EXPIRED`
|
|
@@ -515,10 +524,14 @@ answered: `secondFactor.confirm: no such challenge`,
|
|
|
515
524
|
|
|
516
525
|
**When:** `secondFactor.confirm(challenge, code)`.
|
|
517
526
|
**Why:** a challenge lives five minutes and takes five codes. It is spent by
|
|
518
|
-
the code that opens the session, by the fifth wrong code,
|
|
519
|
-
that ends it (`USER_INACTIVE`, `SECOND_FACTOR_NOT_ENROLLED`)
|
|
520
|
-
|
|
521
|
-
|
|
527
|
+
the code that opens the session, by the fifth wrong code, by a refusal
|
|
528
|
+
that ends it (`USER_INACTIVE`, `SECOND_FACTOR_NOT_ENROLLED`), and by a
|
|
529
|
+
password written — `resetPassword.confirm`, `setPassword`, `changePassword` —
|
|
530
|
+
which ends every sign-in left waiting on its code.
|
|
531
|
+
`TOKEN_UNKNOWN` also covers a challenge whose user was deleted, one
|
|
532
|
+
confirmed through another user type's `secondFactor`, and a challenge passed
|
|
533
|
+
where a code was expected — the two arguments swapped. Another type's
|
|
534
|
+
`confirm` still costs an attempt, and the fifth spends the challenge.
|
|
522
535
|
**Fix:** answer 400 and send the visitor back to sign in, which asks for a new
|
|
523
536
|
code. To give slower visitors more time:
|
|
524
537
|
|
|
@@ -526,10 +539,37 @@ code. To give slower visitors more time:
|
|
|
526
539
|
janus({ ..., secondFactor: { issuer: 'Acme', keys, challenge: '10m' } });
|
|
527
540
|
```
|
|
528
541
|
|
|
542
|
+
**On `signInCode.confirm`**, the messages name the challenge
|
|
543
|
+
`signInCode.request` answered: `signInCode.confirm: no such challenge`,
|
|
544
|
+
`signInCode.confirm: the challenge was already used`,
|
|
545
|
+
`signInCode.confirm: the challenge has expired`.
|
|
546
|
+
|
|
547
|
+
**When:** `signInCode.confirm(challenge, code)`.
|
|
548
|
+
**Why:** a challenge lives ten minutes and takes five codes. It is spent by
|
|
549
|
+
the code that signs the user in, by the fifth wrong code, by a refusal
|
|
550
|
+
that ends it (`TOKEN_STALE`, `USER_INACTIVE`), and by the next `request` for
|
|
551
|
+
the same user: only the last code sent works, so a visitor who asked twice
|
|
552
|
+
and typed the first code gets `TOKEN_SPENT` — and when two requests race,
|
|
553
|
+
even the last code can be spent: at most one survives, sometimes none. `TOKEN_UNKNOWN` also covers a
|
|
554
|
+
challenge whose user was deleted, one confirmed through another user type's
|
|
555
|
+
`signInCode`, the decoy challenge of a `request` that answered `null`, and
|
|
556
|
+
the two arguments swapped. Another type's `confirm` compares no code, but it
|
|
557
|
+
has already cost one of the challenge's five
|
|
558
|
+
attempts: the attempt is counted before the type is known. The challenge is
|
|
559
|
+
left for its own type, with one attempt fewer — and the fifth such call
|
|
560
|
+
spends it, as a fifth wrong code would.
|
|
561
|
+
**Fix:** answer 400 and offer to send a new code — and tell the visitor to
|
|
562
|
+
use the latest e-mail. To give slower inboxes
|
|
563
|
+
more time:
|
|
564
|
+
|
|
565
|
+
```ts
|
|
566
|
+
janus({ ..., tokens: { signInCode: '15m' } });
|
|
567
|
+
```
|
|
568
|
+
|
|
529
569
|
### `TOKEN_STALE` — `<call>: the token was sent to an e-mail the user no longer has`
|
|
530
570
|
|
|
531
|
-
**When:** `verifyEmail.confirm` or `resetPassword.confirm`, after the user changed their e-mail.
|
|
532
|
-
**Why:** confirming it would verify an address nobody holds any more. The token is spent.
|
|
571
|
+
**When:** `verifyEmail.confirm` or `resetPassword.confirm`, after the user changed their e-mail — even while the link was being redeemed: the address is checked again on the very record the write replaces.
|
|
572
|
+
**Why:** confirming it would verify an address nobody holds any more, or reset a password through one. The token is spent, and nothing is written.
|
|
533
573
|
**Fix:** send a new token to the current address: `await auth.verifyEmail.send(user)`.
|
|
534
574
|
|
|
535
575
|
### `INVALID_CURSOR` — `<call>: this cursor was not minted by this store, or was minted for another ordering (<n> characters)`
|
|
@@ -582,7 +622,8 @@ const page = await access.list(user, 'view', 'record', {
|
|
|
582
622
|
|
|
583
623
|
## Second factor
|
|
584
624
|
|
|
585
|
-
Every entry here needs `janus({ ..., secondFactor })
|
|
625
|
+
Every entry here needs `janus({ ..., secondFactor })`, but for the
|
|
626
|
+
`signInCode.confirm` paragraph of `CODE_INVALID`. The messages start with
|
|
586
627
|
`secondFactor.enroll`, `secondFactor.activate`, `secondFactor.confirm` or
|
|
587
628
|
`signIn` — prefixed by the type with several user types:
|
|
588
629
|
`staff.secondFactor.confirm: …`. `secondFactor.confirm` also rejects with
|
|
@@ -595,8 +636,8 @@ each of those entries has a paragraph for it.
|
|
|
595
636
|
|
|
596
637
|
The same for `session` and `user`.
|
|
597
638
|
|
|
598
|
-
**When:** `tsc`, wherever `signIn`'s answer is read, once `janus()` is given a `secondFactor
|
|
599
|
-
**Why:** `signIn` then answers one of two shapes: `{ status: 'signedIn', user, session, token }`, or `{ status: 'secondFactor', challenge, expiresAt }` for a user whose second factor is active — the password alone opens no session for them. Without `secondFactor`, `signIn` still answers a session.
|
|
639
|
+
**When:** `tsc`, wherever `signIn`'s answer is read, once `janus()` is given a `secondFactor` — and `signInCode.confirm`'s, on a user type with a password.
|
|
640
|
+
**Why:** `signIn` then answers one of two shapes: `{ status: 'signedIn', user, session, token }`, or `{ status: 'secondFactor', challenge, expiresAt, userId }` for a user whose second factor is active — the password alone opens no session for them. Without `secondFactor`, `signIn` still answers a session.
|
|
600
641
|
**Fix:** switch on `status`:
|
|
601
642
|
|
|
602
643
|
```ts
|
|
@@ -613,11 +654,25 @@ const signedIn = await auth.secondFactor.confirm(challenge, code); // { status:
|
|
|
613
654
|
|
|
614
655
|
### `CODE_INVALID` — `<call>: the code does not match, or was already used`
|
|
615
656
|
|
|
616
|
-
`TokenError
|
|
657
|
+
`TokenError`. The same message answers three calls; there is a paragraph
|
|
658
|
+
for each below.
|
|
659
|
+
|
|
660
|
+
**When:** `secondFactor.activate`, `secondFactor.confirm` or
|
|
661
|
+
`signInCode.confirm`, with a code that does not match.
|
|
662
|
+
|
|
663
|
+
**`secondFactor.activate(user, code)`**
|
|
664
|
+
**Why:** the code is wrong, or it was accepted before: a code is accepted
|
|
665
|
+
once. There is no challenge here: no `attemptsLeft`, and nothing is spent.
|
|
666
|
+
**Fix:** let the user try the next code the app shows. If it is the one on
|
|
667
|
+
screen, see [the next entry](#the-code-the-authenticator-app-shows-is-refused-with-code_invalid).
|
|
617
668
|
|
|
618
|
-
|
|
619
|
-
**Why:** the code is wrong, or it was accepted before
|
|
620
|
-
|
|
669
|
+
**`secondFactor.confirm(challenge, code)`**
|
|
670
|
+
**Why:** the code is wrong, or it was accepted before. Every call costs one of
|
|
671
|
+
the challenge's five attempts, counted before anything is checked. The error
|
|
672
|
+
carries `attemptsLeft`, what remains; at `0`, the challenge is spent and the
|
|
673
|
+
next `confirm` is `TOKEN_SPENT`.
|
|
674
|
+
**Fix:** answer 401 with `attemptsLeft`, and send the visitor back to sign in
|
|
675
|
+
once it is `0`:
|
|
621
676
|
|
|
622
677
|
```ts
|
|
623
678
|
import { TokenError } from '@nxgt/janus';
|
|
@@ -632,7 +687,34 @@ try {
|
|
|
632
687
|
}
|
|
633
688
|
```
|
|
634
689
|
|
|
635
|
-
|
|
690
|
+
**`signInCode.confirm(challenge, code)`** — the e-mailed code.
|
|
691
|
+
**Why:** the code is not the one sent with this challenge, or is not six
|
|
692
|
+
digits — a space, a dash, a code pasted with its label. The error carries
|
|
693
|
+
`attemptsLeft` and the `userId` of the user the code was sent to. Every call
|
|
694
|
+
costs one of the challenge's five attempts, counted before the code is
|
|
695
|
+
compared — a call through another user type's `signInCode` too, although it
|
|
696
|
+
answers `TOKEN_UNKNOWN`. At `0` the challenge is spent, and the next
|
|
697
|
+
`confirm` is `TOKEN_SPENT`, even with the right code. Codes sent at once past
|
|
698
|
+
the fifth attempt are all refused, the right one included.
|
|
699
|
+
**Fix:** answer 401 with `attemptsLeft`, and request a new code once it is
|
|
700
|
+
`0`. Strip what the visitor may have typed around the digits before calling
|
|
701
|
+
`confirm`:
|
|
702
|
+
|
|
703
|
+
```ts
|
|
704
|
+
import { TokenError } from '@nxgt/janus';
|
|
705
|
+
|
|
706
|
+
try {
|
|
707
|
+
return await auth.signInCode.confirm(challenge, code.replace(/\D/g, ''));
|
|
708
|
+
} catch (error) {
|
|
709
|
+
if (error instanceof TokenError && error.code === 'CODE_INVALID') {
|
|
710
|
+
return Response.json({ code: error.code, attemptsLeft: error.attemptsLeft }, { status: 401 });
|
|
711
|
+
}
|
|
712
|
+
throw error;
|
|
713
|
+
}
|
|
714
|
+
```
|
|
715
|
+
|
|
716
|
+
If the visitor typed the code from the e-mail correctly, see
|
|
717
|
+
[the code from an earlier e-mail](#the-code-from-an-earlier-e-mail-is-refused-with-code_invalid).
|
|
636
718
|
|
|
637
719
|
### The code the authenticator app shows is refused with `CODE_INVALID`
|
|
638
720
|
|
|
@@ -741,6 +823,89 @@ A `TypeError`.
|
|
|
741
823
|
|
|
742
824
|
---
|
|
743
825
|
|
|
826
|
+
## Sign-in codes
|
|
827
|
+
|
|
828
|
+
The messages start with `signInCode.confirm` — prefixed by the type with
|
|
829
|
+
several user types: `patient.signInCode.confirm: …`. `signInCode.request`
|
|
830
|
+
throws nothing but `STORE_FAILED`: an e-mail it cannot sign in is `null`.
|
|
831
|
+
`signInCode.confirm` also rejects with
|
|
832
|
+
[`CODE_INVALID`](#code_invalid--call-the-code-does-not-match-or-was-already-used),
|
|
833
|
+
[`TOKEN_UNKNOWN`, `TOKEN_SPENT`, `TOKEN_EXPIRED`](#token_unknown-token_spent-token_expired),
|
|
834
|
+
[`USER_INACTIVE`](#user_inactive--call-the-user-is-inactive) and
|
|
835
|
+
[`VERSION_CONFLICT`](#version_conflict--call-expected-version-n-found-m);
|
|
836
|
+
each of those entries has a paragraph for it.
|
|
837
|
+
[The sign-in code guide](guide/sign-in-code.md) has the whole flow.
|
|
838
|
+
|
|
839
|
+
### `TOKEN_STALE` — `<call>: the code was sent to an e-mail the user no longer has`
|
|
840
|
+
|
|
841
|
+
`TokenError`, carrying the `userId`.
|
|
842
|
+
|
|
843
|
+
**When:** `signInCode.confirm`, after the user's e-mail was changed — by
|
|
844
|
+
`update`, or a patch naming the e-mail field — since the code was sent.
|
|
845
|
+
**Why:** the code proves an address, and signing in with it would mark as
|
|
846
|
+
verified an address the user no longer holds. The challenge is spent.
|
|
847
|
+
**Fix:** answer 400, and request a new code: it goes to the current address.
|
|
848
|
+
|
|
849
|
+
```ts
|
|
850
|
+
const issued = await auth.signInCode.request(currentEmail);
|
|
851
|
+
```
|
|
852
|
+
|
|
853
|
+
### The code from an earlier e-mail is refused with `CODE_INVALID`
|
|
854
|
+
|
|
855
|
+
**When:** the visitor asked for a code twice — pressed "send again", or
|
|
856
|
+
opened the form in two tabs — and typed the code of the first e-mail.
|
|
857
|
+
**Why:** every `request` issues a new code **with its own challenge**, and a
|
|
858
|
+
code is checked against the challenge it was sent with. The cookie or the
|
|
859
|
+
form field now holds the second challenge, so the first code does not
|
|
860
|
+
match it — and costs an attempt.
|
|
861
|
+
**Fix:** tell the visitor that only the last code sent works, and put the
|
|
862
|
+
time it was sent in the e-mail's subject or text so they can tell the
|
|
863
|
+
e-mails apart. The earlier challenge is spent by the new `request`: the
|
|
864
|
+
first code, even with its own challenge, answers `TOKEN_SPENT`.
|
|
865
|
+
|
|
866
|
+
### `signInCode.request` answers `null` for a user who exists
|
|
867
|
+
|
|
868
|
+
**When:** `signInCode.request(email)`, for an address you can see in the
|
|
869
|
+
database.
|
|
870
|
+
**Why**, in the order to check:
|
|
871
|
+
|
|
872
|
+
1. **The user is inactive.** An inactive user gets no code, and the answer
|
|
873
|
+
is the same as for nobody.
|
|
874
|
+
2. **It is another user type.** `clinic.patient.signInCode.request` looks
|
|
875
|
+
among patients only; the same address may hold a user of another type.
|
|
876
|
+
3. **The address is not the type's e-mail field.** A type whose `email`
|
|
877
|
+
option names `contact` is looked up by `contact`. A login that looks like
|
|
878
|
+
an e-mail — a `username` of `ada@example.com` — is not an e-mail, and is
|
|
879
|
+
`null` too.
|
|
880
|
+
|
|
881
|
+
The e-mail is trimmed and lowercased before the lookup, so case and
|
|
882
|
+
surrounding spaces are never the cause.
|
|
883
|
+
**Fix:** `setActive(user, true)`; call the right type's `signInCode`; name
|
|
884
|
+
the field with `email: 'contact'`. Do not tell the visitor which case it
|
|
885
|
+
was — the route answers the same page either way.
|
|
886
|
+
|
|
887
|
+
### `TS2339: Property 'signInCode' does not exist on type 'TypeApi<…>'.`
|
|
888
|
+
|
|
889
|
+
Also `Property 'signInCode' does not exist on type 'Janus<…>'.`, with the
|
|
890
|
+
single-type form, `janus({ user })`.
|
|
891
|
+
|
|
892
|
+
**When:** `tsc`, on `auth.signInCode` or `clinic.<type>.signInCode`.
|
|
893
|
+
**Why:** the user type has no e-mail: no field called `email`, and no
|
|
894
|
+
`email` option naming one. A code has nowhere to be sent, so the flow is
|
|
895
|
+
absent from the type.
|
|
896
|
+
**Fix:** name the field that holds the e-mail:
|
|
897
|
+
|
|
898
|
+
```ts
|
|
899
|
+
janus({
|
|
900
|
+
users: {
|
|
901
|
+
staff: { schema: Staff, password: { login: 'username' }, email: 'workEmail' },
|
|
902
|
+
},
|
|
903
|
+
...
|
|
904
|
+
});
|
|
905
|
+
```
|
|
906
|
+
|
|
907
|
+
---
|
|
908
|
+
|
|
744
909
|
## Permissions
|
|
745
910
|
|
|
746
911
|
### `PERMISSION_DEPTH` — `can: checking <type>#<permission> crossed more than <n> relations without an answer`
|