@things-factory/auth-base 10.1.278 → 10.1.280
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/package.json +24 -7
- package/CHANGELOG.md +0 -55
- package/dist-client/actions/auth.js.map +0 -1
- package/dist-client/apply-domain-theme.js.map +0 -1
- package/dist-client/auth.js.map +0 -1
- package/dist-client/bootstrap.js.map +0 -1
- package/dist-client/directive/privileged.js.map +0 -1
- package/dist-client/domain-required.js.map +0 -1
- package/dist-client/index.js.map +0 -1
- package/dist-client/profiled.js.map +0 -1
- package/dist-client/reducers/auth.js.map +0 -1
- package/dist-client/tsconfig.tsbuildinfo +0 -1
- package/dist-client/verify-webauthn.js.map +0 -1
- package/dist-server/ai-entry/access-assistant.js.map +0 -1
- package/dist-server/ai-entry/access-entry-targets.js.map +0 -1
- package/dist-server/ai-entry/entry-target-shape.js.map +0 -1
- package/dist-server/constants/error-code.js.map +0 -1
- package/dist-server/constants/error-message.js.map +0 -1
- package/dist-server/constants/max-age.js.map +0 -1
- package/dist-server/controllers/access-request.js.map +0 -1
- package/dist-server/controllers/account-mail.js.map +0 -1
- package/dist-server/controllers/auth.js.map +0 -1
- package/dist-server/controllers/change-pwd.js.map +0 -1
- package/dist-server/controllers/confirm-identity.js.map +0 -1
- package/dist-server/controllers/delete-user.js.map +0 -1
- package/dist-server/controllers/passkey.js.map +0 -1
- package/dist-server/controllers/password-setup.js.map +0 -1
- package/dist-server/controllers/profile.js.map +0 -1
- package/dist-server/controllers/reset-password.js.map +0 -1
- package/dist-server/controllers/signin.js.map +0 -1
- package/dist-server/controllers/signup.js.map +0 -1
- package/dist-server/controllers/unlock-user.js.map +0 -1
- package/dist-server/controllers/utils/find-verification-token.js.map +0 -1
- package/dist-server/controllers/utils/make-verification-token.js.map +0 -1
- package/dist-server/controllers/utils/password-rule.js.map +0 -1
- package/dist-server/controllers/utils/save-verification-token.js.map +0 -1
- package/dist-server/controllers/verification.js.map +0 -1
- package/dist-server/errors/auth-error.js.map +0 -1
- package/dist-server/errors/index.js.map +0 -1
- package/dist-server/errors/user-domain-not-match-error.js.map +0 -1
- package/dist-server/index.js.map +0 -1
- package/dist-server/middlewares/audit-scope-middleware.js.map +0 -1
- package/dist-server/middlewares/authenticate-401-middleware.js.map +0 -1
- package/dist-server/middlewares/bypass-signin-middleware.js.map +0 -1
- package/dist-server/middlewares/domain-authenticate-middleware.js.map +0 -1
- package/dist-server/middlewares/graphql-authenticate-middleware.js.map +0 -1
- package/dist-server/middlewares/index.js.map +0 -1
- package/dist-server/middlewares/jwt-authenticate-middleware.js.map +0 -1
- package/dist-server/middlewares/signin-middleware.js.map +0 -1
- package/dist-server/middlewares/verify-recaptcha-middleware.js.map +0 -1
- package/dist-server/migrations/1548206416130-SeedUser.js.map +0 -1
- package/dist-server/migrations/1566805283882-SeedPrivilege.js.map +0 -1
- package/dist-server/migrations/1745000000000-MigrateDomainOwnersToTable.js.map +0 -1
- package/dist-server/migrations/index.js.map +0 -1
- package/dist-server/role-tasks.js.map +0 -1
- package/dist-server/router/auth-checkin-router.js.map +0 -1
- package/dist-server/router/auth-private-process-router.js.map +0 -1
- package/dist-server/router/auth-public-process-router.js.map +0 -1
- package/dist-server/router/auth-signin-router.js.map +0 -1
- package/dist-server/router/auth-signup-router.js.map +0 -1
- package/dist-server/router/index.js.map +0 -1
- package/dist-server/router/oauth2/index.js.map +0 -1
- package/dist-server/router/oauth2/oauth2-authorize-router.js.map +0 -1
- package/dist-server/router/oauth2/oauth2-router.js.map +0 -1
- package/dist-server/router/oauth2/oauth2-server.js.map +0 -1
- package/dist-server/router/oauth2/passport-oauth2-client-password.js.map +0 -1
- package/dist-server/router/oauth2/passport-refresh-token.js.map +0 -1
- package/dist-server/router/path-base-domain-router.js.map +0 -1
- package/dist-server/router/site-root-router.js.map +0 -1
- package/dist-server/router/webauthn-router.js.map +0 -1
- package/dist-server/routes.js.map +0 -1
- package/dist-server/service/access-request/access-request-mutation.js.map +0 -1
- package/dist-server/service/access-request/access-request-query.js.map +0 -1
- package/dist-server/service/access-request/access-request-types.js.map +0 -1
- package/dist-server/service/access-request/access-request.js.map +0 -1
- package/dist-server/service/access-request/index.js.map +0 -1
- package/dist-server/service/app-binding/app-binding-mutation.js.map +0 -1
- package/dist-server/service/app-binding/app-binding-query.js.map +0 -1
- package/dist-server/service/app-binding/app-binding-types.js.map +0 -1
- package/dist-server/service/app-binding/app-binding.js.map +0 -1
- package/dist-server/service/app-binding/index.js.map +0 -1
- package/dist-server/service/appliance/appliance-mutation.js.map +0 -1
- package/dist-server/service/appliance/appliance-query.js.map +0 -1
- package/dist-server/service/appliance/appliance-types.js.map +0 -1
- package/dist-server/service/appliance/appliance.js.map +0 -1
- package/dist-server/service/appliance/index.js.map +0 -1
- package/dist-server/service/application/application-mutation.js.map +0 -1
- package/dist-server/service/application/application-query.js.map +0 -1
- package/dist-server/service/application/application-types.js.map +0 -1
- package/dist-server/service/application/application.js.map +0 -1
- package/dist-server/service/application/index.js.map +0 -1
- package/dist-server/service/audit/audit-action.js.map +0 -1
- package/dist-server/service/audit/audit-kinds.js.map +0 -1
- package/dist-server/service/audit/audit-query.js.map +0 -1
- package/dist-server/service/audit/audit-recorder.js.map +0 -1
- package/dist-server/service/audit/audit-request.js.map +0 -1
- package/dist-server/service/audit/audit-retention.js.map +0 -1
- package/dist-server/service/audit/audit-subscriber.js.map +0 -1
- package/dist-server/service/audit/audit-target.js.map +0 -1
- package/dist-server/service/audit/audit-types.js.map +0 -1
- package/dist-server/service/audit/audit-work.js.map +0 -1
- package/dist-server/service/audit/index.js.map +0 -1
- package/dist-server/service/auth-provider/auth-provider-mutation.js.map +0 -1
- package/dist-server/service/auth-provider/auth-provider-parameter-spec.js.map +0 -1
- package/dist-server/service/auth-provider/auth-provider-query.js.map +0 -1
- package/dist-server/service/auth-provider/auth-provider-type.js.map +0 -1
- package/dist-server/service/auth-provider/auth-provider.js.map +0 -1
- package/dist-server/service/auth-provider/index.js.map +0 -1
- package/dist-server/service/domain-generator/domain-generator-mutation.js.map +0 -1
- package/dist-server/service/domain-generator/domain-generator-types.js.map +0 -1
- package/dist-server/service/domain-generator/index.js.map +0 -1
- package/dist-server/service/domain-link/domain-link-mutation.js.map +0 -1
- package/dist-server/service/domain-link/domain-link-query.js.map +0 -1
- package/dist-server/service/domain-link/domain-link-types.js.map +0 -1
- package/dist-server/service/domain-link/domain-link.js.map +0 -1
- package/dist-server/service/domain-link/index.js.map +0 -1
- package/dist-server/service/domain-owner/domain-owner-mutation.js.map +0 -1
- package/dist-server/service/domain-owner/domain-owner-query.js.map +0 -1
- package/dist-server/service/domain-owner/domain-owner.js.map +0 -1
- package/dist-server/service/domain-owner/index.js.map +0 -1
- package/dist-server/service/granted-role/granted-role-mutation.js.map +0 -1
- package/dist-server/service/granted-role/granted-role-query.js.map +0 -1
- package/dist-server/service/granted-role/granted-role.js.map +0 -1
- package/dist-server/service/granted-role/index.js.map +0 -1
- package/dist-server/service/identity-confirmation/identity-confirmation.js.map +0 -1
- package/dist-server/service/index.js.map +0 -1
- package/dist-server/service/open-fields.js.map +0 -1
- package/dist-server/service/partner/index.js.map +0 -1
- package/dist-server/service/partner/partner-mutation.js.map +0 -1
- package/dist-server/service/partner/partner-query.js.map +0 -1
- package/dist-server/service/partner/partner-types.js.map +0 -1
- package/dist-server/service/partner/partner.js.map +0 -1
- package/dist-server/service/password-history/index.js.map +0 -1
- package/dist-server/service/password-history/password-history.js.map +0 -1
- package/dist-server/service/privilege/index.js.map +0 -1
- package/dist-server/service/privilege/open-field-rule.js.map +0 -1
- package/dist-server/service/privilege/privilege-axis.js.map +0 -1
- package/dist-server/service/privilege/privilege-deprecation.js.map +0 -1
- package/dist-server/service/privilege/privilege-directive.js.map +0 -1
- package/dist-server/service/privilege/privilege-mutation.js.map +0 -1
- package/dist-server/service/privilege/privilege-query.js.map +0 -1
- package/dist-server/service/privilege/privilege-rejection.js.map +0 -1
- package/dist-server/service/privilege/privilege-sync.js.map +0 -1
- package/dist-server/service/privilege/privilege-types.js.map +0 -1
- package/dist-server/service/privilege/privilege.js.map +0 -1
- package/dist-server/service/role/index.js.map +0 -1
- package/dist-server/service/role/role-mutation.js.map +0 -1
- package/dist-server/service/role/role-privilege-query.js.map +0 -1
- package/dist-server/service/role/role-query.js.map +0 -1
- package/dist-server/service/role/role-types.js.map +0 -1
- package/dist-server/service/role/role.js.map +0 -1
- package/dist-server/service/role-template/index.js.map +0 -1
- package/dist-server/service/role-template/role-task.js.map +0 -1
- package/dist-server/service/role-template/role-template-gap-resolver.js.map +0 -1
- package/dist-server/service/role-template/role-template-mutation.js.map +0 -1
- package/dist-server/service/role-template/role-template-query.js.map +0 -1
- package/dist-server/service/role-template/role-template-types.js.map +0 -1
- package/dist-server/service/role-template/role-template.js.map +0 -1
- package/dist-server/service/user/domain-query.js.map +0 -1
- package/dist-server/service/user/index.js.map +0 -1
- package/dist-server/service/user/user-mutation.js.map +0 -1
- package/dist-server/service/user/user-query.js.map +0 -1
- package/dist-server/service/user/user-types.js.map +0 -1
- package/dist-server/service/user/user.js.map +0 -1
- package/dist-server/service/users-auth-providers/index.js.map +0 -1
- package/dist-server/service/users-auth-providers/users-auth-providers.js.map +0 -1
- package/dist-server/service/verification-token/index.js.map +0 -1
- package/dist-server/service/verification-token/verification-token.js.map +0 -1
- package/dist-server/service/web-auth-credential/index.js.map +0 -1
- package/dist-server/service/web-auth-credential/web-auth-credential-mutation.js.map +0 -1
- package/dist-server/service/web-auth-credential/web-auth-credential-query.js.map +0 -1
- package/dist-server/service/web-auth-credential/web-auth-credential.js.map +0 -1
- package/dist-server/tsconfig.tsbuildinfo +0 -1
- package/dist-server/types.js.map +0 -1
- package/dist-server/utils/accepts.js.map +0 -1
- package/dist-server/utils/access-request-review.js.map +0 -1
- package/dist-server/utils/access-token-cookie.js.map +0 -1
- package/dist-server/utils/account-lock-rule.js.map +0 -1
- package/dist-server/utils/assignable-roles.js.map +0 -1
- package/dist-server/utils/build-auth-url.js.map +0 -1
- package/dist-server/utils/can-user-checkin-domain.js.map +0 -1
- package/dist-server/utils/check-permission.js.map +0 -1
- package/dist-server/utils/check-user-belongs-domain.js.map +0 -1
- package/dist-server/utils/check-user-has-role.js.map +0 -1
- package/dist-server/utils/checkin-domain-facts.js.map +0 -1
- package/dist-server/utils/checkin-domain-rule.js.map +0 -1
- package/dist-server/utils/credential-at-rest.js.map +0 -1
- package/dist-server/utils/credential-features.js.map +0 -1
- package/dist-server/utils/credential-serial-rule.js.map +0 -1
- package/dist-server/utils/domain-ownership.js.map +0 -1
- package/dist-server/utils/encrypt-state.js.map +0 -1
- package/dist-server/utils/get-aes-256-key.js.map +0 -1
- package/dist-server/utils/get-domain-users.js.map +0 -1
- package/dist-server/utils/get-secret.js.map +0 -1
- package/dist-server/utils/get-user-domains.js.map +0 -1
- package/dist-server/utils/lock-duration.js.map +0 -1
- package/dist-server/utils/password-reset-rule.js.map +0 -1
- package/dist-server/utils/profile-user.js.map +0 -1
- package/dist-server/utils/refuse-role-name.js.map +0 -1
- package/dist-server/utils/role-name-standing.js.map +0 -1
- package/dist-server/utils/same-origin-redirect.js.map +0 -1
- package/dist-server/utils/verification-token-rule.js.map +0 -1
- package/spec/unit/checkin-domain.spec.ts +0 -307
- package/spec/unit/checkin-privilege.spec.ts +0 -205
- package/spec/unit/domain-owner-entity.spec.ts +0 -179
- package/spec/unit/domain-owner-granted.spec.ts +0 -215
- package/spec/unit/domain-owner-migration.spec.ts +0 -236
- package/spec/unit/domain-owner-mutation.spec.ts +0 -337
- package/spec/unit/domain-ownership.spec.ts +0 -188
- package/tests/access-assistant-db.test.ts +0 -155
- package/tests/access-entry-targets-db.test.ts +0 -176
- package/tests/access-request-db.test.ts +0 -394
- package/tests/access-request-pg.test.ts +0 -83
- package/tests/access-request-review.test.ts +0 -193
- package/tests/account-lock-db.test.ts +0 -216
- package/tests/account-lock-rule.test.ts +0 -141
- package/tests/account-mail.test.ts +0 -69
- package/tests/activate-page-case.test.ts +0 -53
- package/tests/add-user-gates.test.ts +0 -34
- package/tests/app-binding-db.test.ts +0 -210
- package/tests/app-binding-delete-db.test.ts +0 -188
- package/tests/appliance-credential-state-db.test.ts +0 -207
- package/tests/appliance-delete-db.test.ts +0 -119
- package/tests/appliance-revoke-db.test.ts +0 -294
- package/tests/appliance-schema.test.ts +0 -129
- package/tests/application-delete-db.test.ts +0 -190
- package/tests/application-token-payload-db.test.ts +0 -185
- package/tests/assignable-roles-db.test.ts +0 -219
- package/tests/audit-db.test.ts +0 -410
- package/tests/audit-work.test.ts +0 -58
- package/tests/auth-base-role-tasks.test.ts +0 -58
- package/tests/checkin-domain-rule.test.ts +0 -128
- package/tests/checkin-privilege-seam-db.test.ts +0 -158
- package/tests/compiled.ts +0 -41
- package/tests/confirm-identity-db.test.ts +0 -85
- package/tests/credential-at-rest-db.test.ts +0 -246
- package/tests/credential-features-off-db.test.ts +0 -189
- package/tests/credential-serial-rule.test.ts +0 -84
- package/tests/declare-privileges.test.ts +0 -29
- package/tests/domain-assignees-db.test.ts +0 -95
- package/tests/domain-inheritance-sentinel.test.ts +0 -93
- package/tests/domain-required.test.ts +0 -127
- package/tests/has-privilege-db.test.ts +0 -198
- package/tests/identity-confirmation.test.ts +0 -146
- package/tests/list-inheritance-db.test.ts +0 -127
- package/tests/lock-recovery.test.ts +0 -206
- package/tests/mail-request-answers-db.test.ts +0 -142
- package/tests/open-doors.test.ts +0 -121
- package/tests/open-fields-enforcement.test.ts +0 -215
- package/tests/open-fields-framework.test.ts +0 -212
- package/tests/open-redirect-db.test.ts +0 -143
- package/tests/passkey-authenticator.ts +0 -104
- package/tests/passkey-db.test.ts +0 -295
- package/tests/passkey-signin-route-db.test.ts +0 -139
- package/tests/password-hashing.test.ts +0 -131
- package/tests/password-persistence.test.ts +0 -147
- package/tests/password-reset-rule-db.test.ts +0 -159
- package/tests/password-setup-db.test.ts +0 -215
- package/tests/permission-gate.test.ts +0 -228
- package/tests/picture-reducer.test.ts +0 -25
- package/tests/privilege-axis.test.ts +0 -157
- package/tests/privilege-deprecation.test.ts +0 -65
- package/tests/privilege-directive.test.ts +0 -332
- package/tests/privilege-rejection.test.ts +0 -104
- package/tests/privilege-sync-db.test.ts +0 -58
- package/tests/profile-user.test.ts +0 -24
- package/tests/refusal-cause.test.ts +0 -80
- package/tests/refusals-have-korean.test.ts +0 -24
- package/tests/request-holds.test.ts +0 -53
- package/tests/role-inheritance-db.test.ts +0 -286
- package/tests/role-mutation-db.test.ts +0 -293
- package/tests/role-name-lookup-sentinel.test.ts +0 -79
- package/tests/role-name-standing.test.ts +0 -83
- package/tests/role-privileges-db.test.ts +0 -252
- package/tests/role-task.test.ts +0 -168
- package/tests/role-template-gap-db.test.ts +0 -268
- package/tests/role-template-seed-db.test.ts +0 -180
- package/tests/role-template.test.ts +0 -162
- package/tests/role-users-gate.test.ts +0 -22
- package/tests/signin-policy.test.ts +0 -200
- package/tests/signup-duplicate-db.test.ts +0 -68
- package/tests/token-issuance.test.ts +0 -244
- package/tests/user-roles-field-db.test.ts +0 -134
- package/tests/verification-token-db.test.ts +0 -285
|
@@ -1,193 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The access decision review - pure (ADR-0103, docs/design/access-request.md §6-1).
|
|
3
|
-
*
|
|
4
|
-
* The list screen, the AI 입력 draft card and the decision door all read this one function, so what it says
|
|
5
|
-
* is what every one of them says. Each code is held here on its own.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import {
|
|
9
|
-
effectiveState,
|
|
10
|
-
expiryFrom,
|
|
11
|
-
isBlocked,
|
|
12
|
-
reviewAccessDecision,
|
|
13
|
-
smallestRole,
|
|
14
|
-
type DecisionFacts
|
|
15
|
-
} from '../server/utils/access-request-review.js'
|
|
16
|
-
|
|
17
|
-
const NOW = new Date('2026-10-02T09:00:00Z')
|
|
18
|
-
const ASKED = { category: 'warehouse', privilege: 'mutation' }
|
|
19
|
-
const SEE = { category: 'warehouse', privilege: 'query' }
|
|
20
|
-
const GRANT_ROLES = { category: 'user', privilege: 'mutation' }
|
|
21
|
-
|
|
22
|
-
function facts(over: Partial<DecisionFacts> = {}): DecisionFacts {
|
|
23
|
-
return {
|
|
24
|
-
request: { state: 'PENDING', requesterId: 'kim', ...ASKED, expiresAt: expiryFrom(NOW) },
|
|
25
|
-
deciderId: 'park',
|
|
26
|
-
decision: 'GRANT',
|
|
27
|
-
reason: null,
|
|
28
|
-
role: { id: 'r-inbound', name: 'inbound', privileges: [ASKED] },
|
|
29
|
-
requester: { active: true, inDomain: true },
|
|
30
|
-
requesterHoldsIt: false,
|
|
31
|
-
candidates: [{ id: 'r-inbound', name: 'inbound', privilegeCount: 1 }],
|
|
32
|
-
deciderPrivileges: [ASKED, SEE, GRANT_ROLES],
|
|
33
|
-
deciderIsOwner: false,
|
|
34
|
-
deniedBefore: null,
|
|
35
|
-
now: NOW,
|
|
36
|
-
...over
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
const codes = (f: DecisionFacts) => reviewAccessDecision(f).map(issue => `${issue.blocking ? '⛔' : '⚠'}${issue.code}`)
|
|
41
|
-
|
|
42
|
-
describe('a clean grant', () => {
|
|
43
|
-
it('★ the smallest role that holds exactly what was asked passes with nothing to say', () => {
|
|
44
|
-
expect(reviewAccessDecision(facts())).toEqual([])
|
|
45
|
-
})
|
|
46
|
-
})
|
|
47
|
-
|
|
48
|
-
describe('what keeps a decision from being made', () => {
|
|
49
|
-
it('★ no such request', () => {
|
|
50
|
-
expect(codes(facts({ request: null }))).toEqual(['⛔access-request-not-found'])
|
|
51
|
-
})
|
|
52
|
-
|
|
53
|
-
it('★ decided, withdrawn or expired already - another person may have decided a moment ago', () => {
|
|
54
|
-
for (const state of ['GRANTED', 'DENIED', 'WITHDRAWN', 'EXPIRED'] as const) {
|
|
55
|
-
expect(codes(facts({ request: { ...facts().request!, state } }))).toContain('⛔request-not-pending')
|
|
56
|
-
}
|
|
57
|
-
})
|
|
58
|
-
|
|
59
|
-
it('★ a pending request past its time is expired whatever the column says', () => {
|
|
60
|
-
const late = facts({ request: { ...facts().request!, expiresAt: new Date(NOW.getTime() - 1) } })
|
|
61
|
-
expect(codes(late)).toContain('⛔request-not-pending')
|
|
62
|
-
})
|
|
63
|
-
|
|
64
|
-
it('★ the decider is the requester', () => {
|
|
65
|
-
expect(codes(facts({ deciderId: 'kim' }))).toContain('⛔own-request')
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
it('★ the requester is locked, inactive or gone from the domain', () => {
|
|
69
|
-
expect(codes(facts({ requester: { active: false, inDomain: true } }))).toContain('⛔requester-unavailable')
|
|
70
|
-
expect(codes(facts({ requester: { active: true, inDomain: false } }))).toContain('⛔requester-unavailable')
|
|
71
|
-
expect(codes(facts({ requester: null }))).toContain('⛔requester-unavailable')
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
it('★ a denial without a reason', () => {
|
|
75
|
-
expect(codes(facts({ decision: 'DENY', reason: ' ', role: null }))).toEqual(['⛔deny-needs-reason'])
|
|
76
|
-
expect(codes(facts({ decision: 'DENY', reason: 'IT only', role: null }))).toEqual([])
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
it('★ the requester holds it already', () => {
|
|
80
|
-
expect(codes(facts({ requesterHoldsIt: true }))).toContain('⛔already-holds')
|
|
81
|
-
})
|
|
82
|
-
|
|
83
|
-
it('★ no role chosen, or one this domain cannot assign', () => {
|
|
84
|
-
expect(codes(facts({ role: null }))).toEqual(['⛔role-required'])
|
|
85
|
-
expect(codes(facts({ role: null, roleNamedButNotAssignable: true }))).toEqual(['⛔role-not-assignable'])
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
it('★ the role does not hold the asked privilege', () => {
|
|
89
|
-
expect(codes(facts({ role: { id: 'r-viewer', name: 'viewer', privileges: [SEE] } }))).toEqual(['⛔role-does-not-grant'])
|
|
90
|
-
})
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
describe('what the person deciding is told', () => {
|
|
94
|
-
const manager = { id: 'r-manager', name: 'manager', privileges: [ASKED, SEE, GRANT_ROLES] }
|
|
95
|
-
const candidates = [
|
|
96
|
-
{ id: 'r-inbound', name: 'inbound', privilegeCount: 1 },
|
|
97
|
-
{ id: 'r-manager', name: 'manager', privilegeCount: 3 }
|
|
98
|
-
]
|
|
99
|
-
|
|
100
|
-
it('★ the role gives more than was asked - named', () => {
|
|
101
|
-
const issues = reviewAccessDecision(facts({ role: manager, candidates }))
|
|
102
|
-
expect(issues.find(i => i.code === 'role-grants-more')).toMatchObject({
|
|
103
|
-
blocking: false,
|
|
104
|
-
params: { count: 2, privileges: 'warehouse:query, user:mutation' }
|
|
105
|
-
})
|
|
106
|
-
})
|
|
107
|
-
|
|
108
|
-
it('★ the role hands on the power to give roles', () => {
|
|
109
|
-
expect(codes(facts({ role: manager, candidates }))).toContain('⚠role-grants-admin')
|
|
110
|
-
})
|
|
111
|
-
|
|
112
|
-
it('asking for the role-giving power itself is not warned as a side effect', () => {
|
|
113
|
-
const asked = facts({
|
|
114
|
-
request: { ...facts().request!, ...GRANT_ROLES },
|
|
115
|
-
role: { id: 'r-admin', name: 'admin', privileges: [GRANT_ROLES] },
|
|
116
|
-
candidates: [{ id: 'r-admin', name: 'admin', privilegeCount: 1 }]
|
|
117
|
-
})
|
|
118
|
-
expect(codes(asked)).not.toContain('⚠role-grants-admin')
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
it('★ a smaller role grants it too - its name', () => {
|
|
122
|
-
const issues = reviewAccessDecision(facts({ role: manager, candidates }))
|
|
123
|
-
expect(issues.find(i => i.code === 'smaller-role-exists')).toMatchObject({ params: { role: 'inbound', count: 1 } })
|
|
124
|
-
})
|
|
125
|
-
|
|
126
|
-
it('★ the role gives what the decider does not have - told, not refused (Q6)', () => {
|
|
127
|
-
const issues = reviewAccessDecision(facts({ role: manager, candidates, deciderPrivileges: [ASKED] }))
|
|
128
|
-
expect(issues.find(i => i.code === 'decider-lacks-it')).toMatchObject({ blocking: false, params: { privileges: 'warehouse:query, user:mutation' } })
|
|
129
|
-
expect(isBlocked(issues)).toBe(false)
|
|
130
|
-
})
|
|
131
|
-
|
|
132
|
-
it('an owner is not told they lack anything', () => {
|
|
133
|
-
expect(codes(facts({ role: manager, candidates, deciderPrivileges: [], deciderIsOwner: true }))).not.toContain('⚠decider-lacks-it')
|
|
134
|
-
})
|
|
135
|
-
|
|
136
|
-
it('★ denied before - by whom, when, why', () => {
|
|
137
|
-
const issues = reviewAccessDecision(facts({ deniedBefore: { by: 'Lee', at: '2026-09-01T00:00:00Z', reason: 'IT only' } }))
|
|
138
|
-
expect(issues).toEqual([
|
|
139
|
-
{ code: 'denied-before', blocking: false, field: 'request', params: { by: 'Lee', at: '2026-09-01T00:00:00.000Z', reason: 'IT only' } }
|
|
140
|
-
])
|
|
141
|
-
})
|
|
142
|
-
})
|
|
143
|
-
|
|
144
|
-
describe('helpers', () => {
|
|
145
|
-
it('the smallest role, ties by name - the same answer every time', () => {
|
|
146
|
-
expect(smallestRole([{ name: 'b', privilegeCount: 2 }, { name: 'a', privilegeCount: 2 }, { name: 'c', privilegeCount: 5 }])?.name).toBe('a')
|
|
147
|
-
expect(smallestRole([])).toBeUndefined()
|
|
148
|
-
})
|
|
149
|
-
|
|
150
|
-
it('a request waits fourteen days', () => {
|
|
151
|
-
expect(expiryFrom(NOW).toISOString()).toBe('2026-10-16T09:00:00.000Z')
|
|
152
|
-
expect(effectiveState('PENDING', null, NOW)).toBe('PENDING')
|
|
153
|
-
expect(effectiveState('GRANTED', new Date(0), NOW)).toBe('GRANTED')
|
|
154
|
-
})
|
|
155
|
-
})
|
|
156
|
-
|
|
157
|
-
describe('the parent domain\'s role (ADR-0103 decision 8)', () => {
|
|
158
|
-
it('★ blocks when the decider does not decide in the parent, passes when they do', () => {
|
|
159
|
-
expect(codes(facts({ roleIsParents: true, deciderMayChangeParentRoles: false }))).toContain('⛔role-belongs-to-parent')
|
|
160
|
-
expect(codes(facts({ roleIsParents: true, deciderMayChangeParentRoles: true }))).not.toContain('⛔role-belongs-to-parent')
|
|
161
|
-
expect(codes(facts({ roleIsParents: false }))).not.toContain('⛔role-belongs-to-parent')
|
|
162
|
-
})
|
|
163
|
-
})
|
|
164
|
-
|
|
165
|
-
describe('the task asked for (ADR-0103 decision 9)', () => {
|
|
166
|
-
const task = { id: 'warehouse.keeping', privileges: [ASKED, SEE] }
|
|
167
|
-
|
|
168
|
-
it('★ a role short of the task asked is told which privileges it lacks - not refused', () => {
|
|
169
|
-
const issues = reviewAccessDecision(facts({ task }))
|
|
170
|
-
expect(issues.find(issue => issue.code === 'role-lacks-task')).toEqual({
|
|
171
|
-
code: 'role-lacks-task',
|
|
172
|
-
blocking: false,
|
|
173
|
-
field: 'role',
|
|
174
|
-
params: { role: 'inbound', task: 'warehouse.keeping', privileges: 'warehouse:query' }
|
|
175
|
-
})
|
|
176
|
-
expect(isBlocked(issues)).toBe(false)
|
|
177
|
-
})
|
|
178
|
-
|
|
179
|
-
it('★ says nothing when no task was asked or the task is gone', () => {
|
|
180
|
-
expect(codes(facts({ task: null }))).not.toContain('⚠role-lacks-task')
|
|
181
|
-
expect(codes(facts())).not.toContain('⚠role-lacks-task')
|
|
182
|
-
})
|
|
183
|
-
|
|
184
|
-
it('★ a smaller role that drops the task is not proposed over a role holding it', () => {
|
|
185
|
-
const keeper = { id: 'r-keeper', name: 'keeper', privileges: [ASKED, SEE, GRANT_ROLES] }
|
|
186
|
-
const candidates = [
|
|
187
|
-
{ id: 'r-keeper', name: 'keeper', privilegeCount: 3, holdsTask: true },
|
|
188
|
-
{ id: 'r-inbound', name: 'inbound', privilegeCount: 1, holdsTask: false }
|
|
189
|
-
]
|
|
190
|
-
expect(codes(facts({ task, role: keeper, candidates }))).not.toContain('⚠smaller-role-exists')
|
|
191
|
-
expect(smallestRole(candidates)?.name).toBe('keeper')
|
|
192
|
-
})
|
|
193
|
-
})
|
|
@@ -1,216 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 잠금이 스스로 풀리는 것 — 실 경로(`signin` · `checkAuth`)를 태워 본다.
|
|
3
|
-
*
|
|
4
|
-
* `account-lock-rule.test.ts` 는 규칙만 본다. 규칙이 옳아도 **부르는 자리**가 틀리면 계정은
|
|
5
|
-
* 영구히 잠긴 채로 남는다. 그래서 여기서는 실제 컨트롤러를 태우고 **행을 다시 읽는다.**
|
|
6
|
-
*
|
|
7
|
-
* 시계는 넘길 수 없으므로(컨트롤러가 안에서 `new Date()` 를 읽는다) **`lockedAt` 을 과거로
|
|
8
|
-
* 옮겨** 시간이 지난 상태를 만든다.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import { config } from '@things-factory/env'
|
|
12
|
-
|
|
13
|
-
import { closeAuthDatabase, Domain, getRepository, openAuthDatabase, resetAuthDatabase } from './db'
|
|
14
|
-
import { loadCompiled } from './compiled'
|
|
15
|
-
|
|
16
|
-
const { User } = loadCompiled('service/user/user')
|
|
17
|
-
const { signin } = loadCompiled('controllers/signin')
|
|
18
|
-
|
|
19
|
-
const PASSWORD = 'Passw0rd!'
|
|
20
|
-
const WRONG = 'Wr0ngPass!'
|
|
21
|
-
|
|
22
|
-
/** 기본값은 10분이다 — 설정이 없을 때의 값(`utils/lock-duration.ts`). */
|
|
23
|
-
const DEFAULT_MINUTES = 10
|
|
24
|
-
|
|
25
|
-
let domain: any
|
|
26
|
-
|
|
27
|
-
async function seedUser(overrides: any = {}) {
|
|
28
|
-
const salt = User.generateSalt()
|
|
29
|
-
|
|
30
|
-
return await getRepository(User).save({
|
|
31
|
-
username: 'kim',
|
|
32
|
-
email: 'kim@example.com',
|
|
33
|
-
name: 'Kim',
|
|
34
|
-
salt,
|
|
35
|
-
password: User.encode(PASSWORD, salt),
|
|
36
|
-
status: 'activated',
|
|
37
|
-
domains: [domain],
|
|
38
|
-
...overrides
|
|
39
|
-
})
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const attempt = (password: string) => signin({ username: 'kim', password })
|
|
43
|
-
|
|
44
|
-
const reload = (id: string) => getRepository(User).findOneBy({ id })
|
|
45
|
-
|
|
46
|
-
/** 잠긴 시각을 `minutes` 분 전으로 옮긴다 — 시간이 지난 상태를 만드는 방법. */
|
|
47
|
-
async function ageLock(id: string, minutes: number) {
|
|
48
|
-
await getRepository(User).update({ id }, { lockedAt: new Date(Date.now() - minutes * 60 * 1000) })
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
async function lockOut() {
|
|
52
|
-
const user = await seedUser()
|
|
53
|
-
for (let i = 0; i < 5; i++) {
|
|
54
|
-
await expect(attempt(WRONG)).rejects.toThrow()
|
|
55
|
-
}
|
|
56
|
-
return user
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
beforeAll(async () => {
|
|
60
|
-
await openAuthDatabase()
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
beforeEach(async () => {
|
|
64
|
-
await resetAuthDatabase()
|
|
65
|
-
domain = await getRepository(Domain).save({ name: 'plant-a', subdomain: 'plant-a' })
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
afterAll(async () => {
|
|
69
|
-
await closeAuthDatabase()
|
|
70
|
-
})
|
|
71
|
-
|
|
72
|
-
describe('잠글 때 시각을 남긴다', () => {
|
|
73
|
-
it('다섯 번째 실패에 `lockedAt` 이 써진다', async () => {
|
|
74
|
-
const before = Date.now()
|
|
75
|
-
const user = await lockOut()
|
|
76
|
-
const after = Date.now()
|
|
77
|
-
|
|
78
|
-
const locked = await reload(user.id)
|
|
79
|
-
|
|
80
|
-
expect(locked.status).toBe('locked')
|
|
81
|
-
expect(locked.lockedAt).toBeTruthy()
|
|
82
|
-
|
|
83
|
-
const at = new Date(locked.lockedAt).getTime()
|
|
84
|
-
expect(at).toBeGreaterThanOrEqual(before - 1000)
|
|
85
|
-
expect(at).toBeLessThanOrEqual(after + 1000)
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
it('네 번째까지는 시각이 없다', async () => {
|
|
89
|
-
const user = await seedUser()
|
|
90
|
-
|
|
91
|
-
for (let i = 0; i < 4; i++) {
|
|
92
|
-
await expect(attempt(WRONG)).rejects.toThrow()
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
const still = await reload(user.id)
|
|
96
|
-
expect(still.status).toBe('activated')
|
|
97
|
-
expect(still.lockedAt).toBeFalsy()
|
|
98
|
-
})
|
|
99
|
-
})
|
|
100
|
-
|
|
101
|
-
describe('기본 10분 안에는 잠겨 있다', () => {
|
|
102
|
-
it('맞는 비밀번호도 거절한다', async () => {
|
|
103
|
-
const user = await lockOut()
|
|
104
|
-
await ageLock(user.id, DEFAULT_MINUTES - 1)
|
|
105
|
-
|
|
106
|
-
await expect(attempt(PASSWORD)).rejects.toThrow()
|
|
107
|
-
expect((await reload(user.id)).status).toBe('locked')
|
|
108
|
-
})
|
|
109
|
-
})
|
|
110
|
-
|
|
111
|
-
describe('시간이 지나면 스스로 풀린다', () => {
|
|
112
|
-
it('맞는 비밀번호로 들어가진다', async () => {
|
|
113
|
-
const user = await lockOut()
|
|
114
|
-
await ageLock(user.id, DEFAULT_MINUTES + 1)
|
|
115
|
-
|
|
116
|
-
const result = await attempt(PASSWORD)
|
|
117
|
-
|
|
118
|
-
expect(result.token).toBeTruthy()
|
|
119
|
-
})
|
|
120
|
-
|
|
121
|
-
it('행이 되돌아간다 — 상태·셈·시각 셋 다', async () => {
|
|
122
|
-
const user = await lockOut()
|
|
123
|
-
await ageLock(user.id, DEFAULT_MINUTES + 1)
|
|
124
|
-
|
|
125
|
-
await attempt(PASSWORD)
|
|
126
|
-
|
|
127
|
-
const released = await reload(user.id)
|
|
128
|
-
expect(released.status).toBe('activated')
|
|
129
|
-
expect(released.failCount).toBe(0)
|
|
130
|
-
expect(released.lockedAt).toBeFalsy()
|
|
131
|
-
})
|
|
132
|
-
|
|
133
|
-
it('풀린 뒤 틀려도 셈이 1 부터 다시 센다 — 잠금이 곧 창이다', async () => {
|
|
134
|
-
const user = await lockOut()
|
|
135
|
-
await ageLock(user.id, DEFAULT_MINUTES + 1)
|
|
136
|
-
|
|
137
|
-
/* 틀린 비밀번호로도 잠금이 풀린다 — 판정이 대조보다 먼저다. */
|
|
138
|
-
await expect(attempt(WRONG)).rejects.toThrow()
|
|
139
|
-
|
|
140
|
-
const after = await reload(user.id)
|
|
141
|
-
expect(after.status).toBe('activated')
|
|
142
|
-
expect(after.failCount).toBe(1)
|
|
143
|
-
expect(after.lockedAt).toBeFalsy()
|
|
144
|
-
})
|
|
145
|
-
|
|
146
|
-
it('다시 잠기려면 다섯 번을 새로 틀려야 한다', async () => {
|
|
147
|
-
const user = await lockOut()
|
|
148
|
-
await ageLock(user.id, DEFAULT_MINUTES + 1)
|
|
149
|
-
|
|
150
|
-
for (let i = 0; i < 4; i++) {
|
|
151
|
-
await expect(attempt(WRONG)).rejects.toThrow()
|
|
152
|
-
}
|
|
153
|
-
expect((await reload(user.id)).status).toBe('activated')
|
|
154
|
-
|
|
155
|
-
await expect(attempt(WRONG)).rejects.toThrow()
|
|
156
|
-
expect((await reload(user.id)).status).toBe('locked')
|
|
157
|
-
})
|
|
158
|
-
})
|
|
159
|
-
|
|
160
|
-
describe('`lockedAt` 이 없는 잠긴 행 — 이 변경 전에 잠긴 것', () => {
|
|
161
|
-
it('풀리지 않는다', async () => {
|
|
162
|
-
/* 조용히 열리는 쪽으로 기울면 잠긴 계정 전체가 한 번에 열린다. 관리자(`activateUser`)와
|
|
163
|
-
잠금 해제 메일이 푸는 길로 남긴다. */
|
|
164
|
-
const user = await seedUser({ status: 'locked', failCount: 5, lockedAt: null })
|
|
165
|
-
|
|
166
|
-
await expect(attempt(PASSWORD)).rejects.toThrow()
|
|
167
|
-
expect((await reload(user.id)).status).toBe('locked')
|
|
168
|
-
})
|
|
169
|
-
})
|
|
170
|
-
|
|
171
|
-
describe('요청 경로(`checkAuth`)도 같은 판정을 쓴다', () => {
|
|
172
|
-
it('잠긴 동안에는 토큰을 거절한다', async () => {
|
|
173
|
-
const user = await lockOut()
|
|
174
|
-
await ageLock(user.id, DEFAULT_MINUTES - 1)
|
|
175
|
-
|
|
176
|
-
await expect(User.checkAuth({ username: 'kim' })).rejects.toThrow()
|
|
177
|
-
})
|
|
178
|
-
|
|
179
|
-
it('시간이 지나면 통과시킨다 — 그리고 **행을 고치지 않는다**', async () => {
|
|
180
|
-
/* 매 요청 지나는 경로라 쓰기를 얹지 않는다. 행 정리는 다음 `signin` 이 한다. */
|
|
181
|
-
const user = await lockOut()
|
|
182
|
-
await ageLock(user.id, DEFAULT_MINUTES + 1)
|
|
183
|
-
|
|
184
|
-
const authed = await User.checkAuth({ username: 'kim' })
|
|
185
|
-
expect(authed.id).toBe(user.id)
|
|
186
|
-
|
|
187
|
-
const untouched = await reload(user.id)
|
|
188
|
-
expect(untouched.status).toBe('locked')
|
|
189
|
-
expect(untouched.lockedAt).toBeTruthy()
|
|
190
|
-
})
|
|
191
|
-
})
|
|
192
|
-
|
|
193
|
-
describe('설정으로 자동해제를 끌 수 있다', () => {
|
|
194
|
-
afterEach(() => {
|
|
195
|
-
config.set('password', undefined)
|
|
196
|
-
})
|
|
197
|
-
|
|
198
|
-
it('0 이면 시간이 지나도 풀리지 않는다', async () => {
|
|
199
|
-
config.set('password', { lockDurationMinutes: 0 })
|
|
200
|
-
|
|
201
|
-
const user = await lockOut()
|
|
202
|
-
await ageLock(user.id, 60 * 24 * 365)
|
|
203
|
-
|
|
204
|
-
await expect(attempt(PASSWORD)).rejects.toThrow()
|
|
205
|
-
expect((await reload(user.id)).status).toBe('locked')
|
|
206
|
-
})
|
|
207
|
-
|
|
208
|
-
it('설정한 분이 지나면 풀린다', async () => {
|
|
209
|
-
config.set('password', { lockDurationMinutes: 1 })
|
|
210
|
-
|
|
211
|
-
const user = await lockOut()
|
|
212
|
-
await ageLock(user.id, 2)
|
|
213
|
-
|
|
214
|
-
expect((await attempt(PASSWORD)).token).toBeTruthy()
|
|
215
|
-
})
|
|
216
|
-
})
|
|
@@ -1,141 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 계정 잠금 판정 규칙 시험 — 순수 함수라 엔티티에 닿지 않는다.
|
|
3
|
-
*
|
|
4
|
-
* 이 규칙이 하는 일은 하나다: 「이 잠금이 아직 유효한가」. 그 답을 두 곳이 쓴다 —
|
|
5
|
-
* `signin`(들어오려는 사람)과 `User.checkAuth`(요청마다). 두 곳이 각자 판단하면 로그인은
|
|
6
|
-
* 되는데 요청은 거절되는 상태가 생기므로 규칙을 한 곳에 두었고, 여기서 그 규칙만 본다.
|
|
7
|
-
*
|
|
8
|
-
* 시계를 인자로 받으므로 「10분 뒤」를 기다리지 않고 시험한다.
|
|
9
|
-
*/
|
|
10
|
-
|
|
11
|
-
import {
|
|
12
|
-
AccountLockFacts,
|
|
13
|
-
LOCKED_STATUS,
|
|
14
|
-
LOCK_FOREVER,
|
|
15
|
-
accountLockExpiresAt,
|
|
16
|
-
isAccountLocked,
|
|
17
|
-
isAccountLockExpired
|
|
18
|
-
} from '../server/utils/account-lock-rule.js'
|
|
19
|
-
import { loadCompiled } from './compiled'
|
|
20
|
-
|
|
21
|
-
const MINUTES = 10
|
|
22
|
-
const AT = new Date('2026-09-10T09:00:00.000Z')
|
|
23
|
-
const minutesAfter = (n: number) => new Date(AT.getTime() + n * 60 * 1000)
|
|
24
|
-
|
|
25
|
-
const locked = (overrides: Partial<AccountLockFacts> = {}): AccountLockFacts => ({
|
|
26
|
-
status: LOCKED_STATUS,
|
|
27
|
-
lockedAt: AT,
|
|
28
|
-
...overrides
|
|
29
|
-
})
|
|
30
|
-
|
|
31
|
-
describe('엔티티와 값이 어긋나지 않는다', () => {
|
|
32
|
-
it('`LOCKED_STATUS` 가 `UserStatus.LOCKED` 와 같다', () => {
|
|
33
|
-
/* 이 규칙은 엔티티를 import 하지 않는다(그러면 순수 시험이 닿지 못한다). 그래서 두 값이
|
|
34
|
-
어긋나는 것을 막는 것은 이 시험뿐이다. */
|
|
35
|
-
const { UserStatus } = loadCompiled('service/user/user')
|
|
36
|
-
|
|
37
|
-
expect(LOCKED_STATUS).toBe(UserStatus.LOCKED)
|
|
38
|
-
})
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
describe('잠금이 아닌 상태', () => {
|
|
42
|
-
it('활성 계정은 잠금이 아니다', () => {
|
|
43
|
-
for (const status of ['activated', 'inactive', 'deleted', 'banned', 'password_reset_required']) {
|
|
44
|
-
expect(isAccountLocked({ status, lockedAt: AT }, AT, MINUTES)).toBe(false)
|
|
45
|
-
expect(isAccountLockExpired({ status, lockedAt: AT }, AT, MINUTES)).toBe(false)
|
|
46
|
-
expect(accountLockExpiresAt({ status, lockedAt: AT }, MINUTES)).toBeNull()
|
|
47
|
-
}
|
|
48
|
-
})
|
|
49
|
-
|
|
50
|
-
it('사실이 비어 있어도 던지지 않는다', () => {
|
|
51
|
-
for (const facts of [undefined, null, {}, { status: null }] as any[]) {
|
|
52
|
-
expect(isAccountLocked(facts, AT, MINUTES)).toBe(false)
|
|
53
|
-
}
|
|
54
|
-
})
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
describe('시간이 지나면 풀린다', () => {
|
|
58
|
-
it('설정한 시간 안에는 잠겨 있다', () => {
|
|
59
|
-
expect(isAccountLocked(locked(), AT, MINUTES)).toBe(true)
|
|
60
|
-
expect(isAccountLocked(locked(), minutesAfter(9), MINUTES)).toBe(true)
|
|
61
|
-
})
|
|
62
|
-
|
|
63
|
-
it('설정한 시간이 지나면 풀린다', () => {
|
|
64
|
-
expect(isAccountLocked(locked(), minutesAfter(10), MINUTES)).toBe(false)
|
|
65
|
-
expect(isAccountLocked(locked(), minutesAfter(60), MINUTES)).toBe(false)
|
|
66
|
-
})
|
|
67
|
-
|
|
68
|
-
it('경계는 만료 시각을 포함하지 않는다 — 정확히 10분이면 풀린 것이다', () => {
|
|
69
|
-
const facts = locked()
|
|
70
|
-
|
|
71
|
-
expect(isAccountLocked(facts, minutesAfter(10 - 1 / 60000), MINUTES)).toBe(true)
|
|
72
|
-
expect(isAccountLocked(facts, minutesAfter(10), MINUTES)).toBe(false)
|
|
73
|
-
})
|
|
74
|
-
|
|
75
|
-
it('풀린 것을 따로 물을 수 있다 — 행을 되돌려야 하는지 판단하는 자리', () => {
|
|
76
|
-
expect(isAccountLockExpired(locked(), minutesAfter(5), MINUTES)).toBe(false)
|
|
77
|
-
expect(isAccountLockExpired(locked(), minutesAfter(15), MINUTES)).toBe(true)
|
|
78
|
-
})
|
|
79
|
-
|
|
80
|
-
it('만료 시각을 계산해 준다', () => {
|
|
81
|
-
expect(accountLockExpiresAt(locked(), MINUTES)).toEqual(minutesAfter(10))
|
|
82
|
-
})
|
|
83
|
-
|
|
84
|
-
it('드라이버가 문자열로 준 시각도 읽는다', () => {
|
|
85
|
-
/* sqlite·mssql 은 `Date` 대신 문자열을 주기도 한다. */
|
|
86
|
-
const facts = locked({ lockedAt: AT.toISOString() })
|
|
87
|
-
|
|
88
|
-
expect(isAccountLocked(facts, minutesAfter(5), MINUTES)).toBe(true)
|
|
89
|
-
expect(isAccountLocked(facts, minutesAfter(15), MINUTES)).toBe(false)
|
|
90
|
-
})
|
|
91
|
-
})
|
|
92
|
-
|
|
93
|
-
describe('문은 `status` 가 지킨다 — `lockedAt` 이 아니다', () => {
|
|
94
|
-
it('한 번도 잠긴 적 없는 계정은 `lockedAt` 이 없어도 열려 있다', () => {
|
|
95
|
-
/*
|
|
96
|
-
* 이 한 줄이 배포 사고 하나를 막는다. 판정이 `lockedAt` 을 먼저 보면 칸이 비어 있는
|
|
97
|
-
* **모든** 계정(= 한 번도 잠긴 적 없는 계정 전부)이 잠김이 되어 아무도 못 들어온다.
|
|
98
|
-
* 그래서 `status` 를 먼저 보고, 잠금이 아니면 `lockedAt` 을 아예 보지 않는다.
|
|
99
|
-
*/
|
|
100
|
-
const never = { status: 'activated' } as AccountLockFacts
|
|
101
|
-
|
|
102
|
-
expect(isAccountLocked(never, AT, MINUTES)).toBe(false)
|
|
103
|
-
expect(isAccountLockExpired(never, AT, MINUTES)).toBe(false)
|
|
104
|
-
expect(accountLockExpiresAt(never, MINUTES)).toBeNull()
|
|
105
|
-
})
|
|
106
|
-
|
|
107
|
-
it('자동해제를 끈 설정에서도 잠기지 않은 계정은 열려 있다', () => {
|
|
108
|
-
/* 「자동해제 없음」이 「전원 잠금」이 되지 않는다. */
|
|
109
|
-
expect(isAccountLocked({ status: 'activated' }, AT, LOCK_FOREVER)).toBe(false)
|
|
110
|
-
expect(isAccountLocked({ status: 'activated', lockedAt: null }, AT, LOCK_FOREVER)).toBe(false)
|
|
111
|
-
})
|
|
112
|
-
})
|
|
113
|
-
|
|
114
|
-
describe('잠긴 계정에서 `lockedAt` 이 없을 때', () => {
|
|
115
|
-
it('「잠겼다」가 아니라 **「자동 해제가 없다」**가 된다', () => {
|
|
116
|
-
/*
|
|
117
|
-
* 이 변경 전에 잠긴 행에는 칸이 비어 있다. 언제 풀릴지 계산할 수 없으니 자동으로는 안
|
|
118
|
-
* 풀린다 — 풀어 주는 쪽으로 기울면 **잠긴 계정 전체가 조용히 열린다.** 관리자
|
|
119
|
-
* (`activateUser`)와 잠금 해제 메일이 푸는 길로 남는다.
|
|
120
|
-
*
|
|
121
|
-
* 이것은 위 절과 짝이다: 잠금 여부는 `status` 가 정하고, 여기서 `lockedAt` 이 정하는 것은
|
|
122
|
-
* **해제 시각이 있나**뿐이다.
|
|
123
|
-
*/
|
|
124
|
-
for (const lockedAt of [undefined, null, '', 'not-a-date'] as any[]) {
|
|
125
|
-
const facts = locked({ lockedAt })
|
|
126
|
-
|
|
127
|
-
expect(isAccountLocked(facts, minutesAfter(600), MINUTES)).toBe(true)
|
|
128
|
-
expect(isAccountLockExpired(facts, minutesAfter(600), MINUTES)).toBe(false)
|
|
129
|
-
expect(accountLockExpiresAt(facts, MINUTES)).toBeNull()
|
|
130
|
-
}
|
|
131
|
-
})
|
|
132
|
-
})
|
|
133
|
-
|
|
134
|
-
describe('자동해제를 끄는 설정', () => {
|
|
135
|
-
it('0 이하면 풀리지 않는다', () => {
|
|
136
|
-
for (const duration of [LOCK_FOREVER, 0, -1, undefined, null, NaN] as any[]) {
|
|
137
|
-
expect(isAccountLocked(locked(), minutesAfter(60 * 24 * 365), duration)).toBe(true)
|
|
138
|
-
expect(accountLockExpiresAt(locked(), duration)).toBeNull()
|
|
139
|
-
}
|
|
140
|
-
})
|
|
141
|
-
})
|
|
@@ -1,69 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* The four account mails: each in the person's language, with its own subject, in the one mail every module sends -
|
|
3
|
-
* the application's brand at the head, never the old frame with a logo fetched from hatiolab.com.
|
|
4
|
-
*/
|
|
5
|
-
import fs from 'fs'
|
|
6
|
-
import path from 'path'
|
|
7
|
-
|
|
8
|
-
import { DEFAULT_BRAND } from '@things-factory/email-base'
|
|
9
|
-
import { i18next, NAMESPACE, translatorFor } from '@things-factory/i18n-base/translator'
|
|
10
|
-
|
|
11
|
-
import { ACCOUNT_MAIL_KINDS, accountMail, accountMailWords } from '../server/controllers/account-mail'
|
|
12
|
-
|
|
13
|
-
const LANGUAGES = ['ko', 'en', 'ja', 'zh', 'ms', 'es']
|
|
14
|
-
const LINK = 'https://office.example.com/auth/reset-password#token'
|
|
15
|
-
|
|
16
|
-
/* A test runs outside an application, so the translator has not gathered this module's words; they are added here. */
|
|
17
|
-
beforeAll(() => {
|
|
18
|
-
for (const lng of LANGUAGES) {
|
|
19
|
-
const words = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../translations', `${lng}.json`), 'utf8'))
|
|
20
|
-
i18next.addResourceBundle(lng, NAMESPACE, words, true, true)
|
|
21
|
-
}
|
|
22
|
-
})
|
|
23
|
-
|
|
24
|
-
describe('the words of an account mail', () => {
|
|
25
|
-
test('every mail has its subject, body and button in every language - never a key', () => {
|
|
26
|
-
for (const lng of LANGUAGES) {
|
|
27
|
-
for (const kind of ACCOUNT_MAIL_KINDS) {
|
|
28
|
-
const words = accountMailWords(translatorFor(lng), kind, 'Brand X')
|
|
29
|
-
|
|
30
|
-
for (const [part, text] of Object.entries(words)) {
|
|
31
|
-
expect([lng, kind, part, text]).not.toEqual([lng, kind, part, expect.stringMatching(/^(title|text|button)\.account-mail-/)])
|
|
32
|
-
expect([lng, kind, part, text.trim()]).not.toEqual([lng, kind, part, ''])
|
|
33
|
-
}
|
|
34
|
-
expect(words.body).toContain('Brand X')
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
})
|
|
38
|
-
|
|
39
|
-
test('the four subjects differ in every language - a reset is not mistaken for an unlock', () => {
|
|
40
|
-
for (const lng of LANGUAGES) {
|
|
41
|
-
const subjects = ACCOUNT_MAIL_KINDS.map(kind => accountMailWords(translatorFor(lng), kind, 'Brand X').subject)
|
|
42
|
-
expect(new Set(subjects).size).toBe(ACCOUNT_MAIL_KINDS.length)
|
|
43
|
-
}
|
|
44
|
-
})
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
describe('an account mail', () => {
|
|
48
|
-
const context = (lng?: string) => ({ lng, i18next })
|
|
49
|
-
|
|
50
|
-
test('is written in the person’s language, else the language of the request', () => {
|
|
51
|
-
expect(accountMail({ kind: 'unlock-account', user: { locale: 'ko-KR' }, link: LINK, context: context('en-US') }).subject).toBe('계정 잠김')
|
|
52
|
-
expect(accountMail({ kind: 'unlock-account', user: { locale: null }, link: LINK, context: context('ja') }).subject).toBe(
|
|
53
|
-
translatorFor('ja')('title.account-mail-unlock-account')
|
|
54
|
-
)
|
|
55
|
-
})
|
|
56
|
-
|
|
57
|
-
test('is the one mail: the brand at the head, the subject in the band, the link as the button, no fetched image', () => {
|
|
58
|
-
const { subject, content } = accountMail({ kind: 'reset-password', user: { locale: 'ko' }, link: LINK, context: context() })
|
|
59
|
-
|
|
60
|
-
expect(subject).toBe('비밀번호 재설정')
|
|
61
|
-
expect(content).toContain('<html lang="ko">')
|
|
62
|
-
expect(content).toContain(`>${DEFAULT_BRAND.name}<`)
|
|
63
|
-
expect(content).toContain(`href="${LINK}"`)
|
|
64
|
-
expect(content).toContain('>비밀번호 재설정</font></a')
|
|
65
|
-
expect(content).toContain('Copyright © HatioLab Inc. All rights reserved.')
|
|
66
|
-
expect(content).not.toContain('<img')
|
|
67
|
-
expect(content).not.toContain('Hatio, Lab.')
|
|
68
|
-
})
|
|
69
|
-
})
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { loadCompiled } from './compiled'
|
|
2
|
-
|
|
3
|
-
const { authenticate401Middleware } = loadCompiled('middlewares/authenticate-401-middleware')
|
|
4
|
-
const { AuthError } = loadCompiled('errors/auth-error')
|
|
5
|
-
const CODES = loadCompiled('constants/error-code')
|
|
6
|
-
|
|
7
|
-
/**
|
|
8
|
-
* One page, `auth-activate`, answers a locked account and one not yet confirmed. It said "your
|
|
9
|
-
* account is not activated" to both: the reason was in `message`, and the page never read it. A
|
|
10
|
-
* locked person was not told why, nor that the unlock mail had already gone.
|
|
11
|
-
*
|
|
12
|
-
* The page tells the two apart by `errorCode`, so that is what this holds - for both cases.
|
|
13
|
-
*/
|
|
14
|
-
|
|
15
|
-
async function pageFor(errorCode: string) {
|
|
16
|
-
let rendered: any
|
|
17
|
-
|
|
18
|
-
const context: any = {
|
|
19
|
-
t: (key: string) => key,
|
|
20
|
-
method: 'GET',
|
|
21
|
-
header: { accept: 'text/html' },
|
|
22
|
-
path: '/',
|
|
23
|
-
originalUrl: '/',
|
|
24
|
-
query: {},
|
|
25
|
-
request: { body: {} },
|
|
26
|
-
render: async (view: string, options: any) => {
|
|
27
|
-
rendered = { view, ...options }
|
|
28
|
-
}
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
await authenticate401Middleware(context, async () => {
|
|
32
|
-
throw new AuthError({ errorCode, detail: { username: 'kim', email: 'kim@example.com' } })
|
|
33
|
-
})
|
|
34
|
-
|
|
35
|
-
return rendered
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
describe.each([CODES.USER_LOCKED, CODES.USER_NOT_ACTIVATED])('%s', errorCode => {
|
|
39
|
-
it('goes to the activate page with its code', async () => {
|
|
40
|
-
const page = await pageFor(errorCode)
|
|
41
|
-
|
|
42
|
-
expect(page.pageElement).toBe('auth-activate')
|
|
43
|
-
expect(page.data.errorCode).toBe(errorCode)
|
|
44
|
-
})
|
|
45
|
-
})
|
|
46
|
-
|
|
47
|
-
it('the page reads the locked code by the spelling the server sends', () => {
|
|
48
|
-
const { readFileSync } = require('fs')
|
|
49
|
-
const { join } = require('path')
|
|
50
|
-
const page = readFileSync(join(__dirname, '..', '..', 'auth-ui', 'client', 'entries', 'auth', 'activate.ts'), 'utf-8')
|
|
51
|
-
|
|
52
|
-
expect(page).toContain(`this.data?.errorCode === '${CODES.USER_LOCKED}'`)
|
|
53
|
-
})
|