@starci/hfs 5.2.0 → 5.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +43 -0
- package/README.md +2 -1
- package/cache/hfs-family.d.mts +2 -3
- package/cache/hfs-family.mjs +4 -9
- package/cache/secret-records.d.mts +7 -0
- package/cache/secret-records.mjs +12 -0
- package/cache/session.mjs +4 -2
- package/codemod/account-doors.d.mts +36 -0
- package/codemod/account-doors.mjs +134 -0
- package/codemod/app-prettier.d.mts +5 -0
- package/codemod/app-prettier.mjs +27 -0
- package/codemod/explicit-surface.mjs +1 -25
- package/codemod/main.mjs +35 -1
- package/codemod/module-definition.d.mts +59 -0
- package/codemod/module-definition.mjs +257 -0
- package/codemod/quality-binding.d.mts +33 -0
- package/codemod/quality-binding.mjs +38 -0
- package/codemod/secrets-cache.d.mts +35 -0
- package/codemod/secrets-cache.mjs +70 -0
- package/codemod/sops-rules.d.mts +34 -0
- package/codemod/sops-rules.mjs +69 -0
- package/codemod/theme-tokens.mjs +48 -2
- package/lint/run.d.mts +6 -0
- package/lint/run.mjs +63 -10
- package/package.json +1 -1
- package/runtime/knowledge/hfs/canon-pins.yaml +43 -10
- package/runtime/knowledge/hfs/fe-stack.yaml +3 -3
- package/runtime/knowledge/hfs/rules.yaml +13 -9
- package/runtime/knowledge/hfs/slots.yaml +105 -12
- package/runtime/modules/kernel/failure-codes.yaml +5 -4
- package/runtime/scripts/hfs/architecture/config.d.mts +4 -0
- package/runtime/scripts/hfs/architecture/config.mjs +25 -1
- package/runtime/scripts/hfs/architecture/module-per-transport.d.mts +1 -1
- package/runtime/scripts/hfs/architecture/module-per-transport.mjs +6 -1
- package/runtime/scripts/hfs/architecture/root-packages.d.mts +26 -0
- package/runtime/scripts/hfs/architecture/root-packages.mjs +78 -0
- package/runtime/scripts/hfs/architecture/type-context.d.mts +4 -1
- package/runtime/scripts/hfs/architecture/type-context.mjs +30 -0
- package/runtime/scripts/hfs/architecture/typescript.d.mts +7 -1
- package/runtime/scripts/hfs/architecture/typescript.mjs +21 -1
- package/runtime/scripts/hfs/explicit-surface-rule.mjs +1 -1
- package/runtime/scripts/hfs/rules/database-access.mjs +39 -1
- package/runtime/scripts/hfs/rules/monorepo.d.mts +4 -0
- package/runtime/scripts/hfs/rules/monorepo.mjs +10 -5
- package/runtime/scripts/hfs/rules/secrets.mjs +11 -0
- package/runtime/scripts/hfs/secret-errors.d.mts +3 -0
- package/runtime/scripts/hfs/secret-errors.mjs +8 -0
- package/runtime/scripts/hfs/secret-init.d.mts +6 -0
- package/runtime/scripts/hfs/secret-init.mjs +23 -0
- package/runtime/scripts/hfs/secret-sops.mjs +9 -1
- package/runtime/scripts/hfs/secret-standard.d.mts +20 -0
- package/runtime/scripts/hfs/secret-standard.mjs +106 -0
- package/runtime/scripts/hfs/secret.mjs +5 -1
- package/runtime/scripts/hfs/view.d.mts +23 -0
- package/runtime/scripts/hfs/view.mjs +68 -0
- package/runtime/scripts/lib/declaration-surface.d.mts +4 -0
- package/runtime/scripts/lib/declaration-surface.mjs +55 -1
- package/runtime/scripts/lib/secret-records.d.mts +13 -0
- package/runtime/scripts/lib/secret-records.mjs +83 -0
- package/scaffold/add-inbox-lite.mjs +2 -2
- package/scaffold/add-queue-lite.mjs +1 -3
- package/scaffold/app.d.mts +2 -2
- package/scaffold/app.mjs +10 -12
- package/scaffold/lite-exports.mjs +1 -15
- package/scripts/sync-bundles.d.mts +8 -0
- package/stack/images/openclaw-runtime/Dockerfile +18 -0
- package/stack/images/openclaw-runtime/entrypoint.sh +23 -0
- package/stack/images/openclaw-runtime/openclaw-init.mjs +101 -0
- package/stack/images/openclaw-runtime/openclaw-relay.mjs +25 -0
- package/stack/local-up.mjs +3 -0
- package/stack/runtime-layers.d.mts +21 -0
- package/stack/runtime-layers.mjs +67 -0
- package/sync/compile-guard.d.mts +36 -0
- package/sync/compile-guard.mjs +189 -0
- package/sync/hygiene.mjs +3 -1
- package/sync/index.d.mts +11 -1
- package/sync/index.mjs +61 -10
- package/templates/app/auth-doors.sql.partial +115 -0
- package/templates/app/ci-workflows/github/workflows/ci.yml +1 -1
- package/templates/app/ci-workflows-lite/github/workflows/ci.yml +1 -1
- package/templates/app/package-scripts/core.json +1 -1
- package/templates/app/package-scripts/package.json +1 -1
- package/templates/app/package-scripts-lite/package.json +1 -1
- package/templates/app/skeleton-lite/.starcistacks/application-stacks.yaml +9 -0
- package/templates/app/skeleton-lite/.starcistacks/dev/README.md +3 -1
- package/templates/app/skeleton-lite/.starcistacks/dev/infra/compose/compose.yaml +19 -1
- package/templates/app/skeleton-lite/.starcistacks/dev/runtime/env/KEYS.md +3 -0
- package/templates/app/skeleton-lite/.starcistacks/production/infra/swarm/stack.yaml +5 -0
- package/templates/app/skeleton-lite/.starcistacks/production/runtime/env/KEYS.md +4 -0
- package/templates/app/skeleton-lite/db/migrations/__timestamp___baseline.sql +27 -5
- package/templates/be/auth-lite/src/features/api/auth/application/auth.contracts.ts +33 -0
- package/templates/be/auth-lite/src/features/api/auth/application/confirm.command.ts +11 -0
- package/templates/be/auth-lite/src/features/api/auth/application/confirm.handler.ts +22 -0
- package/templates/be/auth-lite/src/features/api/auth/application/forgot.command.ts +11 -0
- package/templates/be/auth-lite/src/features/api/auth/application/forgot.handler.ts +22 -0
- package/templates/be/auth-lite/src/features/api/auth/application/resend.command.ts +11 -0
- package/templates/be/auth-lite/src/features/api/auth/application/resend.handler.ts +22 -0
- package/templates/be/auth-lite/src/features/api/auth/application/reset.command.ts +11 -0
- package/templates/be/auth-lite/src/features/api/auth/application/reset.handler.ts +22 -0
- package/templates/be/auth-lite/src/features/api/auth/application/signup.command.ts +11 -0
- package/templates/be/auth-lite/src/features/api/auth/application/signup.handler.ts +22 -0
- package/templates/be/auth-lite/src/features/api/auth/auth.module-definition.ts +3 -10
- package/templates/be/auth-lite/src/features/api/auth/auth.module.ts +6 -1
- package/templates/be/auth-lite/src/features/api/auth/transport/http/auth-http.module-definition.ts +3 -10
- package/templates/be/auth-lite/src/features/api/auth/transport/http/auth.controller.ts +56 -1
- package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/forgot.request.ts +16 -0
- package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/resend.request.ts +16 -0
- package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/reset.request.ts +18 -0
- package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/signup.request.ts +22 -0
- package/templates/be/auth-lite/src/features/api/auth/transport/http/dto/token.request.ts +11 -0
- package/templates/be/auth-lite/src/modules/domain/identity/account-directory.service.spec.ts +30 -30
- package/templates/be/auth-lite/src/modules/domain/identity/account-directory.service.ts +7 -16
- package/templates/be/auth-lite/src/modules/domain/identity/authenticator.service.spec.ts +113 -49
- package/templates/be/auth-lite/src/modules/domain/identity/authenticator.service.ts +42 -53
- package/templates/be/auth-lite/src/modules/domain/identity/identity-email.log-events.ts +6 -0
- package/templates/be/auth-lite/src/modules/domain/identity/identity-email.service.spec.ts +233 -0
- package/templates/be/auth-lite/src/modules/domain/identity/identity-email.service.ts +176 -0
- package/templates/be/auth-lite/src/modules/domain/identity/identity-sign-in.log-events.ts +6 -0
- package/templates/be/auth-lite/src/modules/domain/identity/persistence/account-doors.port.ts +98 -0
- package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/identity-email-token.entity.ts +34 -0
- package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/identity-refresh-token.entity.ts +4 -0
- package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/index.ts +3 -10
- package/templates/be/auth-lite/src/modules/domain/identity/session-store.service.spec.ts +6 -3
- package/templates/be/auth-lite/src/modules/domain/identity/session-store.service.ts +6 -1
- package/templates/be/auth-lite/src/modules/domain/identity/sign-in.service.spec.ts +17 -0
- package/templates/be/auth-lite/src/modules/domain/identity/sign-in.service.ts +6 -0
- package/templates/be/auth-lite/src/tests/fixtures/builders/identity.builder.ts +29 -33
- package/templates/be/inbox/module-definition.ts.tpl +2 -9
- package/templates/be/inbox/platform/event-bus/event-bus.module-definition.ts.tpl +2 -11
- package/templates/be/inbox/platform/inbox/inbox.module-definition.ts.tpl +2 -9
- package/templates/be/patterns/api/feature.module-definition.ts.tpl +3 -9
- package/templates/be/patterns/api/graphql.module-definition.ts.tpl +3 -9
- package/templates/be/patterns/cli/group.module-definition.ts.tpl +2 -9
- package/templates/be/patterns/event-bus/platform/event-bus.module-definition.ts.tpl +3 -10
- package/templates/be/patterns/idempotency/platform/inbox/inbox.module-definition.ts.tpl +2 -9
- package/templates/be/patterns/jobs/job.module-definition.ts.tpl +2 -9
- package/templates/be/patterns/jobs/job.queue.module-definition.ts.tpl +3 -9
- package/templates/be/patterns/jobs/platform/jobs.module-definition.ts.tpl +3 -10
- package/templates/be/patterns/projections/projection.module-definition.ts.tpl +2 -9
- package/templates/be/patterns/queues/platform/queue.module-definition.ts.tpl +3 -10
- package/templates/be/patterns/queues/queue.module-definition.ts.tpl +2 -9
- package/templates/be/patterns/reactors/message.module-definition.ts.tpl +3 -9
- package/templates/be/patterns/reactors/reactor.module-definition.ts.tpl +2 -9
- package/templates/be/patterns/realtime/channel.graphql.module-definition.ts.tpl +3 -9
- package/templates/be/patterns/realtime/channel.sse.module-definition.ts.tpl +3 -9
- package/templates/be/patterns/realtime/channel.websocket.module-definition.ts.tpl +3 -9
- package/templates/be/patterns/saga/saga.message.module-definition.ts.tpl +3 -9
- package/templates/be/patterns/saga/saga.module-definition.ts.tpl +2 -9
- package/templates/be/patterns/webhooks/webhook.http.module-definition.ts.tpl +3 -9
- package/templates/be/platform/graphql/graphql.module-definition.ts +2 -9
- package/templates/be/skeleton/src/features/api/system-health/system-health.module-definition.ts +3 -9
- package/templates/be/skeleton/src/features/api/system-health/transport/http/system-health-http.module-definition.ts +3 -9
- package/templates/be/skeleton/src/features/cli/cli.module-definition.ts +2 -9
- package/templates/be/skeleton/src/features/cli/migrate/migrate.module-definition.ts +3 -9
- package/templates/be/skeleton/src/features/cli/seed/seed.module-definition.ts +2 -9
- package/templates/be/skeleton/src/modules/domain/identity/identity.module-definition.ts +2 -9
- package/templates/be/skeleton/src/modules/domain/liveness/liveness.module-definition.ts +2 -9
- package/templates/be/skeleton/src/modules/domain/note/note.module-definition.ts +2 -9
- package/templates/be/skeleton/src/modules/platform/clock/clock.module-definition.ts +2 -9
- package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.contracts.ts +2 -0
- package/templates/be/skeleton/src/modules/platform/cqrs/cqrs.module-definition.ts +2 -9
- package/templates/be/skeleton/src/modules/platform/database/database.module-definition.ts +3 -13
- package/templates/be/skeleton/src/modules/platform/errors/errors.module-definition.ts +4 -11
- package/templates/be/skeleton/src/modules/platform/http/http.module-definition.ts +4 -9
- package/templates/be/skeleton/src/modules/platform/http-security/http-security.module-definition.ts +5 -11
- package/templates/be/skeleton/src/modules/platform/i18n/i18n.module-definition.ts +4 -11
- package/templates/be/skeleton/src/modules/platform/ids/ids.module-definition.ts +2 -9
- package/templates/be/skeleton/src/modules/platform/logging/logging.module-definition.ts +2 -9
- package/templates/be/skeleton/src/modules/platform/retry/retry.module-definition.ts +2 -9
- package/templates/be/skeleton-lite/apps/api/src/api.env.ts +4 -0
- package/templates/be/skeleton-lite/apps/api/src/api.options.ts +3 -0
- package/templates/be/skeleton-lite/apps/api/src/compose/integrations.ts +3 -1
- package/templates/be/skeleton-lite/src/modules/domain/identity/errors/identity.error.ts +6 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/identity.module.ts +3 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/index.ts +1 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/messages/identity-mail.messages.ts +65 -0
- package/templates/be/skeleton-lite/src/modules/domain/identity/messages/identity.messages.ts +4 -0
- package/templates/be/skeleton-lite/src/modules/integrations/mail/index.ts +6 -0
- package/templates/be/skeleton-lite/src/modules/integrations/mail/mail.config.ts +33 -0
- package/templates/be/skeleton-lite/src/modules/integrations/mail/mail.decorators.ts +14 -0
- package/templates/be/skeleton-lite/src/modules/integrations/mail/mail.module-definition.ts +12 -0
- package/templates/be/skeleton-lite/src/modules/integrations/mail/mail.module.ts +27 -0
- package/templates/be/skeleton-lite/src/modules/integrations/mail/mail.options.ts +22 -0
- package/templates/be/skeleton-lite/src/modules/integrations/object-storage/object-storage.module-definition.ts +2 -9
- package/templates/be/skeleton-lite/src/modules/integrations/passport/index.ts +1 -1
- package/templates/be/skeleton-lite/src/modules/platform/database/index.ts +1 -0
- package/templates/be/skeleton-lite/src/modules/platform/http-security/rate-limit.guard.spec.ts +11 -0
- package/templates/be/skeleton-lite/src/tests/fixtures/builders/api.builder.ts +11 -0
- package/templates/be/table/api/feature.module-definition.ts.tpl +3 -9
- package/templates/be/table/api/graphql.module-definition.ts.tpl +3 -9
- package/templates/be/table/module-definition.ts.tpl +2 -9
- package/templates/fe/auth-lite/apps/web/src/modules/auth/email-doors.ts +16 -0
- package/templates/fe/auth-lite/apps/web/src/modules/auth/index.ts +5 -0
- package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +30 -67
- package/templates/fe/skeleton/packages/__project__-i18n/src/proxy.ts +6 -21
- package/templates/fe/skeleton/packages/__project__-i18n/src/routing.ts +10 -16
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/auth/confirm/page.tsx +9 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/auth/forgot/page.tsx +9 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/auth/resend/page.tsx +9 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/auth/reset/page.tsx +9 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/auth/signup/page.tsx +9 -0
- package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/layout.tsx +5 -9
- package/templates/fe/skeleton-lite/apps/web/src/components/blocks/EmailDoorForm/component.tsx +51 -0
- package/templates/fe/skeleton-lite/apps/web/src/components/blocks/EmailDoorForm/index.tsx +84 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppConfirmPage/index.tsx +25 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppForgotPage/index.tsx +21 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/component.tsx +11 -3
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppHomePage/index.tsx +4 -1
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppResendPage/index.tsx +21 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppResetPage/index.tsx +25 -0
- package/templates/fe/skeleton-lite/apps/web/src/features/pages/AppSignupPage/index.tsx +21 -0
- package/templates/fe/skeleton-lite/apps/web/src/hooks/emailAuth/emailAuth.shared.ts +12 -0
- package/templates/fe/skeleton-lite/apps/web/src/hooks/emailAuth/index.ts +2 -0
- package/templates/fe/skeleton-lite/apps/web/src/hooks/emailAuth/useEmailDoor.ts +32 -0
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/index.ts +2 -2
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/en.json +50 -1
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/messages/vi.json +50 -1
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/request.ts +7 -30
- package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/routing.ts +13 -14
- package/templates/fe/skeleton-lite/apps/web/src/modules/routes/index.ts +5 -0
- package/templates/fe/skeleton-lite/apps/web/src/proxy.ts +4 -17
- package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/auth-identity.entity.ts +0 -38
- package/templates/be/auth-lite/src/modules/domain/identity/persistence/entities/auth-user.entity.ts +0 -54
package/templates/be/auth-lite/src/modules/domain/identity/account-directory.service.spec.ts
CHANGED
|
@@ -1,46 +1,46 @@
|
|
|
1
1
|
// Managed by starci app sync (slot be.identity.sign-in); do not edit: run "npx starci app sync --execute".
|
|
2
2
|
import "reflect-metadata"
|
|
3
3
|
import { Test } from "@nestjs/testing"
|
|
4
|
-
import {
|
|
5
|
-
import { CLOCK } from "@modules/platform/clock"
|
|
4
|
+
import { mockEntityManager } from "@starci/jest-preset"
|
|
6
5
|
import { PRIMARY_ENTITY_MANAGER } from "@modules/platform/database"
|
|
7
|
-
import {
|
|
8
|
-
import {
|
|
9
|
-
import { IdentityAccountDirectoryService, isDisabled } from "./account-directory.service"
|
|
6
|
+
import { accountByIdRow } from "@tests/fixtures/builders/identity.builder"
|
|
7
|
+
import { IdentityAccountDirectoryService } from "./account-directory.service"
|
|
10
8
|
|
|
11
|
-
const
|
|
9
|
+
const USER_BY_ID = 'SELECT * FROM "public"."be_auth_user_by_id"($1)'
|
|
12
10
|
|
|
13
|
-
const build = async (rows:
|
|
14
|
-
const entityManager = mockEntityManager({
|
|
11
|
+
const build = async (rows: unknown) => {
|
|
12
|
+
const entityManager = mockEntityManager({ query: [USER_BY_ID, rows] })
|
|
15
13
|
const moduleRef = await Test.createTestingModule({
|
|
16
|
-
providers: [
|
|
17
|
-
IdentityAccountDirectoryService,
|
|
18
|
-
{ provide: PRIMARY_ENTITY_MANAGER, useValue: entityManager },
|
|
19
|
-
{ provide: CLOCK, useValue: new FakeClock(AT) },
|
|
20
|
-
],
|
|
14
|
+
providers: [IdentityAccountDirectoryService, { provide: PRIMARY_ENTITY_MANAGER, useValue: entityManager }],
|
|
21
15
|
}).compile()
|
|
22
|
-
return moduleRef.get(IdentityAccountDirectoryService)
|
|
16
|
+
return { directory: moduleRef.get(IdentityAccountDirectoryService), entityManager }
|
|
23
17
|
}
|
|
24
18
|
|
|
25
19
|
describe("IdentityAccountDirectoryService.find", () => {
|
|
26
|
-
it("answers the standing of a live account
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
expect(await
|
|
30
|
-
expect((
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
20
|
+
it("answers the standing of a live account through the door", async () => {
|
|
21
|
+
const { directory, entityManager } = await build([accountByIdRow("live")])
|
|
22
|
+
|
|
23
|
+
expect(await directory.find("u-1")).toEqual({ id: "u-1", email: "a@example.test", disabled: false })
|
|
24
|
+
expect(entityManager.query).toHaveBeenCalledWith(USER_BY_ID, ["u-1"])
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it("answers a disabled account as disabled, and an account without an address with none", async () => {
|
|
28
|
+
const banned = await build([accountByIdRow("disabled")])
|
|
29
|
+
const addressless = await build([accountByIdRow("addressless")])
|
|
30
|
+
|
|
31
|
+
expect((await banned.directory.find("u-1"))?.disabled).toBe(true)
|
|
32
|
+
expect(await addressless.directory.find("u-1")).toEqual({ id: "u-1", email: null, disabled: false })
|
|
33
|
+
})
|
|
34
|
+
|
|
35
|
+
it("answers undefined for an account that does not exist", async () => {
|
|
36
|
+
const { directory } = await build([])
|
|
37
|
+
|
|
38
|
+
expect(await directory.find("u-2")).toBeUndefined()
|
|
34
39
|
})
|
|
35
|
-
})
|
|
36
40
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
const now = new Date(AT)
|
|
41
|
+
it("refuses a door answer that is not the account shape", async () => {
|
|
42
|
+
const { directory } = await build([{ id: "u-1", email: "a@example.test", disabled: "no" }])
|
|
40
43
|
|
|
41
|
-
expect(
|
|
42
|
-
expect(isDisabled(user("banned"), now)).toBe(true)
|
|
43
|
-
expect(isDisabled(user("ban-ended"), now)).toBe(false)
|
|
44
|
-
expect(isDisabled(user("live"), now)).toBe(false)
|
|
44
|
+
await expect(directory.find("u-1")).rejects.toMatchObject({ code: "DATABASE_RETURNING_ROW_REJECTED" })
|
|
45
45
|
})
|
|
46
46
|
})
|
|
@@ -2,27 +2,18 @@
|
|
|
2
2
|
import { Injectable } from "@nestjs/common"
|
|
3
3
|
import type { EntityManager } from "typeorm"
|
|
4
4
|
import type { AccountDirectory, AccountStanding } from "@modules/integrations/passport"
|
|
5
|
-
import { InjectClock } from "@modules/platform/clock"
|
|
6
|
-
import type { Clock } from "@modules/platform/clock"
|
|
7
5
|
import { InjectPrimaryEntityManager } from "@modules/platform/database"
|
|
8
|
-
import {
|
|
9
|
-
|
|
10
|
-
/** True when the account is deleted or banned at this instant. */
|
|
11
|
-
export const isDisabled = (user: AuthUserEntity, now: Date): boolean =>
|
|
12
|
-
user.deletedAt !== null || (user.bannedUntil !== null && user.bannedUntil > now)
|
|
6
|
+
import { accountDoors } from "./persistence/account-doors.port"
|
|
13
7
|
|
|
14
8
|
@Injectable()
|
|
15
|
-
/** The package's account directory over
|
|
9
|
+
/** The package's account directory over the account doors: whether an account still stands, for a refresh to go on. */
|
|
16
10
|
export class IdentityAccountDirectoryService implements AccountDirectory {
|
|
17
|
-
constructor(
|
|
18
|
-
@InjectPrimaryEntityManager() private readonly entityManager: EntityManager,
|
|
19
|
-
@InjectClock() private readonly clock: Clock,
|
|
20
|
-
) {}
|
|
11
|
+
constructor(@InjectPrimaryEntityManager() private readonly entityManager: EntityManager) {}
|
|
21
12
|
|
|
22
|
-
/** The standing of one account, or undefined when it does not exist. */
|
|
13
|
+
/** The standing of one account (disabled is decided by the database clock), or undefined when it does not exist. */
|
|
23
14
|
async find(userId: string): Promise<AccountStanding | undefined> {
|
|
24
|
-
const
|
|
25
|
-
if (
|
|
26
|
-
return { id:
|
|
15
|
+
const account = await accountDoors.userById(this.entityManager, userId)
|
|
16
|
+
if (account === undefined) return undefined
|
|
17
|
+
return { id: account.id, email: account.email, disabled: account.disabled }
|
|
27
18
|
}
|
|
28
19
|
}
|
|
@@ -8,16 +8,25 @@ import type { AuthPasswords, GoogleProfile, SignInBudget } from "@modules/integr
|
|
|
8
8
|
import { CLOCK } from "@modules/platform/clock"
|
|
9
9
|
import { PRIMARY_ENTITY_MANAGER } from "@modules/platform/database"
|
|
10
10
|
import { IDS } from "@modules/platform/ids"
|
|
11
|
+
import { LOGGER } from "@modules/platform/logging"
|
|
12
|
+
import type { Logger } from "@modules/platform/logging"
|
|
11
13
|
import { IdentityError, IdentityErrorCode } from "./errors/identity.error"
|
|
12
14
|
import { IDENTITY_AUTH_PASSWORDS, IDENTITY_SIGN_IN_BUDGET } from "./identity.decorators"
|
|
13
|
-
import {
|
|
15
|
+
import { IdentitySignInLogEvent } from "./identity-sign-in.log-events"
|
|
16
|
+
import { accountByEmailRow, accountByIdRow } from "@tests/fixtures/builders/identity.builder"
|
|
14
17
|
import { IdentityAuthenticatorService } from "./authenticator.service"
|
|
15
|
-
|
|
18
|
+
|
|
19
|
+
/** The statements callDbFunction sends for the account doors. */
|
|
20
|
+
const USER_BY_EMAIL = 'SELECT * FROM "public"."be_auth_user_by_email"($1)'
|
|
21
|
+
const USER_BY_ID = 'SELECT * FROM "public"."be_auth_user_by_id"($1)'
|
|
22
|
+
const USER_BY_GOOGLE = 'SELECT * FROM "public"."be_auth_user_by_google"($1)'
|
|
23
|
+
const CREATE_USER = 'SELECT * FROM "public"."be_auth_create_user"($1, $2, $3, $4, $5, $6)'
|
|
24
|
+
const LINK_GOOGLE = 'SELECT * FROM "public"."be_auth_link_google"($1, $2, $3, $4)'
|
|
25
|
+
const TOUCH_USER = 'SELECT * FROM "public"."be_auth_touch_user"($1, $2, $3, $4)'
|
|
16
26
|
|
|
17
27
|
const NEW_ID = "00000000-0000-4000-8000-000000000001"
|
|
18
28
|
const AT = "2026-02-03T04:05:06.000Z"
|
|
19
29
|
const NOW = new Date(AT)
|
|
20
|
-
const BEFORE = new Date("2026-02-03T04:00:00.000Z")
|
|
21
30
|
|
|
22
31
|
const profile = (patch: Partial<GoogleProfile> = {}): GoogleProfile => ({
|
|
23
32
|
subject: "g-1",
|
|
@@ -28,11 +37,16 @@ const profile = (patch: Partial<GoogleProfile> = {}): GoogleProfile => ({
|
|
|
28
37
|
...patch,
|
|
29
38
|
})
|
|
30
39
|
|
|
31
|
-
|
|
40
|
+
type Stubs = ReadonlyArray<readonly [string, unknown]>
|
|
41
|
+
|
|
42
|
+
const build = async (answers: Stubs, matches = true) => {
|
|
43
|
+
const entityManager: MockEntityManager = answers.length === 0 ? mockEntityManager() : mockEntityManager({ query: answers })
|
|
44
|
+
fakeTransaction(entityManager)
|
|
32
45
|
const passwords = mock<AuthPasswords>()
|
|
33
46
|
passwords.verify.mockResolvedValue(matches)
|
|
34
47
|
const budget = mock<SignInBudget>()
|
|
35
48
|
budget.counted.mockImplementation(async (_kind, _address, check) => (await check()).value)
|
|
49
|
+
const logger = mock<Logger>()
|
|
36
50
|
const moduleRef = await Test.createTestingModule({
|
|
37
51
|
providers: [
|
|
38
52
|
IdentityAuthenticatorService,
|
|
@@ -41,105 +55,155 @@ const build = async (entityManager: MockEntityManager, matches = true) => {
|
|
|
41
55
|
{ provide: IDENTITY_SIGN_IN_BUDGET, useValue: budget },
|
|
42
56
|
{ provide: CLOCK, useValue: new FakeClock(AT) },
|
|
43
57
|
{ provide: IDS, useValue: fakeIds() },
|
|
58
|
+
{ provide: LOGGER, useValue: logger },
|
|
44
59
|
],
|
|
45
60
|
}).compile()
|
|
46
|
-
return { subject: moduleRef.get(IdentityAuthenticatorService), passwords, budget }
|
|
61
|
+
return { subject: moduleRef.get(IdentityAuthenticatorService), entityManager, passwords, budget, logger }
|
|
47
62
|
}
|
|
48
63
|
|
|
49
64
|
const refusal = (code: string): unknown => expect.objectContaining({ code })
|
|
50
65
|
|
|
51
66
|
describe("IdentityAuthenticatorService.verifyPassword", () => {
|
|
52
|
-
it("answers the person of a right pair, normalising the address, and records the sign-in", async () => {
|
|
53
|
-
const
|
|
54
|
-
|
|
67
|
+
it("answers the person of a right pair, normalising the address, and records the sign-in through the door", async () => {
|
|
68
|
+
const { subject, entityManager, passwords, budget } = await build([
|
|
69
|
+
[USER_BY_EMAIL, [accountByEmailRow()]],
|
|
70
|
+
[TOUCH_USER, []],
|
|
71
|
+
])
|
|
55
72
|
|
|
56
73
|
expect(await subject.verifyPassword(" A@Example.test ", "correct horse")).toEqual({ userId: "u-1", email: "a@example.test" })
|
|
57
74
|
|
|
58
75
|
expect(budget.counted).toHaveBeenCalledWith("login", "a@example.test", expect.any(Function))
|
|
59
76
|
expect(passwords.verify).toHaveBeenCalledWith("correct horse", "hash")
|
|
60
|
-
expect(entityManager.
|
|
77
|
+
expect(entityManager.query).toHaveBeenCalledWith(USER_BY_EMAIL, ["a@example.test"])
|
|
78
|
+
expect(entityManager.query).toHaveBeenCalledWith(TOUCH_USER, ["u-1", NOW, false, "{}"])
|
|
61
79
|
})
|
|
62
80
|
|
|
63
81
|
it("refuses an unknown address and a wrong password with the same code, and still spends the check on a decoy", async () => {
|
|
64
|
-
const unknown = await build(
|
|
65
|
-
const wrong = await build(
|
|
82
|
+
const unknown = await build([[USER_BY_EMAIL, []]])
|
|
83
|
+
const wrong = await build([[USER_BY_EMAIL, [accountByEmailRow()]]], false)
|
|
66
84
|
|
|
67
85
|
await expect(unknown.subject.verifyPassword("a@example.test", "pw")).rejects.toEqual(refusal(IdentityErrorCode.PairRefused))
|
|
68
86
|
await expect(wrong.subject.verifyPassword("a@example.test", "pw")).rejects.toBeInstanceOf(IdentityError)
|
|
69
87
|
expect(unknown.passwords.verify).toHaveBeenCalledWith("pw", "")
|
|
88
|
+
expect(wrong.entityManager.query).not.toHaveBeenCalledWith(TOUCH_USER, expect.anything())
|
|
70
89
|
})
|
|
71
90
|
|
|
72
91
|
it("compares an empty password for one bcrypt cannot read, an account without a hash against the decoy", async () => {
|
|
73
|
-
const { subject, passwords } = await build(
|
|
92
|
+
const { subject, passwords } = await build([[USER_BY_EMAIL, [accountByEmailRow("passwordless")]]], false)
|
|
74
93
|
|
|
75
94
|
await expect(subject.verifyPassword("a@example.test", "x".repeat(100))).rejects.toBeInstanceOf(IdentityError)
|
|
76
95
|
|
|
77
96
|
expect(passwords.verify).toHaveBeenCalledWith("", "")
|
|
78
97
|
})
|
|
79
98
|
|
|
80
|
-
it("refuses a
|
|
81
|
-
const
|
|
82
|
-
const unconfirmed = await build(
|
|
83
|
-
const addressless = await build(mockEntityManager({ findOne: [AuthUserEntity, user("addressless")] }))
|
|
99
|
+
it("refuses a disabled account and an unconfirmed one after the password was right, recording no sign-in", async () => {
|
|
100
|
+
const disabled = await build([[USER_BY_EMAIL, [accountByEmailRow("disabled")]]])
|
|
101
|
+
const unconfirmed = await build([[USER_BY_EMAIL, [accountByEmailRow("unconfirmed")]]])
|
|
84
102
|
|
|
85
|
-
await expect(
|
|
103
|
+
await expect(disabled.subject.verifyPassword("a@example.test", "pw")).rejects.toEqual(refusal(IdentityErrorCode.AccountDisabled))
|
|
86
104
|
await expect(unconfirmed.subject.verifyPassword("a@example.test", "pw")).rejects.toEqual(refusal(IdentityErrorCode.EmailNotConfirmed))
|
|
87
|
-
|
|
105
|
+
expect(disabled.entityManager.query).not.toHaveBeenCalledWith(TOUCH_USER, expect.anything())
|
|
106
|
+
expect(unconfirmed.entityManager.query).not.toHaveBeenCalledWith(TOUCH_USER, expect.anything())
|
|
88
107
|
})
|
|
89
108
|
})
|
|
90
109
|
|
|
91
110
|
describe("IdentityAuthenticatorService.signInWithGoogle", () => {
|
|
92
|
-
it("refuses a profile without a verified address", async () => {
|
|
93
|
-
const { subject } = await build(
|
|
111
|
+
it("refuses a profile without a verified address, and calls no door", async () => {
|
|
112
|
+
const { subject, entityManager } = await build([])
|
|
94
113
|
|
|
95
114
|
await expect(subject.signInWithGoogle(profile({ emailVerified: false }))).rejects.toEqual(refusal(PassportErrorCode.GoogleRejected))
|
|
96
115
|
await expect(subject.signInWithGoogle(profile({ email: null }))).rejects.toEqual(refusal(PassportErrorCode.GoogleRejected))
|
|
116
|
+
expect(entityManager.query).not.toHaveBeenCalled()
|
|
97
117
|
})
|
|
98
118
|
|
|
99
|
-
it("signs in the account a Google subject is already linked to", async () => {
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
119
|
+
it("signs in the account a Google subject is already linked to, confirming its mailbox", async () => {
|
|
120
|
+
const { subject, entityManager } = await build([
|
|
121
|
+
[USER_BY_GOOGLE, [{ be_auth_user_by_google: "u-1" }]],
|
|
122
|
+
[LINK_GOOGLE, []],
|
|
123
|
+
[TOUCH_USER, []],
|
|
124
|
+
[USER_BY_ID, [accountByIdRow()]],
|
|
125
|
+
])
|
|
104
126
|
|
|
105
127
|
expect(await subject.signInWithGoogle(profile())).toEqual({ userId: "u-1", email: "a@example.test" })
|
|
106
128
|
|
|
107
|
-
expect(entityManager.
|
|
108
|
-
expect(entityManager.
|
|
129
|
+
expect(entityManager.query).not.toHaveBeenCalledWith(USER_BY_EMAIL, expect.anything())
|
|
130
|
+
expect(entityManager.query).not.toHaveBeenCalledWith(CREATE_USER, expect.anything())
|
|
131
|
+
expect(entityManager.query).toHaveBeenCalledWith(TOUCH_USER, ["u-1", NOW, true, expect.stringContaining('"full_name":"A"')])
|
|
109
132
|
})
|
|
110
133
|
|
|
111
|
-
it("links an existing account by its verified address
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
134
|
+
it("links an existing account by its verified address", async () => {
|
|
135
|
+
const { subject, entityManager } = await build([
|
|
136
|
+
[USER_BY_GOOGLE, [{ be_auth_user_by_google: null }]],
|
|
137
|
+
[USER_BY_EMAIL, [accountByEmailRow()]],
|
|
138
|
+
[LINK_GOOGLE, []],
|
|
139
|
+
[TOUCH_USER, []],
|
|
140
|
+
[USER_BY_ID, [accountByIdRow()]],
|
|
141
|
+
])
|
|
115
142
|
|
|
116
143
|
expect(await subject.signInWithGoogle(profile())).toEqual({ userId: "u-1", email: "a@example.test" })
|
|
117
144
|
|
|
118
|
-
expect(entityManager.
|
|
119
|
-
expect(entityManager.
|
|
145
|
+
expect(entityManager.query).toHaveBeenCalledWith(USER_BY_EMAIL, ["a@example.test"])
|
|
146
|
+
expect(entityManager.query).toHaveBeenCalledWith(LINK_GOOGLE, ["u-1", "g-1", expect.stringContaining('"sub":"g-1"'), NOW])
|
|
147
|
+
expect(entityManager.query).not.toHaveBeenCalledWith(CREATE_USER, expect.anything())
|
|
120
148
|
})
|
|
121
149
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
150
|
+
})
|
|
151
|
+
|
|
152
|
+
describe("IdentityAuthenticatorService.signInWithGoogle (new accounts and refusals)", () => {
|
|
153
|
+
it("creates the account of a new person, links their Google identity and logs the id alone", async () => {
|
|
154
|
+
const { subject, entityManager, logger } = await build([
|
|
155
|
+
[USER_BY_GOOGLE, [{ be_auth_user_by_google: null }]],
|
|
156
|
+
[USER_BY_EMAIL, []],
|
|
157
|
+
[CREATE_USER, [{ be_auth_create_user: NEW_ID }]],
|
|
158
|
+
[LINK_GOOGLE, []],
|
|
159
|
+
[TOUCH_USER, []],
|
|
160
|
+
[USER_BY_ID, [accountByIdRow("live", NEW_ID)]],
|
|
161
|
+
])
|
|
130
162
|
|
|
131
163
|
expect(await subject.signInWithGoogle(profile())).toEqual({ userId: NEW_ID, email: "a@example.test" })
|
|
132
164
|
|
|
133
|
-
expect(entityManager.
|
|
165
|
+
expect(entityManager.query).toHaveBeenCalledWith(CREATE_USER, [NEW_ID, "a@example.test", null, true, expect.stringContaining('"full_name":"A"'), NOW])
|
|
166
|
+
expect(entityManager.query).toHaveBeenCalledWith(LINK_GOOGLE, [NEW_ID, "g-1", expect.stringContaining("g-1"), NOW])
|
|
167
|
+
expect(logger.info).toHaveBeenCalledWith(IdentitySignInLogEvent.AccountCreated, { userId: NEW_ID })
|
|
134
168
|
})
|
|
135
169
|
|
|
136
|
-
it("
|
|
137
|
-
const
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
170
|
+
it("fills only the profile keys the Google profile has", async () => {
|
|
171
|
+
const { subject, entityManager } = await build([
|
|
172
|
+
[USER_BY_GOOGLE, [{ be_auth_user_by_google: "u-1" }]],
|
|
173
|
+
[LINK_GOOGLE, []],
|
|
174
|
+
[TOUCH_USER, []],
|
|
175
|
+
[USER_BY_ID, [accountByIdRow()]],
|
|
176
|
+
])
|
|
177
|
+
|
|
178
|
+
await subject.signInWithGoogle(profile({ name: null, picture: "https://img.example.test/a.png" }))
|
|
141
179
|
|
|
142
|
-
|
|
143
|
-
|
|
180
|
+
const keys = { avatar_url: "https://img.example.test/a.png", picture: "https://img.example.test/a.png", email: "A@Example.test", email_verified: true }
|
|
181
|
+
expect(entityManager.query).toHaveBeenCalledWith(TOUCH_USER, ["u-1", NOW, true, JSON.stringify(keys)])
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
it("refuses when the create door answers no account", async () => {
|
|
185
|
+
const { subject, entityManager } = await build([
|
|
186
|
+
[USER_BY_GOOGLE, [{ be_auth_user_by_google: null }]],
|
|
187
|
+
[USER_BY_EMAIL, []],
|
|
188
|
+
[CREATE_USER, []],
|
|
189
|
+
])
|
|
190
|
+
|
|
191
|
+
await expect(subject.signInWithGoogle(profile())).rejects.toEqual(refusal(PassportErrorCode.GoogleRejected))
|
|
192
|
+
expect(entityManager.query).not.toHaveBeenCalledWith(LINK_GOOGLE, expect.anything())
|
|
193
|
+
})
|
|
194
|
+
|
|
195
|
+
it("refuses a disabled account and an account that cannot be read back", async () => {
|
|
196
|
+
const linked: Stubs = [
|
|
197
|
+
[USER_BY_GOOGLE, [{ be_auth_user_by_google: "u-1" }]],
|
|
198
|
+
[LINK_GOOGLE, []],
|
|
199
|
+
[TOUCH_USER, []],
|
|
200
|
+
]
|
|
201
|
+
const disabled = await build([...linked, [USER_BY_ID, [accountByIdRow("disabled")]]])
|
|
202
|
+
const gone = await build([...linked, [USER_BY_ID, []]])
|
|
203
|
+
const addressless = await build([...linked, [USER_BY_ID, [accountByIdRow("addressless")]]])
|
|
204
|
+
|
|
205
|
+
await expect(disabled.subject.signInWithGoogle(profile())).rejects.toEqual(refusal(IdentityErrorCode.AccountDisabled))
|
|
206
|
+
await expect(gone.subject.signInWithGoogle(profile())).rejects.toEqual(refusal(PassportErrorCode.GoogleRejected))
|
|
207
|
+
await expect(addressless.subject.signInWithGoogle(profile())).rejects.toEqual(refusal(PassportErrorCode.GoogleRejected))
|
|
144
208
|
})
|
|
145
209
|
})
|
|
@@ -8,15 +8,26 @@ import type { Clock } from "@modules/platform/clock"
|
|
|
8
8
|
import { InjectPrimaryEntityManager } from "@modules/platform/database"
|
|
9
9
|
import { InjectIds } from "@modules/platform/ids"
|
|
10
10
|
import type { Ids } from "@modules/platform/ids"
|
|
11
|
-
import {
|
|
11
|
+
import { InjectLogger } from "@modules/platform/logging"
|
|
12
|
+
import type { Logger } from "@modules/platform/logging"
|
|
12
13
|
import { IdentityError, IdentityErrorCode } from "./errors/identity.error"
|
|
13
14
|
import { InjectAuthPasswords, InjectSignInBudget } from "./identity.decorators"
|
|
14
|
-
import {
|
|
15
|
+
import { IdentitySignInLogEvent } from "./identity-sign-in.log-events"
|
|
16
|
+
import { accountDoors } from "./persistence/account-doors.port"
|
|
15
17
|
|
|
16
|
-
|
|
18
|
+
/** A Google profile whose email Google verified, so it has one. */
|
|
19
|
+
type VerifiedProfile = GoogleProfile & { readonly email: string }
|
|
20
|
+
|
|
21
|
+
/** The profile keys a Google sign-in may fill on the account; they never overwrite what the person already set. */
|
|
22
|
+
const profileKeysOf = (profile: VerifiedProfile): Record<string, string | boolean> => ({
|
|
23
|
+
...(profile.name === null ? {} : { full_name: profile.name, name: profile.name }),
|
|
24
|
+
...(profile.picture === null ? {} : { avatar_url: profile.picture, picture: profile.picture }),
|
|
25
|
+
email: profile.email,
|
|
26
|
+
email_verified: profile.emailVerified,
|
|
27
|
+
})
|
|
17
28
|
|
|
18
29
|
@Injectable()
|
|
19
|
-
/** The two sign-in decisions: is this email and password right, and which account does this Google profile stand for. */
|
|
30
|
+
/** The two sign-in decisions over the account doors: is this email and password right, and which account does this Google profile stand for. */
|
|
20
31
|
export class IdentityAuthenticatorService implements AuthAuthenticator {
|
|
21
32
|
constructor(
|
|
22
33
|
@InjectPrimaryEntityManager() private readonly entityManager: EntityManager,
|
|
@@ -24,72 +35,50 @@ export class IdentityAuthenticatorService implements AuthAuthenticator {
|
|
|
24
35
|
@InjectSignInBudget() private readonly budget: SignInBudget,
|
|
25
36
|
@InjectClock() private readonly clock: Clock,
|
|
26
37
|
@InjectIds() private readonly ids: Ids,
|
|
38
|
+
@InjectLogger() private readonly logger: Logger,
|
|
27
39
|
) {}
|
|
28
40
|
|
|
29
41
|
/** Verifies the pair. An unknown address and a wrong password answer the same refusal; only wrong passwords count against the budget. */
|
|
30
42
|
async verifyPassword(email: string, password: string): Promise<AuthSubject> {
|
|
31
43
|
const address = normalizeEmail(email)
|
|
32
|
-
const
|
|
33
|
-
const found = await this.entityManager
|
|
34
|
-
const matches = await this.passwords.verify(isPresentablePassword(password) ? password : "", found?.
|
|
35
|
-
return { passed: found !==
|
|
44
|
+
const account = await this.budget.counted("login", address, async () => {
|
|
45
|
+
const found = await accountDoors.userByEmail(this.entityManager, address)
|
|
46
|
+
const matches = await this.passwords.verify(isPresentablePassword(password) ? password : "", found?.encrypted_password ?? "")
|
|
47
|
+
return { passed: found !== undefined && matches, value: found !== undefined && matches ? found : undefined }
|
|
36
48
|
})
|
|
37
|
-
if (
|
|
38
|
-
|
|
39
|
-
if (
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
return { userId: user.id, email: user.email }
|
|
49
|
+
if (account === undefined) throw new IdentityError({ code: IdentityErrorCode.PairRefused })
|
|
50
|
+
if (account.disabled) throw new IdentityError({ code: IdentityErrorCode.AccountDisabled })
|
|
51
|
+
if (account.email_confirmed_at === null) throw new IdentityError({ code: IdentityErrorCode.EmailNotConfirmed })
|
|
52
|
+
await accountDoors.touchUser(this.entityManager, account.id, this.clock.now(), false, "{}")
|
|
53
|
+
return { userId: account.id, email: account.email }
|
|
43
54
|
}
|
|
44
55
|
|
|
45
56
|
/** Finds the account a Google subject is linked to, else links by a verified email, else creates the account. */
|
|
46
57
|
async signInWithGoogle(profile: GoogleProfile): Promise<AuthSubject> {
|
|
47
58
|
if (profile.email === null || !profile.emailVerified) throw new PassportError({ code: PassportErrorCode.GoogleRejected })
|
|
48
59
|
const address = normalizeEmail(profile.email)
|
|
60
|
+
const verified: VerifiedProfile = { ...profile, email: profile.email }
|
|
49
61
|
const now = this.clock.now()
|
|
50
|
-
const
|
|
51
|
-
const linked = await
|
|
52
|
-
const
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
id: this.ids.next(),
|
|
59
|
-
providerId: profile.subject,
|
|
60
|
-
userId: person.id,
|
|
61
|
-
provider: GOOGLE,
|
|
62
|
-
identityData: { sub: profile.subject, email: address, name: profile.name, picture: profile.picture, email_verified: true },
|
|
63
|
-
lastSignInAt: now,
|
|
64
|
-
createdAt: now,
|
|
65
|
-
updatedAt: now,
|
|
66
|
-
})
|
|
67
|
-
}
|
|
68
|
-
await scoped.update(AuthUserEntity, { id: person.id }, { lastSignInAt: now, emailConfirmedAt: person.emailConfirmedAt ?? now })
|
|
69
|
-
return person
|
|
62
|
+
const userId = await this.entityManager.transaction(async (scoped) => {
|
|
63
|
+
const linked = await accountDoors.userByGoogle(scoped, profile.subject)
|
|
64
|
+
const byEmail = linked === null ? await accountDoors.userByEmail(scoped, address) : undefined
|
|
65
|
+
const id = linked ?? byEmail?.id ?? (await this.createFor(scoped, address, verified, now))
|
|
66
|
+
const identityData = { sub: profile.subject, email: address, name: profile.name, picture: profile.picture, email_verified: true }
|
|
67
|
+
await accountDoors.linkGoogle(scoped, id, profile.subject, JSON.stringify(identityData), now)
|
|
68
|
+
await accountDoors.touchUser(scoped, id, now, true, JSON.stringify(profileKeysOf(verified)))
|
|
69
|
+
return id
|
|
70
70
|
})
|
|
71
|
-
|
|
72
|
-
|
|
71
|
+
const account = await accountDoors.userById(this.entityManager, userId)
|
|
72
|
+
if (account?.email === undefined || account.email === null) throw new PassportError({ code: PassportErrorCode.GoogleRejected })
|
|
73
|
+
if (account.disabled) throw new IdentityError({ code: IdentityErrorCode.AccountDisabled })
|
|
74
|
+
return { userId: account.id, email: account.email }
|
|
73
75
|
}
|
|
74
76
|
|
|
75
|
-
private async
|
|
77
|
+
private async createFor(scoped: EntityManager, address: string, profile: VerifiedProfile, now: Date): Promise<string> {
|
|
76
78
|
const id = this.ids.next()
|
|
77
|
-
await scoped.
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
role: "authenticated",
|
|
81
|
-
email: address,
|
|
82
|
-
encryptedPassword: null,
|
|
83
|
-
emailConfirmedAt: now,
|
|
84
|
-
lastSignInAt: now,
|
|
85
|
-
bannedUntil: null,
|
|
86
|
-
deletedAt: null,
|
|
87
|
-
isAnonymous: false,
|
|
88
|
-
createdAt: now,
|
|
89
|
-
updatedAt: now,
|
|
90
|
-
})
|
|
91
|
-
const created = await scoped.findOne(AuthUserEntity, { where: { id } })
|
|
92
|
-
if (created === null) throw new PassportError({ code: PassportErrorCode.GoogleRejected })
|
|
79
|
+
const created = await accountDoors.createUser(scoped, { id, email: address, hash: null, confirmed: true, meta: JSON.stringify(profileKeysOf(profile)), now })
|
|
80
|
+
if (created === undefined) throw new PassportError({ code: PassportErrorCode.GoogleRejected })
|
|
81
|
+
this.logger.info(IdentitySignInLogEvent.AccountCreated, { userId: created })
|
|
93
82
|
return created
|
|
94
83
|
}
|
|
95
84
|
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// Managed by starci app sync (slot be.identity.sign-in); do not edit: run "npx starci app sync --execute".
|
|
2
|
+
/** Log events owned by the email doors of sign-in. */
|
|
3
|
+
export enum IdentityEmailLogEvent {
|
|
4
|
+
/** A confirmation or reset mail could not be sent; the door still answers as if it was, so an address is never confirmed or denied. */
|
|
5
|
+
MailFailed = "identity.email.mail_failed",
|
|
6
|
+
}
|