@starci/hfs 5.2.0 → 5.3.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 +34 -0
- package/README.md +1 -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 +128 -0
- package/codemod/main.mjs +31 -1
- package/codemod/module-definition.d.mts +52 -0
- package/codemod/module-definition.mjs +242 -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/hygiene.mjs +3 -1
- package/sync/index.d.mts +2 -0
- package/sync/index.mjs +33 -2
- 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 +24 -4
- 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/index.ts +3 -10
- 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
|
@@ -7,10 +7,15 @@ import { LogoutHandler } from "./application/logout.handler"
|
|
|
7
7
|
import { GoogleBeginHandler } from "./application/google-begin.handler"
|
|
8
8
|
import { GoogleCompleteHandler } from "./application/google-complete.handler"
|
|
9
9
|
import { GetJwksHandler } from "./application/get-jwks.handler"
|
|
10
|
+
import { SignupHandler } from "./application/signup.handler"
|
|
11
|
+
import { ResendHandler } from "./application/resend.handler"
|
|
12
|
+
import { ConfirmHandler } from "./application/confirm.handler"
|
|
13
|
+
import { ForgotHandler } from "./application/forgot.handler"
|
|
14
|
+
import { ResetHandler } from "./application/reset.handler"
|
|
10
15
|
import { ConfigurableModuleClass } from "./auth.module-definition"
|
|
11
16
|
|
|
12
17
|
@Module({
|
|
13
|
-
providers: [LoginHandler, RefreshHandler, SessionHandler, LogoutHandler, GoogleBeginHandler, GoogleCompleteHandler, GetJwksHandler],
|
|
18
|
+
providers: [LoginHandler, RefreshHandler, SessionHandler, LogoutHandler, GoogleBeginHandler, GoogleCompleteHandler, GetJwksHandler, SignupHandler, ResendHandler, ConfirmHandler, ForgotHandler, ResetHandler],
|
|
14
19
|
})
|
|
15
20
|
/** The auth feature: the handlers behind the sign-in doors. */
|
|
16
21
|
export class AuthApiModule extends ConfigurableModuleClass {}
|
package/templates/be/auth-lite/src/features/api/auth/transport/http/auth-http.module-definition.ts
CHANGED
|
@@ -1,13 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
import { ConfigurableModuleBuilder, type ConfigurableModuleHost } from "@nestjs/common"
|
|
1
|
+
import { defineConfigurableModule } from "@starci/nest-kit"
|
|
3
2
|
import type { AuthHttpOptions } from "./auth-http.options"
|
|
4
3
|
|
|
5
|
-
const built = new ConfigurableModuleBuilder<AuthHttpOptions>().setClassMethodName("register").build()
|
|
6
|
-
type Host = ConfigurableModuleHost<AuthHttpOptions, "register", "create", {}>
|
|
7
|
-
|
|
8
4
|
/** The configurable-module base of the auth-http module. */
|
|
9
|
-
export const
|
|
10
|
-
|
|
11
|
-
export const MODULE_OPTIONS_TOKEN: Host["MODULE_OPTIONS_TOKEN"] = built.MODULE_OPTIONS_TOKEN
|
|
12
|
-
/** The options of a synchronous registration, for the register signature. */
|
|
13
|
-
export const OPTIONS_TYPE: Host["OPTIONS_TYPE"] = built.OPTIONS_TYPE
|
|
5
|
+
export const { ConfigurableModuleClass, MODULE_OPTIONS_TOKEN, OPTIONS_TYPE } =
|
|
6
|
+
defineConfigurableModule<AuthHttpOptions>()
|
|
@@ -6,6 +6,11 @@ import { Public, PublicReason } from "@modules/domain/identity"
|
|
|
6
6
|
import type { AccessGrant, GoogleRedirect, JwksDocument } from "@modules/integrations/passport"
|
|
7
7
|
import { InjectCommandBus, InjectQueryBus } from "@modules/platform/cqrs"
|
|
8
8
|
import { RateLimit, RateTier } from "@modules/platform/http-security"
|
|
9
|
+
import { SignupCommand } from "../../application/signup.command"
|
|
10
|
+
import { ResendCommand } from "../../application/resend.command"
|
|
11
|
+
import { ConfirmCommand } from "../../application/confirm.command"
|
|
12
|
+
import { ForgotCommand } from "../../application/forgot.command"
|
|
13
|
+
import { ResetCommand } from "../../application/reset.command"
|
|
9
14
|
import { GetJwksQuery } from "../../application/get-jwks.query"
|
|
10
15
|
import { GoogleBeginCommand } from "../../application/google-begin.command"
|
|
11
16
|
import { GoogleCompleteCommand } from "../../application/google-complete.command"
|
|
@@ -13,11 +18,16 @@ import { LoginCommand } from "../../application/login.command"
|
|
|
13
18
|
import { LogoutCommand } from "../../application/logout.command"
|
|
14
19
|
import { RefreshCommand } from "../../application/refresh.command"
|
|
15
20
|
import { SessionCommand } from "../../application/session.command"
|
|
21
|
+
import { ForgotRequest } from "./dto/forgot.request"
|
|
16
22
|
import { GoogleStartRequest } from "./dto/google-start.request"
|
|
17
23
|
import { LoginRequest } from "./dto/login.request"
|
|
24
|
+
import { ResendRequest } from "./dto/resend.request"
|
|
25
|
+
import { ResetRequest } from "./dto/reset.request"
|
|
26
|
+
import { SignupRequest } from "./dto/signup.request"
|
|
27
|
+
import { TokenRequest } from "./dto/token.request"
|
|
18
28
|
|
|
19
29
|
@Controller("auth")
|
|
20
|
-
/** The cookie doors of sign-in: email and password, Google, refresh, session, sign-out and the public keys. */
|
|
30
|
+
/** The cookie doors of sign-in: email and password, the email doors (sign up, confirm, resend, forgot, reset), Google, refresh, session, sign-out and the public keys. */
|
|
21
31
|
export class AuthController {
|
|
22
32
|
constructor(
|
|
23
33
|
@InjectCommandBus() private readonly commandBus: CommandBus,
|
|
@@ -80,6 +90,51 @@ export class AuthController {
|
|
|
80
90
|
return this.commandBus.execute(new GoogleCompleteCommand({ request: { exchange: { req, res } } }))
|
|
81
91
|
}
|
|
82
92
|
|
|
93
|
+
/** Creates an unconfirmed account and mails the confirmation link; always answers 204. */
|
|
94
|
+
@Post("signup")
|
|
95
|
+
@HttpCode(204)
|
|
96
|
+
@Public({ reason: PublicReason.AuthHandshake })
|
|
97
|
+
@RateLimit(RateTier.Strict)
|
|
98
|
+
signup(@Body() body: SignupRequest): Promise<void> {
|
|
99
|
+
return this.commandBus.execute(new SignupCommand({ request: { email: body.email, password: body.password, language: body.language } }))
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/** Mails a new confirmation link; always answers 204. */
|
|
103
|
+
@Post("resend")
|
|
104
|
+
@HttpCode(204)
|
|
105
|
+
@Public({ reason: PublicReason.AuthHandshake })
|
|
106
|
+
@RateLimit(RateTier.Strict)
|
|
107
|
+
resend(@Body() body: ResendRequest): Promise<void> {
|
|
108
|
+
return this.commandBus.execute(new ResendCommand({ request: { email: body.email, language: body.language } }))
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Spends a confirmation link; a spent, expired or unknown link is refused with IDENTITY_RESET_TOKEN_INVALID. */
|
|
112
|
+
@Post("confirm")
|
|
113
|
+
@HttpCode(204)
|
|
114
|
+
@Public({ reason: PublicReason.AuthHandshake })
|
|
115
|
+
@RateLimit(RateTier.Strict)
|
|
116
|
+
confirm(@Body() body: TokenRequest): Promise<void> {
|
|
117
|
+
return this.commandBus.execute(new ConfirmCommand({ request: { token: body.token } }))
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Mails a password reset link when the address has an account; always answers 204. */
|
|
121
|
+
@Post("forgot")
|
|
122
|
+
@HttpCode(204)
|
|
123
|
+
@Public({ reason: PublicReason.AuthHandshake })
|
|
124
|
+
@RateLimit(RateTier.Strict)
|
|
125
|
+
forgot(@Body() body: ForgotRequest): Promise<void> {
|
|
126
|
+
return this.commandBus.execute(new ForgotCommand({ request: { email: body.email, language: body.language } }))
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Spends a reset link and sets the new password; every session of the person ends. */
|
|
130
|
+
@Post("reset")
|
|
131
|
+
@HttpCode(204)
|
|
132
|
+
@Public({ reason: PublicReason.AuthHandshake })
|
|
133
|
+
@RateLimit(RateTier.Strict)
|
|
134
|
+
reset(@Body() body: ResetRequest): Promise<void> {
|
|
135
|
+
return this.commandBus.execute(new ResetCommand({ request: { token: body.token, password: body.password } }))
|
|
136
|
+
}
|
|
137
|
+
|
|
83
138
|
/** The public keys access tokens verify against. */
|
|
84
139
|
@Get("jwks")
|
|
85
140
|
@Public({ reason: PublicReason.AuthHandshake })
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Managed by starci app sync (slot be.feature.auth); do not edit: run "npx starci app sync --execute".
|
|
2
|
+
import { IsEmail, IsIn, IsOptional, MaxLength } from "class-validator"
|
|
3
|
+
import type { MailLanguage } from "@modules/integrations/mail"
|
|
4
|
+
|
|
5
|
+
/** The posted address of a password reset request. */
|
|
6
|
+
export class ForgotRequest {
|
|
7
|
+
/** The sign-in address. */
|
|
8
|
+
@IsEmail()
|
|
9
|
+
@MaxLength(254)
|
|
10
|
+
email!: string
|
|
11
|
+
|
|
12
|
+
/** The language of the mail; the app's default when left out. */
|
|
13
|
+
@IsOptional()
|
|
14
|
+
@IsIn(["vi", "en"])
|
|
15
|
+
language?: MailLanguage
|
|
16
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
// Managed by starci app sync (slot be.feature.auth); do not edit: run "npx starci app sync --execute".
|
|
2
|
+
import { IsEmail, IsIn, IsOptional, MaxLength } from "class-validator"
|
|
3
|
+
import type { MailLanguage } from "@modules/integrations/mail"
|
|
4
|
+
|
|
5
|
+
/** The posted address of a resend. */
|
|
6
|
+
export class ResendRequest {
|
|
7
|
+
/** The sign-in address. */
|
|
8
|
+
@IsEmail()
|
|
9
|
+
@MaxLength(254)
|
|
10
|
+
email!: string
|
|
11
|
+
|
|
12
|
+
/** The language of the mail; the app's default when left out. */
|
|
13
|
+
@IsOptional()
|
|
14
|
+
@IsIn(["vi", "en"])
|
|
15
|
+
language?: MailLanguage
|
|
16
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
// Managed by starci app sync (slot be.feature.auth); do not edit: run "npx starci app sync --execute".
|
|
2
|
+
import { IsString, MaxLength, MinLength } from "class-validator"
|
|
3
|
+
|
|
4
|
+
/** The posted link token and new password. */
|
|
5
|
+
export class ResetRequest {
|
|
6
|
+
/** The token the link carries. */
|
|
7
|
+
@IsString()
|
|
8
|
+
@MinLength(16)
|
|
9
|
+
@MaxLength(256)
|
|
10
|
+
token!: string
|
|
11
|
+
|
|
12
|
+
/** The new password. */
|
|
13
|
+
@IsString()
|
|
14
|
+
@MinLength(1)
|
|
15
|
+
@MaxLength(128)
|
|
16
|
+
password!: string
|
|
17
|
+
|
|
18
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
// Managed by starci app sync (slot be.feature.auth); do not edit: run "npx starci app sync --execute".
|
|
2
|
+
import { IsEmail, IsIn, IsOptional, IsString, MaxLength, MinLength } from "class-validator"
|
|
3
|
+
import type { MailLanguage } from "@modules/integrations/mail"
|
|
4
|
+
|
|
5
|
+
/** The posted sign-up data. */
|
|
6
|
+
export class SignupRequest {
|
|
7
|
+
/** The sign-in address. */
|
|
8
|
+
@IsEmail()
|
|
9
|
+
@MaxLength(254)
|
|
10
|
+
email!: string
|
|
11
|
+
|
|
12
|
+
/** The password. */
|
|
13
|
+
@IsString()
|
|
14
|
+
@MinLength(1)
|
|
15
|
+
@MaxLength(128)
|
|
16
|
+
password!: string
|
|
17
|
+
|
|
18
|
+
/** The language of the mail; the app's default when left out. */
|
|
19
|
+
@IsOptional()
|
|
20
|
+
@IsIn(["vi", "en"])
|
|
21
|
+
language?: MailLanguage
|
|
22
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
// Managed by starci app sync (slot be.feature.auth); do not edit: run "npx starci app sync --execute".
|
|
2
|
+
import { IsString, MaxLength, MinLength } from "class-validator"
|
|
3
|
+
|
|
4
|
+
/** The posted link token. */
|
|
5
|
+
export class TokenRequest {
|
|
6
|
+
/** The token the link carries. */
|
|
7
|
+
@IsString()
|
|
8
|
+
@MinLength(16)
|
|
9
|
+
@MaxLength(256)
|
|
10
|
+
token!: string
|
|
11
|
+
}
|
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
|
})
|