@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.
- package/config/config.development.js +2 -4
- package/config/config.production.js +1 -2
- package/dist-client/actions/auth.d.ts +1 -0
- package/dist-client/actions/auth.js +6 -0
- package/dist-client/actions/auth.js.map +1 -1
- package/dist-client/reducers/auth.d.ts +8 -0
- package/dist-client/reducers/auth.js +8 -2
- package/dist-client/reducers/auth.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/constants/error-code.d.ts +1 -0
- package/dist-server/constants/error-code.js +2 -1
- package/dist-server/constants/error-code.js.map +1 -1
- package/dist-server/controllers/auth.d.ts +0 -1
- package/dist-server/controllers/auth.js +0 -1
- package/dist-server/controllers/auth.js.map +1 -1
- package/dist-server/controllers/password-setup.d.ts +33 -0
- package/dist-server/controllers/password-setup.js +77 -0
- package/dist-server/controllers/password-setup.js.map +1 -0
- package/dist-server/controllers/reset-password.d.ts +8 -1
- package/dist-server/controllers/reset-password.js +21 -11
- package/dist-server/controllers/reset-password.js.map +1 -1
- package/dist-server/controllers/signin.js +10 -0
- package/dist-server/controllers/signin.js.map +1 -1
- package/dist-server/index.d.ts +1 -0
- package/dist-server/index.js +2 -0
- package/dist-server/index.js.map +1 -1
- package/dist-server/middlewares/jwt-authenticate-middleware.js +8 -25
- package/dist-server/middlewares/jwt-authenticate-middleware.js.map +1 -1
- package/dist-server/router/auth-public-process-router.js +3 -41
- package/dist-server/router/auth-public-process-router.js.map +1 -1
- package/dist-server/service/access-request/access-request.js +2 -2
- package/dist-server/service/access-request/access-request.js.map +1 -1
- package/dist-server/service/audit/audit-kinds.js +1 -0
- package/dist-server/service/audit/audit-kinds.js.map +1 -1
- package/dist-server/service/audit/audit-subscriber.d.ts +1 -7
- package/dist-server/service/audit/audit-subscriber.js +2 -33
- package/dist-server/service/audit/audit-subscriber.js.map +1 -1
- package/dist-server/service/audit/index.d.ts +1 -1
- package/dist-server/service/index.d.ts +2 -4
- package/dist-server/service/index.js +22 -27
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/open-fields.js +0 -2
- package/dist-server/service/open-fields.js.map +1 -1
- package/dist-server/service/user/user-mutation.d.ts +27 -6
- package/dist-server/service/user/user-mutation.js +26 -114
- package/dist-server/service/user/user-mutation.js.map +1 -1
- package/dist-server/service/user/user-query.d.ts +3 -4
- package/dist-server/service/user/user-query.js +7 -32
- package/dist-server/service/user/user-query.js.map +1 -1
- package/dist-server/service/user/user-types.d.ts +6 -2
- package/dist-server/service/user/user-types.js +20 -9
- package/dist-server/service/user/user-types.js.map +1 -1
- package/dist-server/service/user/user.js +17 -8
- package/dist-server/service/user/user.js.map +1 -1
- package/dist-server/service/verification-token/verification-token.d.ts +1 -0
- package/dist-server/service/verification-token/verification-token.js +1 -0
- package/dist-server/service/verification-token/verification-token.js.map +1 -1
- package/dist-server/templates/reset-password-email.d.ts +10 -3
- package/dist-server/templates/reset-password-email.js +8 -4
- package/dist-server/templates/reset-password-email.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/dist-server/utils/verification-token-rule.js +3 -1
- package/dist-server/utils/verification-token-rule.js.map +1 -1
- package/package.json +2 -2
- package/tests/add-user-gates.test.ts +34 -0
- package/tests/list-inheritance-db.test.ts +0 -15
- package/tests/lock-recovery.test.ts +2 -2
- package/tests/open-doors.test.ts +2 -5
- package/tests/password-reset-rule-db.test.ts +17 -16
- package/tests/password-setup-db.test.ts +213 -0
- package/tests/picture-reducer.test.ts +25 -0
- package/tests/verification-token-db.test.ts +12 -7
- package/translations/en.json +2 -2
- package/translations/es.json +2 -2
- package/translations/ja.json +2 -2
- package/translations/ko.json +2 -2
- package/translations/ms.json +2 -2
- package/translations/zh.json +2 -2
- package/dist-server/controllers/invitation.d.ts +0 -75
- package/dist-server/controllers/invitation.js +0 -195
- package/dist-server/controllers/invitation.js.map +0 -1
- package/dist-server/controllers/utils/make-invitation-token.d.ts +0 -1
- package/dist-server/controllers/utils/make-invitation-token.js +0 -9
- package/dist-server/controllers/utils/make-invitation-token.js.map +0 -1
- package/dist-server/controllers/utils/save-invitation-token.d.ts +0 -5
- package/dist-server/controllers/utils/save-invitation-token.js +0 -12
- package/dist-server/controllers/utils/save-invitation-token.js.map +0 -1
- package/dist-server/service/invitation/index.d.ts +0 -5
- package/dist-server/service/invitation/index.js +0 -9
- package/dist-server/service/invitation/index.js.map +0 -1
- package/dist-server/service/invitation/invitation-mutation.d.ts +0 -20
- package/dist-server/service/invitation/invitation-mutation.js +0 -109
- package/dist-server/service/invitation/invitation-mutation.js.map +0 -1
- package/dist-server/service/invitation/invitation-query.d.ts +0 -23
- package/dist-server/service/invitation/invitation-query.js +0 -89
- package/dist-server/service/invitation/invitation-query.js.map +0 -1
- package/dist-server/service/invitation/invitation-types.d.ts +0 -5
- package/dist-server/service/invitation/invitation-types.js +0 -21
- package/dist-server/service/invitation/invitation-types.js.map +0 -1
- package/dist-server/service/invitation/invitation.d.ts +0 -26
- package/dist-server/service/invitation/invitation.js +0 -142
- package/dist-server/service/invitation/invitation.js.map +0 -1
- package/dist-server/templates/invitation-email.d.ts +0 -6
- package/dist-server/templates/invitation-email.js +0 -82
- package/dist-server/templates/invitation-email.js.map +0 -1
- package/dist-server/utils/invitation-state.d.ts +0 -52
- package/dist-server/utils/invitation-state.js +0 -76
- package/dist-server/utils/invitation-state.js.map +0 -1
- package/tests/invitation-db.test.ts +0 -416
- package/tests/invitation-route.test.ts +0 -116
- 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
|
|
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":";;;
|
|
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.
|
|
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": "
|
|
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
|
})
|
package/tests/open-doors.test.ts
CHANGED
|
@@ -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
|
-
*
|
|
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'
|
|
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
|
-
*
|
|
2
|
+
* Whose password a domain may take back - by issuing a password setup link (ADR-0161).
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
* of
|
|
7
|
-
*
|
|
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 {
|
|
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().
|
|
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().
|
|
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
|
|
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.
|
|
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(
|
|
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({
|
|
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
|
|
package/translations/en.json
CHANGED
|
@@ -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",
|
package/translations/es.json
CHANGED
|
@@ -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",
|
package/translations/ja.json
CHANGED
|
@@ -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": "パスワードが正常にリセットされました.",
|