@arthur.eudeline/payload-plugin-mfa 0.1.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.
Files changed (72) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +124 -0
  3. package/dist/backup-codes.d.ts +28 -0
  4. package/dist/backup-codes.d.ts.map +1 -0
  5. package/dist/backup-codes.js +66 -0
  6. package/dist/backup-codes.js.map +1 -0
  7. package/dist/coded-error.d.ts +24 -0
  8. package/dist/coded-error.d.ts.map +1 -0
  9. package/dist/coded-error.js +33 -0
  10. package/dist/coded-error.js.map +1 -0
  11. package/dist/components/login-form.d.ts +22 -0
  12. package/dist/components/login-form.d.ts.map +1 -0
  13. package/dist/components/login-form.js +91 -0
  14. package/dist/components/login-form.js.map +1 -0
  15. package/dist/components/login-view.d.ts +15 -0
  16. package/dist/components/login-view.d.ts.map +1 -0
  17. package/dist/components/login-view.js +43 -0
  18. package/dist/components/login-view.js.map +1 -0
  19. package/dist/components/mfa-panel.d.ts +16 -0
  20. package/dist/components/mfa-panel.d.ts.map +1 -0
  21. package/dist/components/mfa-panel.js +94 -0
  22. package/dist/components/mfa-panel.js.map +1 -0
  23. package/dist/components/mfa.css +92 -0
  24. package/dist/endpoints.d.ts +4 -0
  25. package/dist/endpoints.d.ts.map +1 -0
  26. package/dist/endpoints.js +190 -0
  27. package/dist/endpoints.js.map +1 -0
  28. package/dist/errors.d.ts +13 -0
  29. package/dist/errors.d.ts.map +1 -0
  30. package/dist/errors.js +12 -0
  31. package/dist/errors.js.map +1 -0
  32. package/dist/exports/client.d.ts +5 -0
  33. package/dist/exports/client.d.ts.map +1 -0
  34. package/dist/exports/client.js +5 -0
  35. package/dist/exports/client.js.map +1 -0
  36. package/dist/exports/rsc.d.ts +2 -0
  37. package/dist/exports/rsc.d.ts.map +1 -0
  38. package/dist/exports/rsc.js +2 -0
  39. package/dist/exports/rsc.js.map +1 -0
  40. package/dist/fields.d.ts +4 -0
  41. package/dist/fields.d.ts.map +1 -0
  42. package/dist/fields.js +60 -0
  43. package/dist/fields.js.map +1 -0
  44. package/dist/hooks.d.ts +21 -0
  45. package/dist/hooks.d.ts.map +1 -0
  46. package/dist/hooks.js +100 -0
  47. package/dist/hooks.js.map +1 -0
  48. package/dist/i18n.d.ts +49 -0
  49. package/dist/i18n.d.ts.map +1 -0
  50. package/dist/i18n.js +70 -0
  51. package/dist/i18n.js.map +1 -0
  52. package/dist/index.d.ts +19 -0
  53. package/dist/index.d.ts.map +1 -0
  54. package/dist/index.js +86 -0
  55. package/dist/index.js.map +1 -0
  56. package/dist/reset.d.ts +23 -0
  57. package/dist/reset.d.ts.map +1 -0
  58. package/dist/reset.js +32 -0
  59. package/dist/reset.js.map +1 -0
  60. package/dist/state.d.ts +39 -0
  61. package/dist/state.d.ts.map +1 -0
  62. package/dist/state.js +104 -0
  63. package/dist/state.js.map +1 -0
  64. package/dist/totp.d.ts +38 -0
  65. package/dist/totp.d.ts.map +1 -0
  66. package/dist/totp.js +49 -0
  67. package/dist/totp.js.map +1 -0
  68. package/dist/types.d.ts +60 -0
  69. package/dist/types.d.ts.map +1 -0
  70. package/dist/types.js +2 -0
  71. package/dist/types.js.map +1 -0
  72. package/package.json +81 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Arthur Eudeline
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,124 @@
1
+ # @arthur-eudeline/payload-plugin-mfa
2
+
3
+ Two-factor authentication for the [Payload CMS](https://payloadcms.com) 3 admin panel: TOTP
4
+ (Google Authenticator, 1Password, Aegis…) plus one-time backup codes. Payload 3 ships nothing for
5
+ this — the plugin adds the fields, the endpoints, the login step and the enrolment UI.
6
+
7
+ ```bash
8
+ pnpm add @arthur-eudeline/payload-plugin-mfa
9
+ ```
10
+
11
+ ```ts
12
+ // payload.config.ts
13
+ import { mfaPlugin } from '@arthur-eudeline/payload-plugin-mfa';
14
+
15
+ export default buildConfig({
16
+ admin: { user: 'users' },
17
+ collections: [Users /* … */],
18
+ plugins: [mfaPlugin({ issuer: 'Acme' })],
19
+ });
20
+ ```
21
+
22
+ Then, once:
23
+
24
+ ```bash
25
+ pnpm payload generate:types
26
+ pnpm payload generate:importmap
27
+ pnpm payload migrate:create # the plugin adds columns
28
+ ```
29
+
30
+ That is the whole setup. Users enable 2FA themselves from `/admin/account`; the next sign-in asks
31
+ for a code.
32
+
33
+ ## What it does
34
+
35
+ - **Enrolment** — a panel on the user's own account renders a QR code, confirms the secret with a
36
+ first valid code, then shows ten single-use backup codes once.
37
+ - **Sign-in** — `/admin/login` is replaced by a two-step form: email + password first, then the
38
+ code, only for accounts that have 2FA on. Backup codes are accepted in the same field.
39
+ - **Recovery** — `resetMfa()` for the one dead end the UI can't fix (phone _and_ backup codes lost).
40
+
41
+ ## Security properties
42
+
43
+ - The TOTP secret is encrypted at rest with `payload.encrypt` (keyed on `PAYLOAD_SECRET`); backup
44
+ codes are stored as per-code salted scrypt hashes, never in clear.
45
+ - Every MFA field denies `create`/`update` through the API and the secrets also deny `read`. The
46
+ only write path is the plugin's own endpoints, which use `overrideAccess`. A `PATCH` on
47
+ `/api/users/:id` from a stolen session can neither disable 2FA nor plant a chosen secret.
48
+ - The last accepted TOTP timestep is recorded, so an intercepted code cannot be replayed within its
49
+ ±30 s validity window.
50
+ - Five consecutive invalid codes lock the second factor for 15 minutes (both configurable). Payload's
51
+ own `maxLoginAttempts` does not cover this: a bad code rolls the login transaction back, taking
52
+ the attempt counter with it — which is why the counter is written from an `afterError` hook.
53
+ - Every endpoint acts on `req.user` only. No account can enrol, unlock or disable another.
54
+ - Disabling 2FA requires a valid TOTP code rather than the password: verifying a password outside
55
+ the login flow would mean reimplementing Payload's internal hashing (`authenticateLocalStrategy`
56
+ is not exported), and `payload.login()` would mint a stray session. Re-asking for the second
57
+ factor is the guarantee that matters here — it blocks a stolen session, which is what 2FA is for.
58
+
59
+ ## Options
60
+
61
+ | Option | Default | |
62
+ | ------------------- | --------------------------------------- | ---------------------------------------------------------------------------------------------------------------------- |
63
+ | `collections` | `[config.admin.user]` | Auth collections to protect. |
64
+ | `issuer` | `admin.meta.titleSuffix` ?? `'Payload'` | Name shown in the authenticator app. |
65
+ | `enabled` | `true` | `false` neutralises the behaviour but **keeps the fields**, so an already-migrated database doesn't lose its columns. |
66
+ | `fieldName` | `'mfa'` | Name of the field group added to the collection. |
67
+ | `backupCodeCount` | `10` | Backup codes issued on activation. |
68
+ | `maxAttempts` | `5` | Consecutive invalid codes before locking. |
69
+ | `lockDuration` | `900000` | Lock duration, in milliseconds. |
70
+ | `overrideLoginView` | `true` | Set `false` to keep your own login view — then mount `MfaLoginForm` yourself, or handle the `MFA_REQUIRED` error code. |
71
+ | `labels` | — | Per-language label overrides: `{ fr: { panelTitle: '…' } }`. |
72
+ | `language` | admin language | Pins the label language instead of following the admin's. |
73
+
74
+ English and French are built in; any other admin language falls back to English.
75
+
76
+ ## HTTP surface
77
+
78
+ All five are mounted on the protected collection and act on the caller only.
79
+
80
+ | | |
81
+ | ----------------------------------------- | ---------------------------------------------------------------------------------------------------------- |
82
+ | `GET /api/<collection>/mfa/status` | `{ enabled, confirmedAt, backupCodesRemaining, hasPendingSecret }` |
83
+ | `POST /api/<collection>/mfa/setup` | → `{ secret, qrCode }` (data URL). Stores a _pending_ secret; sign-in is unaffected until it is confirmed. |
84
+ | `POST /api/<collection>/mfa/activate` | `{ code }` → `{ backupCodes }`. The only time backup codes are readable. |
85
+ | `POST /api/<collection>/mfa/backup-codes` | `{ code }` → `{ backupCodes }`. Invalidates the previous set. |
86
+ | `POST /api/<collection>/mfa/disable` | `{ code }` → `{ enabled: false }` |
87
+
88
+ On sign-in, `POST /api/<collection>/login` accepts an extra `mfaCode` field. Without it, an enrolled
89
+ account gets `401` with `errors[0].data.code === 'MFA_REQUIRED'`; a wrong code gives `MFA_INVALID`,
90
+ and a locked account `MFA_LOCKED` (`429`). Those constants are exported as `MFA_ERROR_CODES`.
91
+
92
+ > **Note for plugin authors.** The error codes above are delivered through `formatErrors`'s
93
+ > `Array.isArray(message)` branch rather than an `APIError`'s `data` field. Payload only forwards
94
+ > `data` for an error it recognises with `instanceof APIError`, and that check fails in a Next.js
95
+ > production build — `withPayload` externalises `payload` only in development, so the bundled copy
96
+ > the plugin throws from is not the one `formatErrors` compares against. Going through the array
97
+ > branch is identity-independent and behaves the same in dev and in production.
98
+
99
+ ## Recovery
100
+
101
+ ```ts
102
+ import { resetMfa } from '@arthur-eudeline/payload-plugin-mfa';
103
+
104
+ await resetMfa({ payload, email: 'someone@example.com' });
105
+ ```
106
+
107
+ Server-side and privileged — call it from an admin script, never from an exposed route.
108
+
109
+ ## Exports
110
+
111
+ | | |
112
+ | -------------------------------------------- | ------------------------------------------------------------------ |
113
+ | `@arthur-eudeline/payload-plugin-mfa` | `mfaPlugin`, `resetMfa`, `MFA_ERROR_CODES`, `builtInLabels`, types |
114
+ | `@arthur-eudeline/payload-plugin-mfa/client` | `MfaPanel`, `MfaLoginForm`, `resolveLabels` |
115
+ | `@arthur-eudeline/payload-plugin-mfa/rsc` | `MfaLoginView` |
116
+
117
+ ## Requirements
118
+
119
+ Payload 3.88+, React 19, Next 15 or 16. The database adapter must support transactions (the
120
+ Postgres, SQLite and MongoDB adapters all do).
121
+
122
+ ## License
123
+
124
+ MIT
@@ -0,0 +1,28 @@
1
+ export declare const DEFAULT_BACKUP_CODE_COUNT = 10;
2
+ export type StoredBackupCode = {
3
+ hash: string;
4
+ salt: string;
5
+ usedAt: null | string;
6
+ };
7
+ /** Tolère la casse et les séparateurs, pour que « abcd-efgh » et « ABCDEFGH » soient équivalents. */
8
+ export declare function normalizeBackupCode(code: string): string;
9
+ /**
10
+ * Retourne les codes en clair — à afficher une seule fois — et leur version stockée en base.
11
+ * Les codes en clair ne sont jamais persistés.
12
+ */
13
+ export declare function generateBackupCodes(count?: number): {
14
+ plain: string[];
15
+ stored: StoredBackupCode[];
16
+ };
17
+ /**
18
+ * Cherche `candidate` parmi les codes non encore utilisés et le marque comme consommé.
19
+ *
20
+ * La boucle parcourt toujours l'intégralité des codes (pas de sortie anticipée) pour ne pas laisser
21
+ * fuiter, par le temps de réponse, la position du code dans la liste.
22
+ */
23
+ export declare function consumeBackupCode(codes: StoredBackupCode[], candidate: string): {
24
+ codes: StoredBackupCode[];
25
+ matched: boolean;
26
+ };
27
+ export declare function countRemainingBackupCodes(codes: StoredBackupCode[]): number;
28
+ //# sourceMappingURL=backup-codes.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backup-codes.d.ts","sourceRoot":"","sources":["../src/backup-codes.ts"],"names":[],"mappings":"AAOA,eAAO,MAAM,yBAAyB,KAAK,CAAC;AAE5C,MAAM,MAAM,gBAAgB,GAAG;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,IAAI,GAAG,MAAM,CAAC;CACvB,CAAC;AAaF,qGAAqG;AACrG,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,CAExD;AAMD;;;GAGG;AACH,wBAAgB,mBAAmB,CAAC,KAAK,SAA4B,GAAG;IACtE,KAAK,EAAE,MAAM,EAAE,CAAC;IAChB,MAAM,EAAE,gBAAgB,EAAE,CAAC;CAC5B,CAWA;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,gBAAgB,EAAE,EACzB,SAAS,EAAE,MAAM,GAChB;IAAE,KAAK,EAAE,gBAAgB,EAAE,CAAC;IAAC,OAAO,EAAE,OAAO,CAAA;CAAE,CAqBjD;AAED,wBAAgB,yBAAyB,CAAC,KAAK,EAAE,gBAAgB,EAAE,GAAG,MAAM,CAE3E"}
@@ -0,0 +1,66 @@
1
+ import { randomBytes, scryptSync, timingSafeEqual } from 'crypto';
2
+ /** 32 caractères : A-Z sans I ni L, et 2-9. Ni 0/O ni 1/l ne peuvent être confondus. */
3
+ const ALPHABET = 'ABCDEFGHJKMNOPQRSTUVWXYZ23456789';
4
+ const GROUP_LENGTH = 4;
5
+ const GROUPS_PER_CODE = 2;
6
+ export const DEFAULT_BACKUP_CODE_COUNT = 10;
7
+ /**
8
+ * `ALPHABET` fait exactement 32 caractères : `256 % 32 === 0`, donc le modulo ne biaise pas la
9
+ * distribution des octets aléatoires. Toute modification de sa longueur casserait cette propriété.
10
+ */
11
+ function randomChars(length) {
12
+ const bytes = randomBytes(length);
13
+ let out = '';
14
+ for (let i = 0; i < length; i++)
15
+ out += ALPHABET[bytes[i] % ALPHABET.length];
16
+ return out;
17
+ }
18
+ /** Tolère la casse et les séparateurs, pour que « abcd-efgh » et « ABCDEFGH » soient équivalents. */
19
+ export function normalizeBackupCode(code) {
20
+ return code.toUpperCase().replace(/[^A-Z0-9]/g, '');
21
+ }
22
+ function hashBackupCode(code, salt) {
23
+ return scryptSync(normalizeBackupCode(code), salt, 32).toString('hex');
24
+ }
25
+ /**
26
+ * Retourne les codes en clair — à afficher une seule fois — et leur version stockée en base.
27
+ * Les codes en clair ne sont jamais persistés.
28
+ */
29
+ export function generateBackupCodes(count = DEFAULT_BACKUP_CODE_COUNT) {
30
+ const plain = Array.from({ length: count }, () => Array.from({ length: GROUPS_PER_CODE }, () => randomChars(GROUP_LENGTH)).join('-'));
31
+ const stored = plain.map((code) => {
32
+ const salt = randomBytes(16).toString('hex');
33
+ return { hash: hashBackupCode(code, salt), salt, usedAt: null };
34
+ });
35
+ return { plain, stored };
36
+ }
37
+ /**
38
+ * Cherche `candidate` parmi les codes non encore utilisés et le marque comme consommé.
39
+ *
40
+ * La boucle parcourt toujours l'intégralité des codes (pas de sortie anticipée) pour ne pas laisser
41
+ * fuiter, par le temps de réponse, la position du code dans la liste.
42
+ */
43
+ export function consumeBackupCode(codes, candidate) {
44
+ const normalized = normalizeBackupCode(candidate);
45
+ if (normalized.length !== GROUP_LENGTH * GROUPS_PER_CODE)
46
+ return { codes, matched: false };
47
+ let matchedIndex = -1;
48
+ codes.forEach((code, index) => {
49
+ if (code.usedAt)
50
+ return;
51
+ const candidateHash = Buffer.from(hashBackupCode(normalized, code.salt), 'hex');
52
+ const storedHash = Buffer.from(code.hash, 'hex');
53
+ if (candidateHash.length !== storedHash.length)
54
+ return;
55
+ if (timingSafeEqual(candidateHash, storedHash) && matchedIndex === -1)
56
+ matchedIndex = index;
57
+ });
58
+ if (matchedIndex === -1)
59
+ return { codes, matched: false };
60
+ const next = codes.map((code, index) => index === matchedIndex ? { ...code, usedAt: new Date().toISOString() } : code);
61
+ return { codes: next, matched: true };
62
+ }
63
+ export function countRemainingBackupCodes(codes) {
64
+ return codes.filter((code) => !code.usedAt).length;
65
+ }
66
+ //# sourceMappingURL=backup-codes.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"backup-codes.js","sourceRoot":"","sources":["../src/backup-codes.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Erreur d'API portant un code exploitable par le formulaire de connexion.
3
+ *
4
+ * Payload transmet bien `data` au client — mais seulement pour une erreur que `formatErrors`
5
+ * reconnaît par `instanceof APIError`. Ce test échoue dès que le module `payload` vu par le plugin
6
+ * et celui vu par `formatErrors` ne sont pas la même instance, ce qui est précisément le cas dans
7
+ * un build de production Next.js : `withPayload` n'ajoute `payload` à `serverExternalPackages` que
8
+ * si `NODE_ENV === 'development'`, donc il est externalisé en dev et bundlé en production.
9
+ *
10
+ * Conséquence observée : en développement le code arrivait, en production `data` disparaissait
11
+ * silencieusement, la réponse ne contenait plus que `message`, et le formulaire — incapable de
12
+ * distinguer « code requis » d'une erreur quelconque — affichait le message au lieu de révéler le
13
+ * champ du second facteur.
14
+ *
15
+ * `formatErrors` a une seconde branche, indépendante de toute identité de classe :
16
+ * `Array.isArray(message)` renvoie le tableau tel quel. On y place directement l'objet d'erreur
17
+ * final. `data` n'est délibérément pas posé sur l'erreur, pour que la branche `instanceof` ne
18
+ * puisse pas prendre le pas et renvoyer un `message` qui serait un tableau.
19
+ *
20
+ * `isPublic` et `status` restent de simples propriétés : `isErrorPublic` et `routeError` les lisent
21
+ * sans passer par `instanceof`, et fonctionnent donc dans les deux cas.
22
+ */
23
+ export declare function codedError(code: string, message: string, status: number): Error;
24
+ //# sourceMappingURL=coded-error.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"coded-error.d.ts","sourceRoot":"","sources":["../src/coded-error.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,wBAAgB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,KAAK,CAU/E"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Erreur d'API portant un code exploitable par le formulaire de connexion.
3
+ *
4
+ * Payload transmet bien `data` au client — mais seulement pour une erreur que `formatErrors`
5
+ * reconnaît par `instanceof APIError`. Ce test échoue dès que le module `payload` vu par le plugin
6
+ * et celui vu par `formatErrors` ne sont pas la même instance, ce qui est précisément le cas dans
7
+ * un build de production Next.js : `withPayload` n'ajoute `payload` à `serverExternalPackages` que
8
+ * si `NODE_ENV === 'development'`, donc il est externalisé en dev et bundlé en production.
9
+ *
10
+ * Conséquence observée : en développement le code arrivait, en production `data` disparaissait
11
+ * silencieusement, la réponse ne contenait plus que `message`, et le formulaire — incapable de
12
+ * distinguer « code requis » d'une erreur quelconque — affichait le message au lieu de révéler le
13
+ * champ du second facteur.
14
+ *
15
+ * `formatErrors` a une seconde branche, indépendante de toute identité de classe :
16
+ * `Array.isArray(message)` renvoie le tableau tel quel. On y place directement l'objet d'erreur
17
+ * final. `data` n'est délibérément pas posé sur l'erreur, pour que la branche `instanceof` ne
18
+ * puisse pas prendre le pas et renvoyer un `message` qui serait un tableau.
19
+ *
20
+ * `isPublic` et `status` restent de simples propriétés : `isErrorPublic` et `routeError` les lisent
21
+ * sans passer par `instanceof`, et fonctionnent donc dans les deux cas.
22
+ */
23
+ export function codedError(code, message, status) {
24
+ const error = new Error(message);
25
+ // Conservé pour que Payload journalise cette erreur comme les siennes (`config.loggingLevels`).
26
+ error.name = 'APIError';
27
+ return Object.assign(error, {
28
+ isPublic: true,
29
+ message: [{ data: { code }, message }],
30
+ status,
31
+ });
32
+ }
33
+ //# sourceMappingURL=coded-error.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"coded-error.js","sourceRoot":"","sources":["../src/coded-error.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AACH,MAAM,UAAU,UAAU,CAAC,IAAY,EAAE,OAAe,EAAE,MAAc;IACtE,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC;IACjC,gGAAgG;IAChG,KAAK,CAAC,IAAI,GAAG,UAAU,CAAC;IAExB,OAAO,MAAM,CAAC,MAAM,CAAC,KAAK,EAAE;QAC1B,QAAQ,EAAE,IAAI;QACd,OAAO,EAAE,CAAC,EAAE,IAAI,EAAE,EAAE,IAAI,EAAE,EAAE,OAAO,EAAE,CAAC;QACtC,MAAM;KACP,CAAC,CAAC;AACL,CAAC"}
@@ -0,0 +1,22 @@
1
+ import React from 'react';
2
+ import { type MfaLabelOverrides } from '../i18n.js';
3
+ import './mfa.css';
4
+ export type MfaLoginFormProps = {
5
+ labels?: MfaLabelOverrides;
6
+ language?: string;
7
+ searchParams?: {
8
+ [key: string]: string | string[] | undefined;
9
+ };
10
+ };
11
+ /**
12
+ * Formulaire de connexion en deux temps.
13
+ *
14
+ * Le premier envoi ne contient qu'email + mot de passe. Si le compte a la double authentification
15
+ * activée, le hook `beforeLogin` répond `MFA_REQUIRED` et le formulaire révèle le champ du code,
16
+ * puis rejoue la requête complète — le mot de passe n'est jamais validé « à moitié » côté serveur,
17
+ * il n'existe pas d'état intermédiaire de session.
18
+ *
19
+ * Il remplace le formulaire natif de Payload, qui ne sait pas envoyer de champ supplémentaire.
20
+ */
21
+ export declare const MfaLoginForm: React.FC<MfaLoginFormProps>;
22
+ //# sourceMappingURL=login-form.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"login-form.d.ts","sourceRoot":"","sources":["../../src/components/login-form.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAsC,MAAM,OAAO,CAAC;AAG3D,OAAO,EAAiB,KAAK,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAEnE,OAAO,WAAW,CAAC;AASnB,MAAM,MAAM,iBAAiB,GAAG;IAC9B,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,YAAY,CAAC,EAAE;QAAE,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAA;KAAE,CAAC;CACjE,CAAC;AAEF;;;;;;;;;GASG;AACH,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CA0KpD,CAAC"}
@@ -0,0 +1,91 @@
1
+ 'use client';
2
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
3
+ import { Button, Link, useAuth, useConfig, useTranslation } from '@payloadcms/ui';
4
+ import { useRouter } from 'next/navigation.js';
5
+ import { formatAdminURL, getSafeRedirect } from 'payload/shared';
6
+ import { useEffect, useRef, useState } from 'react';
7
+ import { MFA_ERROR_CODES } from '../errors.js';
8
+ import { resolveLabels } from '../i18n.js';
9
+ import './mfa.css';
10
+ /** `login__form` porte la mise en forme native des champs ; `mfa-login` la nôtre. */
11
+ const baseClass = 'login__form';
12
+ /**
13
+ * Formulaire de connexion en deux temps.
14
+ *
15
+ * Le premier envoi ne contient qu'email + mot de passe. Si le compte a la double authentification
16
+ * activée, le hook `beforeLogin` répond `MFA_REQUIRED` et le formulaire révèle le champ du code,
17
+ * puis rejoue la requête complète — le mot de passe n'est jamais validé « à moitié » côté serveur,
18
+ * il n'existe pas d'état intermédiaire de session.
19
+ *
20
+ * Il remplace le formulaire natif de Payload, qui ne sait pas envoyer de champ supplémentaire.
21
+ */
22
+ export const MfaLoginForm = ({ labels, language, searchParams }) => {
23
+ const { config } = useConfig();
24
+ const { setUser } = useAuth();
25
+ const { i18n, t } = useTranslation();
26
+ const router = useRouter();
27
+ const { admin: { routes: { forgot: forgotRoute } = {}, user: userSlug }, routes: { admin: adminRoute, api: apiRoute }, } = config;
28
+ const labelsForLanguage = resolveLabels(language ?? i18n.language, labels);
29
+ const [step, setStep] = useState('credentials');
30
+ const [email, setEmail] = useState('');
31
+ const [password, setPassword] = useState('');
32
+ const [mfaCode, setMfaCode] = useState('');
33
+ const [error, setError] = useState(null);
34
+ const [processing, setProcessing] = useState(false);
35
+ const codeRef = useRef(null);
36
+ useEffect(() => {
37
+ if (step === 'mfa')
38
+ codeRef.current?.focus();
39
+ }, [step]);
40
+ const redirectUrl = getSafeRedirect({
41
+ fallbackTo: adminRoute,
42
+ redirectTo: searchParams?.redirect ?? '',
43
+ });
44
+ const handleSubmit = async (event) => {
45
+ event.preventDefault();
46
+ if (processing)
47
+ return;
48
+ setProcessing(true);
49
+ setError(null);
50
+ try {
51
+ const response = await fetch(formatAdminURL({ apiRoute, path: `/${userSlug}/login` }), {
52
+ body: JSON.stringify({
53
+ email,
54
+ password,
55
+ ...(step === 'mfa' ? { mfaCode } : {}),
56
+ }),
57
+ credentials: 'include',
58
+ headers: { 'Content-Type': 'application/json' },
59
+ method: 'POST',
60
+ });
61
+ const json = await response.json();
62
+ if (response.ok) {
63
+ setUser(json);
64
+ router.push(redirectUrl);
65
+ return;
66
+ }
67
+ const failure = json?.errors?.[0] ?? {};
68
+ if (failure.data?.code === MFA_ERROR_CODES.required) {
69
+ setStep('mfa');
70
+ setMfaCode('');
71
+ return;
72
+ }
73
+ if (failure.data?.code === MFA_ERROR_CODES.invalid) {
74
+ setStep('mfa');
75
+ setMfaCode('');
76
+ }
77
+ setError(failure.message || labelsForLanguage.unknownError);
78
+ }
79
+ catch {
80
+ setError(labelsForLanguage.unknownError);
81
+ }
82
+ finally {
83
+ setProcessing(false);
84
+ }
85
+ };
86
+ return (_jsxs("form", { className: `${baseClass} mfa-login`, onSubmit: handleSubmit, children: [_jsxs("div", { className: `${baseClass}__inputWrap`, children: [_jsxs("div", { hidden: step === 'mfa', children: [_jsxs("div", { className: "field-type email", children: [_jsx("label", { className: "field-label", htmlFor: "mfa-login-email", children: t('general:email') }), _jsx("input", { autoComplete: "email", id: "mfa-login-email", name: "email", onChange: (event) => setEmail(event.target.value), required: true, type: "email", value: email })] }), _jsxs("div", { className: "field-type password", children: [_jsx("label", { className: "field-label", htmlFor: "mfa-login-password", children: t('general:password') }), _jsx("input", { autoComplete: "current-password", id: "mfa-login-password", name: "password", onChange: (event) => setPassword(event.target.value), required: true, type: "password", value: password })] })] }), step === 'mfa' && (_jsxs("div", { className: "field-type text", children: [_jsx("label", { className: "field-label", htmlFor: "mfa-login-code", children: labelsForLanguage.loginCodeLabel }), _jsx("input", { autoComplete: "one-time-code", id: "mfa-login-code", inputMode: "text", name: "mfaCode", onChange: (event) => setMfaCode(event.target.value), ref: codeRef, required: true, type: "text", value: mfaCode }), _jsx("p", { className: "field-description", children: labelsForLanguage.loginCodeDescription })] }))] }), error && (_jsx("p", { className: "mfa-login__error", role: "alert", children: error })), step === 'credentials' && forgotRoute && (_jsx(Link, { href: formatAdminURL({ adminRoute, path: forgotRoute }), prefetch: false, children: t('authentication:forgotPasswordQuestion') })), step === 'mfa' && (_jsx("button", { className: "mfa-login__back", onClick: () => {
87
+ setStep('credentials');
88
+ setError(null);
89
+ }, type: "button", children: labelsForLanguage.loginBack })), _jsx("div", { className: "form-submit", children: _jsx(Button, { disabled: processing, size: "large", type: "submit", children: processing ? t('general:loading') : t('authentication:login') }) })] }));
90
+ };
91
+ //# sourceMappingURL=login-form.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"login-form.js","sourceRoot":"","sources":["../../src/components/login-form.tsx"],"names":[],"mappings":"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"}
@@ -0,0 +1,15 @@
1
+ import type { AdminViewServerProps } from 'payload';
2
+ import React from 'react';
3
+ import type { MfaLabelOverrides } from '../i18n.js';
4
+ export type MfaLoginViewProps = AdminViewServerProps & {
5
+ labels?: MfaLabelOverrides;
6
+ language?: string;
7
+ };
8
+ /**
9
+ * Remplace la vue de connexion native (`admin.components.views.login`).
10
+ *
11
+ * Reprend la structure d'origine — logo, points d'extension `beforeLogin` / `afterLogin` — et
12
+ * n'échange que le formulaire, pour pouvoir envoyer le code du second facteur.
13
+ */
14
+ export declare const MfaLoginView: React.FC<MfaLoginViewProps>;
15
+ //# sourceMappingURL=login-view.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"login-view.d.ts","sourceRoot":"","sources":["../../src/components/login-view.tsx"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,oBAAoB,EAAE,MAAM,SAAS,CAAC;AAEpD,OAAO,KAAmB,MAAM,OAAO,CAAC;AAExC,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAGpD,MAAM,MAAM,iBAAiB,GAAG,oBAAoB,GAAG;IACrD,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF;;;;;GAKG;AACH,eAAO,MAAM,YAAY,EAAE,KAAK,CAAC,EAAE,CAAC,iBAAiB,CAyDpD,CAAC"}
@@ -0,0 +1,43 @@
1
+ import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
+ import { Logo } from '@payloadcms/next/rsc';
3
+ import { RenderServerComponent } from '@payloadcms/ui/elements/RenderServerComponent';
4
+ import { redirect } from 'next/navigation.js';
5
+ import { getSafeRedirect } from 'payload/shared';
6
+ import { Fragment } from 'react';
7
+ import { MfaLoginForm } from './login-form.js';
8
+ /**
9
+ * Remplace la vue de connexion native (`admin.components.views.login`).
10
+ *
11
+ * Reprend la structure d'origine — logo, points d'extension `beforeLogin` / `afterLogin` — et
12
+ * n'échange que le formulaire, pour pouvoir envoyer le code du second facteur.
13
+ */
14
+ export const MfaLoginView = ({ initPageResult, labels, language, params, searchParams, }) => {
15
+ const { locale, permissions, req } = initPageResult;
16
+ const { i18n, payload, payload: { config }, user, } = req;
17
+ const { admin: { components: { afterLogin, beforeLogin } = {} }, routes: { admin: adminRoute }, } = config;
18
+ if (user) {
19
+ redirect(getSafeRedirect({
20
+ fallbackTo: adminRoute,
21
+ redirectTo: searchParams?.redirect ?? '',
22
+ }));
23
+ }
24
+ const serverProps = {
25
+ i18n,
26
+ locale,
27
+ params,
28
+ payload,
29
+ permissions,
30
+ searchParams,
31
+ user: user ?? undefined,
32
+ };
33
+ return (_jsxs(Fragment, { children: [_jsx("div", { className: "login__brand", children: _jsx(Logo, { ...serverProps }) }), RenderServerComponent({
34
+ Component: beforeLogin,
35
+ importMap: payload.importMap,
36
+ serverProps,
37
+ }), _jsx(MfaLoginForm, { labels: labels, language: language, searchParams: searchParams }), RenderServerComponent({
38
+ Component: afterLogin,
39
+ importMap: payload.importMap,
40
+ serverProps,
41
+ })] }));
42
+ };
43
+ //# sourceMappingURL=login-view.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"login-view.js","sourceRoot":"","sources":["../../src/components/login-view.tsx"],"names":[],"mappings":";AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,sBAAsB,CAAC;AAC5C,OAAO,EAAE,qBAAqB,EAAE,MAAM,+CAA+C,CAAC;AACtF,OAAO,EAAE,QAAQ,EAAE,MAAM,oBAAoB,CAAC;AAE9C,OAAO,EAAE,eAAe,EAAE,MAAM,gBAAgB,CAAC;AACjD,OAAc,EAAE,QAAQ,EAAE,MAAM,OAAO,CAAC;AAGxC,OAAO,EAAE,YAAY,EAAE,MAAM,iBAAiB,CAAC;AAO/C;;;;;GAKG;AACH,MAAM,CAAC,MAAM,YAAY,GAAgC,CAAC,EACxD,cAAc,EACd,MAAM,EACN,QAAQ,EACR,MAAM,EACN,YAAY,GACb,EAAE,EAAE;IACH,MAAM,EAAE,MAAM,EAAE,WAAW,EAAE,GAAG,EAAE,GAAG,cAAc,CAAC;IACpD,MAAM,EACJ,IAAI,EACJ,OAAO,EACP,OAAO,EAAE,EAAE,MAAM,EAAE,EACnB,IAAI,GACL,GAAG,GAAG,CAAC;IAER,MAAM,EACJ,KAAK,EAAE,EAAE,UAAU,EAAE,EAAE,UAAU,EAAE,WAAW,EAAE,GAAG,EAAE,EAAE,EACvD,MAAM,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,GAC9B,GAAG,MAAM,CAAC;IAEX,IAAI,IAAI,EAAE,CAAC;QACT,QAAQ,CACN,eAAe,CAAC;YACd,UAAU,EAAE,UAAU;YACtB,UAAU,EAAE,YAAY,EAAE,QAAQ,IAAI,EAAE;SACzC,CAAC,CACH,CAAC;IACJ,CAAC;IAED,MAAM,WAAW,GAAG;QAClB,IAAI;QACJ,MAAM;QACN,MAAM;QACN,OAAO;QACP,WAAW;QACX,YAAY;QACZ,IAAI,EAAE,IAAI,IAAI,SAAS;KACxB,CAAC;IAEF,OAAO,CACL,MAAC,QAAQ,eACP,cAAK,SAAS,EAAC,cAAc,YAC3B,KAAC,IAAI,OAAK,WAAW,GAAI,GACrB,EACL,qBAAqB,CAAC;gBACrB,SAAS,EAAE,WAAW;gBACtB,SAAS,EAAE,OAAO,CAAC,SAAS;gBAC5B,WAAW;aACZ,CAAC,EACF,KAAC,YAAY,IAAC,MAAM,EAAE,MAAM,EAAE,QAAQ,EAAE,QAAQ,EAAE,YAAY,EAAE,YAAY,GAAI,EAC/E,qBAAqB,CAAC;gBACrB,SAAS,EAAE,UAAU;gBACrB,SAAS,EAAE,OAAO,CAAC,SAAS;gBAC5B,WAAW;aACZ,CAAC,IACO,CACZ,CAAC;AACJ,CAAC,CAAC"}
@@ -0,0 +1,16 @@
1
+ import React from 'react';
2
+ import { type MfaLabelOverrides } from '../i18n.js';
3
+ import './mfa.css';
4
+ export type MfaPanelProps = {
5
+ labels?: MfaLabelOverrides;
6
+ language?: string;
7
+ };
8
+ /**
9
+ * Panneau d'enrôlement TOTP, monté comme champ `ui` sur la collection protégée.
10
+ *
11
+ * Il n'apparaît que sur son propre compte : l'API MFA n'agit jamais que sur l'utilisateur de la
12
+ * session, donc l'afficher sur la fiche d'un autre utilisateur ne ferait que proposer des actions
13
+ * qui s'appliqueraient au mauvais compte.
14
+ */
15
+ export declare const MfaPanel: React.FC<MfaPanelProps>;
16
+ //# sourceMappingURL=mfa-panel.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"mfa-panel.d.ts","sourceRoot":"","sources":["../../src/components/mfa-panel.tsx"],"names":[],"mappings":"AAGA,OAAO,KAA2C,MAAM,OAAO,CAAC;AAEhE,OAAO,EAAyB,KAAK,iBAAiB,EAAE,MAAM,YAAY,CAAC;AAE3E,OAAO,WAAW,CAAC;AAanB,MAAM,MAAM,aAAa,GAAG;IAC1B,MAAM,CAAC,EAAE,iBAAiB,CAAC;IAC3B,QAAQ,CAAC,EAAE,MAAM,CAAC;CACnB,CAAC;AAEF;;;;;;GAMG;AACH,eAAO,MAAM,QAAQ,EAAE,KAAK,CAAC,EAAE,CAAC,aAAa,CA0L5C,CAAC"}