@things-factory/auth-base 10.1.215 → 10.1.218

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 (111) hide show
  1. package/config/config.development.js +2 -4
  2. package/config/config.production.js +1 -2
  3. package/dist-client/actions/auth.d.ts +1 -0
  4. package/dist-client/actions/auth.js +6 -0
  5. package/dist-client/actions/auth.js.map +1 -1
  6. package/dist-client/reducers/auth.d.ts +8 -0
  7. package/dist-client/reducers/auth.js +8 -2
  8. package/dist-client/reducers/auth.js.map +1 -1
  9. package/dist-client/tsconfig.tsbuildinfo +1 -1
  10. package/dist-server/constants/error-code.d.ts +1 -0
  11. package/dist-server/constants/error-code.js +2 -1
  12. package/dist-server/constants/error-code.js.map +1 -1
  13. package/dist-server/controllers/auth.d.ts +0 -1
  14. package/dist-server/controllers/auth.js +0 -1
  15. package/dist-server/controllers/auth.js.map +1 -1
  16. package/dist-server/controllers/password-setup.d.ts +33 -0
  17. package/dist-server/controllers/password-setup.js +77 -0
  18. package/dist-server/controllers/password-setup.js.map +1 -0
  19. package/dist-server/controllers/reset-password.d.ts +8 -1
  20. package/dist-server/controllers/reset-password.js +21 -11
  21. package/dist-server/controllers/reset-password.js.map +1 -1
  22. package/dist-server/controllers/signin.js +10 -0
  23. package/dist-server/controllers/signin.js.map +1 -1
  24. package/dist-server/index.d.ts +1 -0
  25. package/dist-server/index.js +2 -0
  26. package/dist-server/index.js.map +1 -1
  27. package/dist-server/middlewares/jwt-authenticate-middleware.js +8 -25
  28. package/dist-server/middlewares/jwt-authenticate-middleware.js.map +1 -1
  29. package/dist-server/router/auth-public-process-router.js +3 -41
  30. package/dist-server/router/auth-public-process-router.js.map +1 -1
  31. package/dist-server/service/access-request/access-request.js +2 -2
  32. package/dist-server/service/access-request/access-request.js.map +1 -1
  33. package/dist-server/service/audit/audit-kinds.js +1 -0
  34. package/dist-server/service/audit/audit-kinds.js.map +1 -1
  35. package/dist-server/service/audit/audit-subscriber.d.ts +1 -7
  36. package/dist-server/service/audit/audit-subscriber.js +2 -33
  37. package/dist-server/service/audit/audit-subscriber.js.map +1 -1
  38. package/dist-server/service/audit/index.d.ts +1 -1
  39. package/dist-server/service/index.d.ts +2 -4
  40. package/dist-server/service/index.js +22 -27
  41. package/dist-server/service/index.js.map +1 -1
  42. package/dist-server/service/open-fields.js +0 -2
  43. package/dist-server/service/open-fields.js.map +1 -1
  44. package/dist-server/service/user/user-mutation.d.ts +27 -6
  45. package/dist-server/service/user/user-mutation.js +26 -114
  46. package/dist-server/service/user/user-mutation.js.map +1 -1
  47. package/dist-server/service/user/user-query.d.ts +3 -4
  48. package/dist-server/service/user/user-query.js +7 -32
  49. package/dist-server/service/user/user-query.js.map +1 -1
  50. package/dist-server/service/user/user-types.d.ts +6 -2
  51. package/dist-server/service/user/user-types.js +20 -9
  52. package/dist-server/service/user/user-types.js.map +1 -1
  53. package/dist-server/service/user/user.js +17 -8
  54. package/dist-server/service/user/user.js.map +1 -1
  55. package/dist-server/service/verification-token/verification-token.d.ts +1 -0
  56. package/dist-server/service/verification-token/verification-token.js +1 -0
  57. package/dist-server/service/verification-token/verification-token.js.map +1 -1
  58. package/dist-server/templates/reset-password-email.d.ts +10 -3
  59. package/dist-server/templates/reset-password-email.js +8 -4
  60. package/dist-server/templates/reset-password-email.js.map +1 -1
  61. package/dist-server/tsconfig.tsbuildinfo +1 -1
  62. package/dist-server/utils/verification-token-rule.js +3 -1
  63. package/dist-server/utils/verification-token-rule.js.map +1 -1
  64. package/package.json +2 -2
  65. package/tests/add-user-gates.test.ts +34 -0
  66. package/tests/list-inheritance-db.test.ts +0 -15
  67. package/tests/lock-recovery.test.ts +2 -2
  68. package/tests/open-doors.test.ts +2 -5
  69. package/tests/password-reset-rule-db.test.ts +17 -16
  70. package/tests/password-setup-db.test.ts +213 -0
  71. package/tests/picture-reducer.test.ts +25 -0
  72. package/tests/verification-token-db.test.ts +12 -7
  73. package/translations/en.json +2 -2
  74. package/translations/es.json +2 -2
  75. package/translations/ja.json +2 -2
  76. package/translations/ko.json +2 -2
  77. package/translations/ms.json +2 -2
  78. package/translations/zh.json +2 -2
  79. package/dist-server/controllers/invitation.d.ts +0 -75
  80. package/dist-server/controllers/invitation.js +0 -195
  81. package/dist-server/controllers/invitation.js.map +0 -1
  82. package/dist-server/controllers/utils/make-invitation-token.d.ts +0 -1
  83. package/dist-server/controllers/utils/make-invitation-token.js +0 -9
  84. package/dist-server/controllers/utils/make-invitation-token.js.map +0 -1
  85. package/dist-server/controllers/utils/save-invitation-token.d.ts +0 -5
  86. package/dist-server/controllers/utils/save-invitation-token.js +0 -12
  87. package/dist-server/controllers/utils/save-invitation-token.js.map +0 -1
  88. package/dist-server/service/invitation/index.d.ts +0 -5
  89. package/dist-server/service/invitation/index.js +0 -9
  90. package/dist-server/service/invitation/index.js.map +0 -1
  91. package/dist-server/service/invitation/invitation-mutation.d.ts +0 -20
  92. package/dist-server/service/invitation/invitation-mutation.js +0 -109
  93. package/dist-server/service/invitation/invitation-mutation.js.map +0 -1
  94. package/dist-server/service/invitation/invitation-query.d.ts +0 -23
  95. package/dist-server/service/invitation/invitation-query.js +0 -89
  96. package/dist-server/service/invitation/invitation-query.js.map +0 -1
  97. package/dist-server/service/invitation/invitation-types.d.ts +0 -5
  98. package/dist-server/service/invitation/invitation-types.js +0 -21
  99. package/dist-server/service/invitation/invitation-types.js.map +0 -1
  100. package/dist-server/service/invitation/invitation.d.ts +0 -26
  101. package/dist-server/service/invitation/invitation.js +0 -142
  102. package/dist-server/service/invitation/invitation.js.map +0 -1
  103. package/dist-server/templates/invitation-email.d.ts +0 -6
  104. package/dist-server/templates/invitation-email.js +0 -82
  105. package/dist-server/templates/invitation-email.js.map +0 -1
  106. package/dist-server/utils/invitation-state.d.ts +0 -52
  107. package/dist-server/utils/invitation-state.js +0 -76
  108. package/dist-server/utils/invitation-state.js.map +0 -1
  109. package/tests/invitation-db.test.ts +0 -416
  110. package/tests/invitation-route.test.ts +0 -116
  111. package/tests/invitation-state.test.ts +0 -92
@@ -27,7 +27,7 @@ const crypto_1 = require("crypto");
27
27
  * and a rule that reads it would extend a link by touching the row. `expiresAt` is written once,
28
28
  * with the token.
29
29
  *
30
- * A row without `expiresAt` reads as **expired** — the rule `invitation-state.ts` uses. Such rows
30
+ * A row without `expiresAt` reads as **expired**. Such rows
31
31
  * were written before this existed, with the token in plain text; refusing them is also how links
32
32
  * already sent stop working when this ships.
33
33
  *
@@ -39,6 +39,8 @@ const crypto_1 = require("crypto");
39
39
  const HOUR = 60 * 60 * 1000;
40
40
  exports.VERIFICATION_TOKEN_LIFETIME_MS = Object.freeze({
41
41
  'password-reset': 1 * HOUR,
42
+ /* Handed over by mail or by hand, so it lives as long as an invitation did (ADR-0161 decision 3). */
43
+ 'password-setup': 48 * HOUR,
42
44
  unlock: 1 * HOUR,
43
45
  activation: 48 * HOUR
44
46
  });
@@ -1 +1 @@
1
- {"version":3,"file":"verification-token-rule.js","sourceRoot":"","sources":["../../server/utils/verification-token-rule.ts"],"names":[],"mappings":";;;AA2CA,sDAEC;AAQD,gEAQC;AAMD,0DAYC;AA/ED,mCAAmC;AAEnC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,MAAM,IAAI,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAA;AAEd,QAAA,8BAA8B,GAAqC,MAAM,CAAC,MAAM,CAAC;IAC5F,gBAAgB,EAAE,CAAC,GAAG,IAAI;IAC1B,MAAM,EAAE,CAAC,GAAG,IAAI;IAChB,UAAU,EAAE,EAAE,GAAG,IAAI;CACtB,CAAC,CAAA;AAEF,6CAA6C;AAC7C,SAAgB,qBAAqB,CAAC,KAAa;IACjD,OAAO,IAAA,mBAAU,EAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;AACjE,CAAC;AAED;;;;;GAKG;AACH,SAAgB,0BAA0B,CAAC,IAAY,EAAE,MAAY,IAAI,IAAI,EAAE;IAC7E,MAAM,QAAQ,GAAG,sCAA8B,CAAC,IAAI,CAAC,CAAA;IAErD,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,IAAI,KAAK,CAAC,4BAA4B,IAAI,mBAAmB,CAAC,CAAA;IACtE,CAAC;IAED,OAAO,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,QAAQ,CAAC,CAAA;AAC3C,CAAC;AAKD,0GAA0G;AAC1G,SAAgB,uBAAuB,CAAC,MAA+B,EAAE,MAAY,IAAI,IAAI,EAAE;IAC7F,IAAI,CAAC,MAAM,EAAE,SAAS,EAAE,CAAC;QACvB,OAAO,KAAK,CAAA;IACd,CAAC;IAED,MAAM,EAAE,GAAG,MAAM,CAAC,SAAS,YAAY,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAA;IAE3F,IAAI,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC;QAC/B,OAAO,KAAK,CAAA;IACd,CAAC;IAED,OAAO,GAAG,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,EAAE,CAAA;AACrC,CAAC","sourcesContent":["import { createHash } from 'crypto'\n\n/**\n * How long a mailed link lives, and what the table keeps of it. (Pure rule.)\n *\n * Password reset, unlock and sign-up confirmation each mail a link that carries a token, and the\n * token is the whole credential: whoever holds the link resets the password or unlocks the account.\n * Before 2026-09-24 those tokens never expired, were stored as written and were found by that same\n * value — so a reset mail from last year still changed a password today, and a copy of the table\n * (a backup, a replica, a dump handed to support) was a set of live reset links.\n *\n * ── The lifetimes ──────────────────────────────────────────────────────────\n *\n * password-reset 1 hour hands the account over at once, so short\n * unlock 1 hour the same\n * activation 48 hours the person may read the mail late; it only confirms an address\n *\n * Decided with the architect, 2026-09-24 (`docs/design/auth-rebuild-questions-2026-09-24.md` Q4).\n *\n * ── Expiry is its own column ───────────────────────────────────────────────\n * Not derived from `createdAt` or `updatedAt`: `updatedAt` moves every time the row is saved again,\n * and a rule that reads it would extend a link by touching the row. `expiresAt` is written once,\n * with the token.\n *\n * A row without `expiresAt` reads as **expired** — the rule `invitation-state.ts` uses. Such rows\n * were written before this existed, with the token in plain text; refusing them is also how links\n * already sent stop working when this ships.\n *\n * ── What is stored is a hash ───────────────────────────────────────────────\n * sha256 of the token. The token is 16 random bytes, so there is nothing for a slow hash to protect\n * — it guards against guessing, and there is no guessing a random 128-bit value. The point is only\n * that the table no longer holds anything a link can be made from.\n */\n\nconst HOUR = 60 * 60 * 1000\n\nexport const VERIFICATION_TOKEN_LIFETIME_MS: Readonly<Record<string, number>> = Object.freeze({\n 'password-reset': 1 * HOUR,\n unlock: 1 * HOUR,\n activation: 48 * HOUR\n})\n\n/** The value the table keeps for a token. */\nexport function hashVerificationToken(token: string): string {\n return createHash('sha256').update(token, 'utf8').digest('hex')\n}\n\n/**\n * When a token of this type, issued at `now`, stops working.\n *\n * Throws for a type with no lifetime here — a new kind of mailed link has to say how long it lives\n * rather than inherit forever.\n */\nexport function verificationTokenExpiresAt(type: string, now: Date = new Date()): Date {\n const lifetime = VERIFICATION_TOKEN_LIFETIME_MS[type]\n\n if (!lifetime) {\n throw new Error(`verification token type '${type}' has no lifetime`)\n }\n\n return new Date(now.getTime() + lifetime)\n}\n\n/** The parts of a stored token this rule reads. */\nexport type VerificationTokenRecord = { expiresAt?: Date | string | null } | null | undefined\n\n/** May this token still be used, as of `now`? Absence of an expiry refuses; the expiry itself refuses. */\nexport function isVerificationTokenLive(record: VerificationTokenRecord, now: Date = new Date()): boolean {\n if (!record?.expiresAt) {\n return false\n }\n\n const at = record.expiresAt instanceof Date ? record.expiresAt : new Date(record.expiresAt)\n\n if (Number.isNaN(at.getTime())) {\n return false\n }\n\n return now.getTime() < at.getTime()\n}\n"]}
1
+ {"version":3,"file":"verification-token-rule.js","sourceRoot":"","sources":["../../server/utils/verification-token-rule.ts"],"names":[],"mappings":";;;AA6CA,sDAEC;AAQD,gEAQC;AAMD,0DAYC;AAjFD,mCAAmC;AAEnC;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA8BG;AAEH,MAAM,IAAI,GAAG,EAAE,GAAG,EAAE,GAAG,IAAI,CAAA;AAEd,QAAA,8BAA8B,GAAqC,MAAM,CAAC,MAAM,CAAC;IAC5F,gBAAgB,EAAE,CAAC,GAAG,IAAI;IAC1B,qGAAqG;IACrG,gBAAgB,EAAE,EAAE,GAAG,IAAI;IAC3B,MAAM,EAAE,CAAC,GAAG,IAAI;IAChB,UAAU,EAAE,EAAE,GAAG,IAAI;CACtB,CAAC,CAAA;AAEF,6CAA6C;AAC7C,SAAgB,qBAAqB,CAAC,KAAa;IACjD,OAAO,IAAA,mBAAU,EAAC,QAAQ,CAAC,CAAC,MAAM,CAAC,KAAK,EAAE,MAAM,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,CAAA;AACjE,CAAC;AAED;;;;;GAKG;AACH,SAAgB,0BAA0B,CAAC,IAAY,EAAE,MAAY,IAAI,IAAI,EAAE;IAC7E,MAAM,QAAQ,GAAG,sCAA8B,CAAC,IAAI,CAAC,CAAA;IAErD,IAAI,CAAC,QAAQ,EAAE,CAAC;QACd,MAAM,IAAI,KAAK,CAAC,4BAA4B,IAAI,mBAAmB,CAAC,CAAA;IACtE,CAAC;IAED,OAAO,IAAI,IAAI,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,QAAQ,CAAC,CAAA;AAC3C,CAAC;AAKD,0GAA0G;AAC1G,SAAgB,uBAAuB,CAAC,MAA+B,EAAE,MAAY,IAAI,IAAI,EAAE;IAC7F,IAAI,CAAC,MAAM,EAAE,SAAS,EAAE,CAAC;QACvB,OAAO,KAAK,CAAA;IACd,CAAC;IAED,MAAM,EAAE,GAAG,MAAM,CAAC,SAAS,YAAY,IAAI,CAAC,CAAC,CAAC,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,IAAI,CAAC,MAAM,CAAC,SAAS,CAAC,CAAA;IAE3F,IAAI,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,EAAE,CAAC;QAC/B,OAAO,KAAK,CAAA;IACd,CAAC;IAED,OAAO,GAAG,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC,OAAO,EAAE,CAAA;AACrC,CAAC","sourcesContent":["import { createHash } from 'crypto'\n\n/**\n * How long a mailed link lives, and what the table keeps of it. (Pure rule.)\n *\n * Password reset, unlock and sign-up confirmation each mail a link that carries a token, and the\n * token is the whole credential: whoever holds the link resets the password or unlocks the account.\n * Before 2026-09-24 those tokens never expired, were stored as written and were found by that same\n * value — so a reset mail from last year still changed a password today, and a copy of the table\n * (a backup, a replica, a dump handed to support) was a set of live reset links.\n *\n * ── The lifetimes ──────────────────────────────────────────────────────────\n *\n * password-reset 1 hour hands the account over at once, so short\n * unlock 1 hour the same\n * activation 48 hours the person may read the mail late; it only confirms an address\n *\n * Decided with the architect, 2026-09-24 (`docs/design/auth-rebuild-questions-2026-09-24.md` Q4).\n *\n * ── Expiry is its own column ───────────────────────────────────────────────\n * Not derived from `createdAt` or `updatedAt`: `updatedAt` moves every time the row is saved again,\n * and a rule that reads it would extend a link by touching the row. `expiresAt` is written once,\n * with the token.\n *\n * A row without `expiresAt` reads as **expired**. Such rows\n * were written before this existed, with the token in plain text; refusing them is also how links\n * already sent stop working when this ships.\n *\n * ── What is stored is a hash ───────────────────────────────────────────────\n * sha256 of the token. The token is 16 random bytes, so there is nothing for a slow hash to protect\n * — it guards against guessing, and there is no guessing a random 128-bit value. The point is only\n * that the table no longer holds anything a link can be made from.\n */\n\nconst HOUR = 60 * 60 * 1000\n\nexport const VERIFICATION_TOKEN_LIFETIME_MS: Readonly<Record<string, number>> = Object.freeze({\n 'password-reset': 1 * HOUR,\n /* Handed over by mail or by hand, so it lives as long as an invitation did (ADR-0161 decision 3). */\n 'password-setup': 48 * HOUR,\n unlock: 1 * HOUR,\n activation: 48 * HOUR\n})\n\n/** The value the table keeps for a token. */\nexport function hashVerificationToken(token: string): string {\n return createHash('sha256').update(token, 'utf8').digest('hex')\n}\n\n/**\n * When a token of this type, issued at `now`, stops working.\n *\n * Throws for a type with no lifetime here — a new kind of mailed link has to say how long it lives\n * rather than inherit forever.\n */\nexport function verificationTokenExpiresAt(type: string, now: Date = new Date()): Date {\n const lifetime = VERIFICATION_TOKEN_LIFETIME_MS[type]\n\n if (!lifetime) {\n throw new Error(`verification token type '${type}' has no lifetime`)\n }\n\n return new Date(now.getTime() + lifetime)\n}\n\n/** The parts of a stored token this rule reads. */\nexport type VerificationTokenRecord = { expiresAt?: Date | string | null } | null | undefined\n\n/** May this token still be used, as of `now`? Absence of an expiry refuses; the expiry itself refuses. */\nexport function isVerificationTokenLive(record: VerificationTokenRecord, now: Date = new Date()): boolean {\n if (!record?.expiresAt) {\n return false\n }\n\n const at = record.expiresAt instanceof Date ? record.expiresAt : new Date(record.expiresAt)\n\n if (Number.isNaN(at.getTime())) {\n return false\n }\n\n return now.getTime() < at.getTime()\n}\n"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@things-factory/auth-base",
3
- "version": "10.1.215",
3
+ "version": "10.1.218",
4
4
  "main": "dist-server/index.js",
5
5
  "browser": "dist-client/index.js",
6
6
  "things-factory": true,
@@ -50,5 +50,5 @@
50
50
  "passport-jwt": "^4.0.0",
51
51
  "passport-local": "^1.0.0"
52
52
  },
53
- "gitHead": "7de5e80eb1f4e2e268746aee39f820e7563f454d"
53
+ "gitHead": "b4e60fdfe307cecebb2264267f616749a502d5fe"
54
54
  }
@@ -0,0 +1,34 @@
1
+ import { buildSchema } from 'type-graphql'
2
+
3
+ import { loadCompiled } from './compiled'
4
+
5
+ /**
6
+ * The questions the user popups ask are gated as the actions they lead to.
7
+ *
8
+ * `checkUserExistence` is asked only by the add-user popup, before `inviteUser` / `createUser`
9
+ * (each `user:mutation`, owner granted). It asked `user:query` alone, so a domain owner without that role was refused
10
+ * the lookup and the popup took the refusal for "no account" - and set out to create an account for someone who
11
+ * had one. `mayIssuePasswordSetupLink` answers for the setup link button, so it stands where `issuePasswordSetupLink` does.
12
+ */
13
+ const { schema: declared } = loadCompiled('service/index')
14
+ const { privilegeDirectiveResolver, privilegeOfField } = loadCompiled('service/privilege/privilege-directive')
15
+
16
+ function gateOf(field: string) {
17
+ const gate = privilegeOfField('Query', field)
18
+ return [gate.known, gate['gate']?.category, gate['gate']?.privilege, gate['gate']?.owner]
19
+ }
20
+
21
+ describe('what the user popups ask', () => {
22
+ beforeAll(async () => {
23
+ const schema = await buildSchema({ resolvers: declared.resolverClasses, validate: false })
24
+ privilegeDirectiveResolver(schema, { modules: [] })
25
+ })
26
+
27
+ it('★ checkUserExistence asks what adding a user asks - user:mutation, or the owner', () => {
28
+ expect(gateOf('checkUserExistence')).toEqual([true, 'user', 'mutation', true])
29
+ })
30
+
31
+ it('mayIssuePasswordSetupLink asks what issuing asks', () => {
32
+ expect(gateOf('mayIssuePasswordSetupLink')).toEqual([true, 'user', 'mutation', true])
33
+ })
34
+ })
@@ -19,9 +19,6 @@ const { Appliance } = loadCompiled('service/appliance/appliance')
19
19
  const { ApplianceQuery } = loadCompiled('service/appliance/appliance-query')
20
20
  const { Application } = loadCompiled('service/application/application')
21
21
  const { ApplicationQuery } = loadCompiled('service/application/application-query')
22
- const { Invitation } = loadCompiled('service/invitation/invitation')
23
- const { InvitationStatus } = loadCompiled('utils/invitation-state')
24
- const { InvitationQuery } = loadCompiled('service/invitation/invitation-query')
25
22
  const { Partner } = loadCompiled('service/partner/partner')
26
23
  const { PartnerQuery } = loadCompiled('service/partner/partner-query')
27
24
 
@@ -101,18 +98,6 @@ const LISTS: [string, () => Promise<any>, (params: any, context: any) => Promise
101
98
  }),
102
99
  (params, context) => new ApplicationQuery().applications(params, context)
103
100
  ],
104
- [
105
- 'invitations',
106
- () =>
107
- getRepository(Invitation).save({
108
- email: 'lee@acme.z',
109
- domain: parent,
110
- status: InvitationStatus.PENDING,
111
- token: 'parent-token',
112
- expiresAt: new Date(Date.now() + 3600_000)
113
- }),
114
- (params, context) => new InvitationQuery().invitations(params, context)
115
- ],
116
101
  [
117
102
  'partners',
118
103
  () => getRepository(Partner).save({ domain: parent, partnerDomain, requestedAt: new Date() }),
@@ -70,7 +70,7 @@ describe('재설정 메일 경로 (PASSWORD_RESET)', () => {
70
70
  const user = await lockedUser()
71
71
  await giveToken(user.id, 'tok', VerificationTokenType.PASSWORD_RESET)
72
72
 
73
- await resetPassword('tok', NEW, context)
73
+ await resetPassword('tok', NEW, 'kim@example.com', context)
74
74
 
75
75
  const after = await getRepository(User).findOneBy({ id: user.id })
76
76
  expect(User.verify(after.password, NEW, after.salt)).toBe(true)
@@ -84,7 +84,7 @@ describe('재설정 메일 경로 (PASSWORD_RESET)', () => {
84
84
  const user = await lockedUser()
85
85
  await giveToken(user.id, 'tok', VerificationTokenType.PASSWORD_RESET)
86
86
 
87
- await resetPassword('tok', NEW, context)
87
+ await resetPassword('tok', NEW, 'kim@example.com', context)
88
88
 
89
89
  await expect(signin({ username: 'kim', password: NEW })).rejects.toThrow()
90
90
  })
@@ -81,8 +81,7 @@ describe('the doors that were closed', () => {
81
81
  * domainUserRoleRegister makes a domain, its roles and its users. `domainRegister`
82
82
  * beside it makes only a domain and is super-user only
83
83
  * loginHistories everybody's sign-ins - now the audit's, behind `audit:query` (ADR-0141)
84
- * invitation / invitations and neither of them scopes by tenant at all
85
- * sendInvitation / cancelInvitation / inviteUser
84
+ * inviteUser
86
85
  */
87
86
  const CLOSED = {
88
87
  Mutation: [
@@ -92,11 +91,9 @@ describe('the doors that were closed', () => {
92
91
  'deleteAuthProvider',
93
92
  'deleteAuthProviders',
94
93
  'domainUserRoleRegister',
95
- 'sendInvitation',
96
- 'cancelInvitation',
97
94
  'inviteUser'
98
95
  ],
99
- Query: ['auditRecords', 'auditPeople', 'auditDoors', 'auditHealth', 'invitation', 'invitations']
96
+ Query: ['auditRecords', 'auditPeople', 'auditDoors', 'auditHealth']
100
97
  }
101
98
 
102
99
  it.each(Object.entries(CLOSED).flatMap(([t, fields]) => fields.map(f => [t, f])))('%s.%s', (typeName, field) => {
@@ -1,14 +1,12 @@
1
1
  /**
2
- * Whom a domain may put back on the default password.
2
+ * Whose password a domain may take back - by issuing a password setup link (ADR-0161).
3
3
  *
4
- * `resetPasswordToDefault` asked only that the person belong to the asking domain. A person has one password for
5
- * every domain, and the default is known: whoever signs in with it first is sent to set a new one. So a user manager
6
- * of any domain could take the account of someone who also works elsewhere, of another domain's owner, and of the
7
- * super user (the system domain's owner) the moment that person was a member here.
4
+ * The reset asked only that the person belong to the asking domain. A person has one password for every domain, so a
5
+ * user manager of any domain could take the password of someone who also works elsewhere, of another domain's owner,
6
+ * and of the super user (the system domain's owner) the moment that person was a member here. It was the default
7
+ * password then; it is a setup link now, and the same people are refused.
8
8
  */
9
9
 
10
- import { config } from '@things-factory/env'
11
-
12
10
  import { closeAuthDatabase, Domain, getRepository, openAuthDatabase, resetAuthDatabase } from './db'
13
11
  import { loadCompiled } from './compiled'
14
12
 
@@ -17,7 +15,6 @@ const { UserMutation } = loadCompiled('service/user/user-mutation')
17
15
  const { UserQuery } = loadCompiled('service/user/user-query')
18
16
  const { DomainOwner } = loadCompiled('service/domain-owner/domain-owner')
19
17
 
20
- const DEFAULT_PASSWORD = 'Default!234'
21
18
  const OWN_PASSWORD = 'Mine!5678'
22
19
 
23
20
  let here: any
@@ -40,15 +37,20 @@ async function makeUser(domains: any[], extra: any = {}) {
40
37
  }
41
38
 
42
39
  function context(domain: any) {
43
- return { state: { domain, tx: undefined }, t: (key: string) => key } as any
40
+ return {
41
+ state: { domain, tx: undefined },
42
+ t: (key: string) => key,
43
+ headers: {},
44
+ request: { protocol: 'https', host: 'acme.example.com' }
45
+ } as any
44
46
  }
45
47
 
46
48
  function reset(user: any, domain: any) {
47
- return new UserMutation().resetPasswordToDefault(user.username, context(domain))
49
+ return new UserMutation().issuePasswordSetupLink(user.username, false, context(domain))
48
50
  }
49
51
 
50
52
  function mayReset(user: any, domain: any) {
51
- return new UserQuery().mayResetPasswordToDefault(user.id, context(domain))
53
+ return new UserQuery().mayIssuePasswordSetupLink(user.id, context(domain))
52
54
  }
53
55
 
54
56
  async function stillHasOwnPassword(user: any) {
@@ -61,13 +63,11 @@ beforeAll(async () => {
61
63
  })
62
64
 
63
65
  afterAll(async () => {
64
- config.set('password', undefined)
65
66
  await closeAuthDatabase()
66
67
  })
67
68
 
68
69
  beforeEach(async () => {
69
70
  await resetAuthDatabase()
70
- config.set('password', { defaultPassword: DEFAULT_PASSWORD })
71
71
 
72
72
  const domainRepository = getRepository(Domain)
73
73
  here = await domainRepository.save({ name: 'Plant A', subdomain: 'plant-a' })
@@ -76,14 +76,15 @@ beforeEach(async () => {
76
76
  })
77
77
 
78
78
  describe('a person wholly of this domain', () => {
79
- it('is reset to the default, and the screen offers it', async () => {
79
+ it('is issued a link and waits for a password, and the screen offers it', async () => {
80
80
  const user = await makeUser([here])
81
81
 
82
82
  expect(await mayReset(user, here)).toBe(true)
83
- await expect(reset(user, here)).resolves.toBe(true)
83
+ await expect(reset(user, here)).resolves.toMatchObject({ link: expect.stringContaining('/auth/reset-password#') })
84
84
 
85
85
  const read = await getRepository(User).findOneBy({ id: user.id })
86
- expect(User.verify(read.password, DEFAULT_PASSWORD, read.salt)).toBe(true)
86
+ expect(read.status).toBe('password_reset_required')
87
+ expect(await stillHasOwnPassword(user)).toBe(false)
87
88
  })
88
89
  })
89
90
 
@@ -0,0 +1,213 @@
1
+ /**
2
+ * An administrator never sets anybody's password (ADR-0161).
3
+ *
4
+ * It used to be the installation's default password - one value everyone handed one knew, with no end, for every
5
+ * account an administrator made or reset. Now such an account waits for its password, and the person sets it through
6
+ * a one-time link that is mailed or handed over: 48 hours, hashed at rest, asking for the address it was for, and the
7
+ * only live one. Issuing it ends the sessions opened before.
8
+ */
9
+
10
+ import { authDataSource, closeAuthDatabase, Domain, getRepository, openAuthDatabase, resetAuthDatabase } from './db'
11
+ import { loadCompiled } from './compiled'
12
+
13
+ const jwt = require('jsonwebtoken')
14
+
15
+ const { User } = loadCompiled('service/user/user')
16
+ const { UserMutation } = loadCompiled('service/user/user-mutation')
17
+ const { VerificationToken } = loadCompiled('service/verification-token/verification-token')
18
+ const { signin } = loadCompiled('controllers/signin')
19
+ const { resetPassword } = loadCompiled('controllers/reset-password')
20
+ const { hashVerificationToken } = loadCompiled('utils/verification-token-rule')
21
+
22
+ const OWN_PASSWORD = 'Mine!5678'
23
+ const NEW_PASSWORD = 'N3w!Passw0rd'
24
+ const HOUR = 60 * 60 * 1000
25
+
26
+ let here: any
27
+
28
+ function context(tx?: any) {
29
+ return {
30
+ state: { domain: here, tx },
31
+ t: (key: string) => key,
32
+ headers: {},
33
+ request: { protocol: 'https', host: 'acme.example.com' }
34
+ } as any
35
+ }
36
+
37
+ async function makeActive(email = 'kim@example.com') {
38
+ const salt = User.generateSalt()
39
+ return await getRepository(User).save({
40
+ username: email,
41
+ email,
42
+ name: 'Kim',
43
+ salt,
44
+ password: User.encode(OWN_PASSWORD, salt),
45
+ status: 'activated',
46
+ domains: [here]
47
+ })
48
+ }
49
+
50
+ function create(email = 'lee@example.com') {
51
+ return new UserMutation().createUser({ username: email, email, name: 'Lee', userType: 'user' }, context())
52
+ }
53
+
54
+ function issue(user: any, mail = false, tx?: any) {
55
+ return new UserMutation().issuePasswordSetupLink(user.username, mail, context(tx))
56
+ }
57
+
58
+ const tokenOf = (link: string) => link.split('#')[1]
59
+ const reload = (id: string) => getRepository(User).findOneBy({ id })
60
+ const setPassword = (token: string, email: string, password = NEW_PASSWORD) =>
61
+ resetPassword(token, password, email, { t: (key: string) => key })
62
+
63
+ beforeAll(async () => {
64
+ await openAuthDatabase()
65
+ })
66
+
67
+ afterAll(async () => {
68
+ await closeAuthDatabase()
69
+ })
70
+
71
+ beforeEach(async () => {
72
+ await resetAuthDatabase()
73
+ here = await getRepository(Domain).save({ name: 'Plant A', subdomain: 'plant-a' })
74
+ })
75
+
76
+ describe('an account an administrator makes', () => {
77
+ it('★ waits for its password, which nobody was given', async () => {
78
+ const made = await create()
79
+ const read = await reload(made.id)
80
+
81
+ expect(read.status).toBe('password_reset_required')
82
+ expect(read.password).toBeTruthy()
83
+ })
84
+
85
+ it('★ does not sign in, and a guess against it is not counted', async () => {
86
+ const made = await create()
87
+
88
+ await expect(signin({ username: made.username, password: 'anything' })).rejects.toMatchObject({
89
+ errorCode: 'password-setup-pending'
90
+ })
91
+ expect((await reload(made.id)).failCount ?? 0).toBe(0)
92
+ })
93
+
94
+ it('★ is refused on every request too', async () => {
95
+ const made = await create()
96
+
97
+ await expect(User.checkAuth({ username: made.username, email: made.email })).rejects.toMatchObject({
98
+ errorCode: 'password-setup-pending'
99
+ })
100
+ })
101
+
102
+ it('is not opened by activating it', async () => {
103
+ const made = await create()
104
+
105
+ await expect(new UserMutation().activateUser(made.username, context())).rejects.toMatchObject({
106
+ code: 'password-setup-pending'
107
+ })
108
+ })
109
+ })
110
+
111
+ describe('the setup link', () => {
112
+ it('★ is handed back once, kept only as a hash, and lives 48 hours', async () => {
113
+ const made = await create()
114
+ const before = Date.now()
115
+
116
+ const issued = await issue(made)
117
+
118
+ expect(issued.link).toMatch(/^https?:\/\/acme\.example\.com\/auth\/reset-password#[0-9a-f]{32}$/)
119
+ const stored = await getRepository(VerificationToken).findOneBy({ userId: made.id })
120
+ expect(stored.type).toBe('password-setup')
121
+ expect(stored.token).toBe(hashVerificationToken(tokenOf(issued.link)))
122
+ expect(stored.token).not.toBe(tokenOf(issued.link))
123
+ expect(new Date(stored.expiresAt).getTime() - before).toBeGreaterThanOrEqual(48 * HOUR - 1000)
124
+ expect(new Date(stored.expiresAt).getTime() - before).toBeLessThanOrEqual(48 * HOUR + 5000)
125
+ })
126
+
127
+ it('★ opens the account with the address it was for, and then signs in', async () => {
128
+ const made = await create()
129
+ const { link } = await issue(made)
130
+
131
+ await setPassword(tokenOf(link), 'LEE@example.com ')
132
+
133
+ expect((await reload(made.id)).status).toBe('activated')
134
+ await expect(signin({ username: made.username, password: NEW_PASSWORD })).resolves.toMatchObject({
135
+ token: expect.any(String)
136
+ })
137
+ })
138
+
139
+ it('★ is not enough without the address it was for', async () => {
140
+ const made = await create()
141
+ const { link } = await issue(made)
142
+
143
+ await expect(setPassword(tokenOf(link), 'someone@else.example')).rejects.toThrow()
144
+ await expect(setPassword(tokenOf(link), '')).rejects.toThrow()
145
+ expect((await reload(made.id)).status).toBe('password_reset_required')
146
+ })
147
+
148
+ it('works once', async () => {
149
+ const made = await create()
150
+ const { link } = await issue(made)
151
+
152
+ await setPassword(tokenOf(link), made.email)
153
+ await expect(setPassword(tokenOf(link), made.email, 'An0ther!Pass')).rejects.toThrow()
154
+ })
155
+
156
+ it('★ issued again, retires the last one', async () => {
157
+ const made = await create()
158
+ const first = await issue(made)
159
+ const second = await issue(made)
160
+
161
+ await expect(setPassword(tokenOf(first.link), made.email)).rejects.toThrow()
162
+ await setPassword(tokenOf(second.link), made.email)
163
+ expect((await reload(made.id)).status).toBe('activated')
164
+ })
165
+ })
166
+
167
+ describe('taking back the password of an account in use', () => {
168
+ it('★ stops the old password and ends the sessions opened before', async () => {
169
+ const kim = await makeActive()
170
+ const { token: before } = await signin({ username: kim.username, password: OWN_PASSWORD })
171
+ await expect(User.checkAuth(jwt.decode(before))).resolves.toBeTruthy()
172
+
173
+ const { link } = await issue(kim)
174
+
175
+ await expect(signin({ username: kim.username, password: OWN_PASSWORD })).rejects.toMatchObject({
176
+ errorCode: 'password-setup-pending'
177
+ })
178
+
179
+ await setPassword(tokenOf(link), kim.email)
180
+
181
+ /* Open again - but the session from before stays ended: its generation has passed. */
182
+ await expect(User.checkAuth(jwt.decode(before))).rejects.toThrow()
183
+ const { token: after } = await signin({ username: kim.username, password: NEW_PASSWORD })
184
+ await expect(User.checkAuth(jwt.decode(after))).resolves.toBeTruthy()
185
+ })
186
+
187
+ it('★ by mail - a mail that does not go out changes nothing', async () => {
188
+ const kim = await makeActive()
189
+
190
+ await expect(
191
+ authDataSource().transaction(async (tx: any) => await issue(kim, true, tx))
192
+ ).rejects.toMatchObject({ code: 'password-setup-mail-not-sent' })
193
+
194
+ const read = await reload(kim.id)
195
+ expect(read.status).toBe('activated')
196
+ expect(User.verify(read.password, OWN_PASSWORD, read.salt)).toBe(true)
197
+ expect(await getRepository(VerificationToken).findOneBy({ userId: kim.id })).toBeNull()
198
+ })
199
+ })
200
+
201
+ describe('a forgotten password', () => {
202
+ it('asks for the address as well, through the same page', async () => {
203
+ const kim = await makeActive()
204
+ const { makeVerificationToken } = loadCompiled('controllers/utils/make-verification-token')
205
+ const { saveVerificationToken } = loadCompiled('controllers/utils/save-verification-token')
206
+ const token = makeVerificationToken()
207
+ await saveVerificationToken(kim.id, token, 'password-reset')
208
+
209
+ await expect(setPassword(token, 'someone@else.example')).rejects.toThrow()
210
+ await setPassword(token, kim.email)
211
+ await expect(signin({ username: kim.username, password: NEW_PASSWORD })).resolves.toBeTruthy()
212
+ })
213
+ })
@@ -0,0 +1,25 @@
1
+ import { SET_PICTURE, SET_PROFILE } from '../client/actions/auth'
2
+ import auth from '../client/reducers/auth'
3
+
4
+ /** The signed-in person's picture, which the header's avatar draws (`auth.picture`). */
5
+ describe('the signed-in person\'s picture', () => {
6
+ const picture = { picture: '/attachment/face.png', left: 0, top: 0, zoom: 1 }
7
+
8
+ it('starts with none - the avatar draws a monogram', () => {
9
+ expect(auth(undefined, { type: '@@INIT' }).picture).toBeNull()
10
+ })
11
+
12
+ it('★ is set by SET_PICTURE and survives a later profile', () => {
13
+ let state = auth(undefined, { type: SET_PICTURE, picture })
14
+ state = auth(state, { type: SET_PROFILE, user: { name: '남상혁' } })
15
+
16
+ expect(state.picture).toEqual(picture)
17
+ expect(state.user).toEqual({ name: '남상혁' })
18
+ })
19
+
20
+ it('a missing picture clears it', () => {
21
+ const state = auth(auth(undefined, { type: SET_PICTURE, picture }), { type: SET_PICTURE, picture: undefined })
22
+
23
+ expect(state.picture).toBeNull()
24
+ })
25
+ })
@@ -109,7 +109,12 @@ describe('the rule', () => {
109
109
  const now = new Date('2026-09-24T12:00:00Z')
110
110
 
111
111
  it('gives each mailed link its lifetime', () => {
112
- expect(VERIFICATION_TOKEN_LIFETIME_MS).toEqual({ 'password-reset': HOUR, unlock: HOUR, activation: 48 * HOUR })
112
+ expect(VERIFICATION_TOKEN_LIFETIME_MS).toEqual({
113
+ 'password-reset': HOUR,
114
+ 'password-setup': 48 * HOUR,
115
+ unlock: HOUR,
116
+ activation: 48 * HOUR
117
+ })
113
118
  expect(verificationTokenExpiresAt('password-reset', now).getTime()).toBe(now.getTime() + HOUR)
114
119
  })
115
120
 
@@ -153,11 +158,11 @@ describe('password reset', () => {
153
158
  await sendPasswordResetEmail({ user, context })
154
159
  const token = tokenInLastMail(RESET_PASSWORD_PATH)
155
160
 
156
- await resetPassword(token, NEW, context)
161
+ await resetPassword(token, NEW, 'kim@example.com', context)
157
162
 
158
163
  const after = await getRepository(User).findOneBy({ id: user.id })
159
164
  expect(User.verify(after.password, NEW, after.salt)).toBe(true)
160
- await expect(resetPassword(token, OLD, context)).rejects.toThrow('text.invalid verification token')
165
+ await expect(resetPassword(token, OLD, 'kim@example.com', context)).rejects.toThrow('text.invalid verification token')
161
166
  })
162
167
 
163
168
  it('refuses the link after it expires', async () => {
@@ -167,7 +172,7 @@ describe('password reset', () => {
167
172
 
168
173
  await expireAt(user.id, new Date(Date.now() - 1))
169
174
 
170
- await expect(resetPassword(token, NEW, context)).rejects.toThrow('text.invalid verification token')
175
+ await expect(resetPassword(token, NEW, 'kim@example.com', context)).rejects.toThrow('text.invalid verification token')
171
176
  const after = await getRepository(User).findOneBy({ id: user.id })
172
177
  expect(User.verify(after.password, OLD, after.salt)).toBe(true)
173
178
  })
@@ -180,7 +185,7 @@ describe('password reset', () => {
180
185
  type: VerificationTokenType.PASSWORD_RESET
181
186
  })
182
187
 
183
- await expect(resetPassword('b'.repeat(32), NEW, context)).rejects.toThrow('text.invalid verification token')
188
+ await expect(resetPassword('b'.repeat(32), NEW, 'kim@example.com', context)).rejects.toThrow('text.invalid verification token')
184
189
  })
185
190
 
186
191
  it('refuses a row whose expiry is missing, even when the hash matches', async () => {
@@ -194,7 +199,7 @@ describe('password reset', () => {
194
199
 
195
200
  await expireAt(user.id, null)
196
201
 
197
- await expect(resetPassword(token, NEW, context)).rejects.toThrow('text.invalid verification token')
202
+ await expect(resetPassword(token, NEW, 'kim@example.com', context)).rejects.toThrow('text.invalid verification token')
198
203
  })
199
204
 
200
205
  it('does not find a token by the value stored in the table', async () => {
@@ -203,7 +208,7 @@ describe('password reset', () => {
203
208
  await sendPasswordResetEmail({ user, context })
204
209
  const stored = (await row(user.id)).token
205
210
 
206
- await expect(resetPassword(stored, NEW, context)).rejects.toThrow('text.invalid verification token')
211
+ await expect(resetPassword(stored, NEW, 'kim@example.com', context)).rejects.toThrow('text.invalid verification token')
207
212
  })
208
213
  })
209
214
 
@@ -9,7 +9,6 @@
9
9
  "error.invalid username": "invalid username: {username}",
10
10
  "error.mail could not be sent": "the email could not be sent. try again in a moment.",
11
11
  "error.ownership required": "unauthorized! domain or system ownership required",
12
- "error.password should be supported": "initial password or default password should be supported",
13
12
  "error.password should match the rule": "password should match following rule. ${rule}",
14
13
  "error.password used in the past": "password used in the past",
15
14
  "error.role-name-stands-inherited": "a role called {name} is already visible here, defined in {domain}. stand this domain's own of that name only if that is what you mean to do.",
@@ -29,6 +28,8 @@
29
28
  "error.x is not a member of y": "{x} is not a member of {y}",
30
29
  "error.x privilege required": "unauthorized! {x} privilege required",
31
30
  "error.role-not-assignable-here": "this domain cannot assign the role(s) {roles}.",
31
+ "error.password-setup-pending": "This account is waiting for its password. Set it through the password setup link you received, then sign in.",
32
+ "error.password-setup-mail-not-sent": "The mail to {email} did not go out. Nothing was changed - copy the link instead.",
32
33
  "error.password-reset-owner-account": "A domain owner's or administrator's password is not reset to the default.",
33
34
  "error.password-reset-reaches-other-domains": "they also work in {domains}, so their password is not this $t(term.domain)'s to reset.",
34
35
  "error.x not allowed from this address": "unauthorized! {x} is not allowed from this address",
@@ -212,7 +213,6 @@
212
213
  "text.delete account succeed": "delete account succeed",
213
214
  "text.inactive user": "inactive user",
214
215
  "text.invalid verification token": "invalid verification token",
215
- "text.invitation email sent": "invitation email sent",
216
216
  "text.password changed successfully": "password changed successfully",
217
217
  "text.password reset email sent": "if an account uses this address, a password reset email is on its way.",
218
218
  "text.password reset succeed": "password reset succeed",
@@ -9,7 +9,6 @@
9
9
  "error.invalid username": "nombre de usuario no válido: {username}",
10
10
  "error.mail could not be sent": "no se pudo enviar el correo. vuelva a intentarlo en un momento.",
11
11
  "error.ownership required": "¡no autorizado! se requiere la propiedad del dominio o del sistema",
12
- "error.password should be supported": "se debe proporcionar una contraseña inicial o una contraseña predeterminada",
13
12
  "error.password should match the rule": "la contraseña debe cumplir la siguiente regla. ${rule}",
14
13
  "error.password used in the past": "contraseña usada anteriormente",
15
14
  "error.role-name-stands-inherited": "ya se muestra aquí un rol llamado {name}, definido en {domain}. cree en este dominio uno propio con ese nombre solo si es lo que pretende hacer.",
@@ -29,6 +28,8 @@
29
28
  "error.x is not a member of y": "{x} no es miembro de {y}",
30
29
  "error.x privilege required": "¡no autorizado! se requiere el privilegio {x}",
31
30
  "error.role-not-assignable-here": "este dominio no puede asignar los roles {roles}.",
31
+ "error.password-setup-pending": "Esta cuenta espera su contraseña. Establézcala con el enlace de configuración recibido y luego inicie sesión.",
32
+ "error.password-setup-mail-not-sent": "No se pudo enviar el correo a {email}. No se cambió nada: copie el enlace.",
32
33
  "error.password-reset-owner-account": "la contraseña de un propietario o administrador no se restablece al valor predeterminado.",
33
34
  "error.password-reset-reaches-other-domains": "también trabaja en {domains}; este dominio no puede restablecer su contraseña.",
34
35
  "error.x not allowed from this address": "¡no autorizado! {x} no está permitido desde esta dirección",
@@ -212,7 +213,6 @@
212
213
  "text.delete account succeed": "cuenta eliminada correctamente",
213
214
  "text.inactive user": "usuario inactivo",
214
215
  "text.invalid verification token": "token de verificación no válido",
215
- "text.invitation email sent": "correo de invitación enviado",
216
216
  "text.password changed successfully": "contraseña cambiada correctamente",
217
217
  "text.password reset email sent": "si hay una cuenta con esta dirección, le enviamos un correo de restablecimiento de contraseña.",
218
218
  "text.password reset succeed": "contraseña restablecida correctamente",
@@ -9,7 +9,6 @@
9
9
  "error.invalid username": "無効なユーザー名: {username}",
10
10
  "error.mail could not be sent": "メールを送れませんでした. しばらくしてからもう一度お試しください.",
11
11
  "error.ownership required": "権限がありません。$t(term.domain)またはシステムの所有者権限が必要です。",
12
- "error.password should be supported": "初期パスワードまたはデフォルトパスワードがサポートされるべきです",
13
12
  "error.password should match the rule": "パスワードは次の規則を守らなければなりません. {rule}",
14
13
  "error.password used in the past": "過去に使用されたパスワードです.",
15
14
  "error.role-name-stands-inherited": "{name} ロールは {domain} に定義されており、ここからも見えています。この$t(term.domain)に同じ名前で別に立てるつもりの場合のみ作成します。",
@@ -29,6 +28,8 @@
29
28
  "error.x is not a member of y": "{x}は{y}のメンバーではありません.",
30
29
  "error.x privilege required": "権限がありません。{x} 権限が必要です。",
31
30
  "error.role-not-assignable-here": "この$t(term.domain)では付与できないロールです: {roles}",
31
+ "error.password-setup-pending": "パスワード設定待ちのアカウントです。受け取ったパスワード設定リンクでパスワードを設定してからログインしてください。",
32
+ "error.password-setup-mail-not-sent": "{email} にメールを送れませんでした。変更はありません。リンクをコピーして伝えてください。",
32
33
  "error.password-reset-owner-account": "$t(term.domain)のオーナーや管理者のパスワードは初期値にリセットしません。",
33
34
  "error.password-reset-reaches-other-domains": "{domains}でも利用している方のため、この$t(term.domain)ではパスワードをリセットできません。",
34
35
  "error.x not allowed from this address": "権限がありません。このアドレスからは {x} 権限を使えません。",
@@ -52,7 +53,6 @@
52
53
  "text.delete account succeed": "アカウントが削除されました.",
53
54
  "text.inactive user": "非アクティブユーザーです.",
54
55
  "text.invalid verification token": "確認トークンが無効です.",
55
- "text.invitation email sent": "招待メールを送りました.",
56
56
  "text.password changed successfully": "パスワードが正常に変更されました.",
57
57
  "text.password reset email sent": "登録済みのアドレスであれば、パスワード リセットメールを送りました.",
58
58
  "text.password reset succeed": "パスワードが正常にリセットされました.",