@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
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useLocale, useTranslations } from "next-intl"
|
|
4
|
+
import { useEmailDoor } from "@/hooks/emailAuth"
|
|
5
|
+
import type { EmailDoorFailure } from "@/hooks/emailAuth"
|
|
6
|
+
import { APP_ROUTES } from "@/modules/routes"
|
|
7
|
+
import { EmailDoorFormBase } from "./component"
|
|
8
|
+
import type { EmailDoorLink } from "./component"
|
|
9
|
+
|
|
10
|
+
/** The doors the block draws; the confirmation is a button, so the mailbox is confirmed by a person and not by a mail scanner opening the link. */
|
|
11
|
+
type FormDoor = "signup" | "resend" | "forgot" | "reset" | "confirm"
|
|
12
|
+
|
|
13
|
+
/** The door the block draws and, for the two link doors, the token of the link. */
|
|
14
|
+
type EmailDoorFormProps = { readonly door: FormDoor; readonly token?: string | undefined }
|
|
15
|
+
|
|
16
|
+
/** The words of one door. */
|
|
17
|
+
interface DoorCopy {
|
|
18
|
+
readonly title: string
|
|
19
|
+
readonly description: string
|
|
20
|
+
readonly submit: string
|
|
21
|
+
readonly done: string
|
|
22
|
+
readonly email: string | undefined
|
|
23
|
+
readonly password: string | undefined
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** The value of a field of the submitted form, or undefined when it is missing. */
|
|
27
|
+
const fieldOf = (input: FormData, name: string): string | undefined => {
|
|
28
|
+
const value = input.get(name)
|
|
29
|
+
return typeof value === "string" ? value : undefined
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The language a mail is written in: the page's, when the back end writes mail in it. */
|
|
33
|
+
const languageOf = (locale: string): "vi" | "en" | undefined => (locale === "vi" || locale === "en" ? locale : undefined)
|
|
34
|
+
|
|
35
|
+
/** One email door of the lite sign-in as a form: sign up, resend the confirmation, forgot the password, or choose a new one from a link. */
|
|
36
|
+
export const EmailDoorForm = (props: EmailDoorFormProps) => {
|
|
37
|
+
const t = useTranslations("app.emailAuth")
|
|
38
|
+
const locale = useLocale()
|
|
39
|
+
const { failure, done, pending, submit, reject } = useEmailDoor(props.door)
|
|
40
|
+
const copy = {
|
|
41
|
+
signup: { title: t("signup.title"), description: t("signup.description"), submit: t("signup.submit"), done: t("signup.done"), email: t("email"), password: t("password") },
|
|
42
|
+
resend: { title: t("resend.title"), description: t("resend.description"), submit: t("resend.submit"), done: t("resend.done"), email: t("email"), password: undefined },
|
|
43
|
+
forgot: { title: t("forgot.title"), description: t("forgot.description"), submit: t("forgot.submit"), done: t("forgot.done"), email: t("email"), password: undefined },
|
|
44
|
+
confirm: { title: t("confirm.title"), description: t("confirm.description"), submit: t("confirm.submit"), done: t("confirm.done"), email: undefined, password: undefined },
|
|
45
|
+
reset: { title: t("reset.title"), description: t("reset.description"), submit: t("reset.submit"), done: t("reset.done"), email: undefined, password: t("newPassword") },
|
|
46
|
+
} satisfies Record<FormDoor, DoorCopy>
|
|
47
|
+
const links = {
|
|
48
|
+
signup: [{ href: APP_ROUTES.home, label: t("toSignIn") }, { href: APP_ROUTES.resend, label: t("toResend") }],
|
|
49
|
+
resend: [{ href: APP_ROUTES.home, label: t("toSignIn") }],
|
|
50
|
+
forgot: [{ href: APP_ROUTES.home, label: t("toSignIn") }],
|
|
51
|
+
reset: [{ href: APP_ROUTES.home, label: t("toSignIn") }, { href: APP_ROUTES.forgot, label: t("toForgot") }],
|
|
52
|
+
confirm: [{ href: APP_ROUTES.home, label: t("toSignIn") }, { href: APP_ROUTES.resend, label: t("toResend") }],
|
|
53
|
+
} satisfies Record<FormDoor, readonly EmailDoorLink[]>
|
|
54
|
+
const failures = {
|
|
55
|
+
invalid: t("errors.invalid"),
|
|
56
|
+
password: t("errors.password"),
|
|
57
|
+
link: t("errors.link"),
|
|
58
|
+
tooMany: t("errors.tooMany"),
|
|
59
|
+
unavailable: t("errors.unavailable"),
|
|
60
|
+
} satisfies Record<EmailDoorFailure, string>
|
|
61
|
+
const chosen = copy[props.door]
|
|
62
|
+
const onSubmit = (input: FormData) => {
|
|
63
|
+
const email = fieldOf(input, "email")
|
|
64
|
+
const password = fieldOf(input, "password")
|
|
65
|
+
if ((chosen.email !== undefined && email === undefined) || (chosen.password !== undefined && password === undefined)) reject()
|
|
66
|
+
else submit({ email, password, token: props.token, language: languageOf(locale) })
|
|
67
|
+
}
|
|
68
|
+
return (
|
|
69
|
+
<EmailDoorFormBase
|
|
70
|
+
props={{
|
|
71
|
+
title: chosen.title,
|
|
72
|
+
description: chosen.description,
|
|
73
|
+
emailLabel: chosen.email,
|
|
74
|
+
passwordLabel: chosen.password,
|
|
75
|
+
submitLabel: chosen.submit,
|
|
76
|
+
pending,
|
|
77
|
+
errorMessage: failure === undefined ? undefined : failures[failure],
|
|
78
|
+
doneMessage: done ? chosen.done : undefined,
|
|
79
|
+
links: links[props.door],
|
|
80
|
+
}}
|
|
81
|
+
on={{ submit: onSubmit }}
|
|
82
|
+
/>
|
|
83
|
+
)
|
|
84
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ReactElement } from "react"
|
|
2
|
+
import type { Metadata } from "next"
|
|
3
|
+
import { Page } from "@starci/grammar/common"
|
|
4
|
+
import { getTranslations } from "next-intl/server"
|
|
5
|
+
import { EmailDoorForm } from "@/components/blocks/EmailDoorForm"
|
|
6
|
+
|
|
7
|
+
/** The document title of the confirm page. */
|
|
8
|
+
export const appConfirmMetadata = async (): Promise<Metadata> => {
|
|
9
|
+
const t = await getTranslations("app.emailAuth.confirm")
|
|
10
|
+
return { title: t("title") }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** The query of the page: the link of the mail carries its token there. */
|
|
14
|
+
type AppConfirmPageProps = { readonly searchParams: Promise<{ readonly token?: string | readonly string[] }> }
|
|
15
|
+
|
|
16
|
+
/** The confirm door of the lite sign-in, drawn as one Grammar Page (R325). */
|
|
17
|
+
export const AppConfirmPage = async (props: AppConfirmPageProps): Promise<ReactElement> => {
|
|
18
|
+
const { token } = await props.searchParams
|
|
19
|
+
const t = await getTranslations("app.emailAuth.confirm")
|
|
20
|
+
return (
|
|
21
|
+
<Page header={{ title: t("title") }} measure="reading">
|
|
22
|
+
<EmailDoorForm door="confirm" token={typeof token === "string" ? token : token?.[0]} />
|
|
23
|
+
</Page>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ReactElement } from "react"
|
|
2
|
+
import type { Metadata } from "next"
|
|
3
|
+
import { Page } from "@starci/grammar/common"
|
|
4
|
+
import { getTranslations } from "next-intl/server"
|
|
5
|
+
import { EmailDoorForm } from "@/components/blocks/EmailDoorForm"
|
|
6
|
+
|
|
7
|
+
/** The document title of the forgot page. */
|
|
8
|
+
export const appForgotMetadata = async (): Promise<Metadata> => {
|
|
9
|
+
const t = await getTranslations("app.emailAuth.forgot")
|
|
10
|
+
return { title: t("title") }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** The forgot door of the lite sign-in, drawn as one Grammar Page (R325). */
|
|
14
|
+
export const AppForgotPage = async (_props: Record<string, never>): Promise<ReactElement> => {
|
|
15
|
+
const t = await getTranslations("app.emailAuth.forgot")
|
|
16
|
+
return (
|
|
17
|
+
<Page header={{ title: t("title") }} measure="reading">
|
|
18
|
+
<EmailDoorForm door="forgot" />
|
|
19
|
+
</Page>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
@@ -1,16 +1,24 @@
|
|
|
1
|
-
import { Page } from "@starci/grammar/common"
|
|
1
|
+
import { Link, Page } from "@starci/grammar/common"
|
|
2
2
|
import { SignInForm } from "@/components/blocks/SignInForm"
|
|
3
3
|
import { SignOutButton } from "@/components/blocks/SignOutButton"
|
|
4
4
|
|
|
5
|
-
/** Props for the home page: its title. */
|
|
5
|
+
/** Props for the home page: its title and the two doors beside the sign-in form. */
|
|
6
6
|
export type AppHomePageBaseProps = {
|
|
7
|
-
readonly props: {
|
|
7
|
+
readonly props: {
|
|
8
|
+
readonly title: string
|
|
9
|
+
readonly signupLabel: string
|
|
10
|
+
readonly signupHref: string
|
|
11
|
+
readonly forgotLabel: string
|
|
12
|
+
readonly forgotHref: string
|
|
13
|
+
}
|
|
8
14
|
}
|
|
9
15
|
|
|
10
16
|
/** Draws the front door as one Grammar Page (R325): the title heads the band, the body the sign-in form or the reader and sign-out. */
|
|
11
17
|
export const AppHomePageBase = (props: AppHomePageBaseProps) => (
|
|
12
18
|
<Page header={{ title: props.props.title }}>
|
|
13
19
|
<SignInForm />
|
|
20
|
+
<Link href={props.props.signupHref}>{props.props.signupLabel}</Link>
|
|
21
|
+
<Link href={props.props.forgotHref}>{props.props.forgotLabel}</Link>
|
|
14
22
|
<SignOutButton />
|
|
15
23
|
</Page>
|
|
16
24
|
)
|
|
@@ -2,6 +2,7 @@ import type { ReactElement } from "react"
|
|
|
2
2
|
import type { Metadata } from "next"
|
|
3
3
|
import { getTranslations } from "next-intl/server"
|
|
4
4
|
import { AppGuideOverlay } from "@/features/overlays/AppGuideOverlay"
|
|
5
|
+
import { APP_ROUTES } from "@/modules/routes"
|
|
5
6
|
import { AppHomePageBase } from "./component"
|
|
6
7
|
|
|
7
8
|
/** The document title of the home page. */
|
|
@@ -15,7 +16,9 @@ export const AppHomePage = async (): Promise<ReactElement> => {
|
|
|
15
16
|
const t = await getTranslations("app.home")
|
|
16
17
|
return (
|
|
17
18
|
<>
|
|
18
|
-
<AppHomePageBase
|
|
19
|
+
<AppHomePageBase
|
|
20
|
+
props={{ title: t("title"), signupLabel: t("signup"), signupHref: APP_ROUTES.signup, forgotLabel: t("forgot"), forgotHref: APP_ROUTES.forgot }}
|
|
21
|
+
/>
|
|
19
22
|
<AppGuideOverlay />
|
|
20
23
|
</>
|
|
21
24
|
)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ReactElement } from "react"
|
|
2
|
+
import type { Metadata } from "next"
|
|
3
|
+
import { Page } from "@starci/grammar/common"
|
|
4
|
+
import { getTranslations } from "next-intl/server"
|
|
5
|
+
import { EmailDoorForm } from "@/components/blocks/EmailDoorForm"
|
|
6
|
+
|
|
7
|
+
/** The document title of the resend page. */
|
|
8
|
+
export const appResendMetadata = async (): Promise<Metadata> => {
|
|
9
|
+
const t = await getTranslations("app.emailAuth.resend")
|
|
10
|
+
return { title: t("title") }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** The resend door of the lite sign-in, drawn as one Grammar Page (R325). */
|
|
14
|
+
export const AppResendPage = async (_props: Record<string, never>): Promise<ReactElement> => {
|
|
15
|
+
const t = await getTranslations("app.emailAuth.resend")
|
|
16
|
+
return (
|
|
17
|
+
<Page header={{ title: t("title") }} measure="reading">
|
|
18
|
+
<EmailDoorForm door="resend" />
|
|
19
|
+
</Page>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ReactElement } from "react"
|
|
2
|
+
import type { Metadata } from "next"
|
|
3
|
+
import { Page } from "@starci/grammar/common"
|
|
4
|
+
import { getTranslations } from "next-intl/server"
|
|
5
|
+
import { EmailDoorForm } from "@/components/blocks/EmailDoorForm"
|
|
6
|
+
|
|
7
|
+
/** The document title of the reset page. */
|
|
8
|
+
export const appResetMetadata = async (): Promise<Metadata> => {
|
|
9
|
+
const t = await getTranslations("app.emailAuth.reset")
|
|
10
|
+
return { title: t("title") }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** The query of the page: the link of the mail carries its token there. */
|
|
14
|
+
type AppResetPageProps = { readonly searchParams: Promise<{ readonly token?: string | readonly string[] }> }
|
|
15
|
+
|
|
16
|
+
/** The reset door of the lite sign-in, drawn as one Grammar Page (R325). */
|
|
17
|
+
export const AppResetPage = async (props: AppResetPageProps): Promise<ReactElement> => {
|
|
18
|
+
const { token } = await props.searchParams
|
|
19
|
+
const t = await getTranslations("app.emailAuth.reset")
|
|
20
|
+
return (
|
|
21
|
+
<Page header={{ title: t("title") }} measure="reading">
|
|
22
|
+
<EmailDoorForm door="reset" token={typeof token === "string" ? token : token?.[0]} />
|
|
23
|
+
</Page>
|
|
24
|
+
)
|
|
25
|
+
}
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
import type { ReactElement } from "react"
|
|
2
|
+
import type { Metadata } from "next"
|
|
3
|
+
import { Page } from "@starci/grammar/common"
|
|
4
|
+
import { getTranslations } from "next-intl/server"
|
|
5
|
+
import { EmailDoorForm } from "@/components/blocks/EmailDoorForm"
|
|
6
|
+
|
|
7
|
+
/** The document title of the signup page. */
|
|
8
|
+
export const appSignupMetadata = async (): Promise<Metadata> => {
|
|
9
|
+
const t = await getTranslations("app.emailAuth.signup")
|
|
10
|
+
return { title: t("title") }
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** The signup door of the lite sign-in, drawn as one Grammar Page (R325). */
|
|
14
|
+
export const AppSignupPage = async (_props: Record<string, never>): Promise<ReactElement> => {
|
|
15
|
+
const t = await getTranslations("app.emailAuth.signup")
|
|
16
|
+
return (
|
|
17
|
+
<Page header={{ title: t("title") }} measure="reading">
|
|
18
|
+
<EmailDoorForm door="signup" />
|
|
19
|
+
</Page>
|
|
20
|
+
)
|
|
21
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { AuthFailure } from "@/modules/auth"
|
|
2
|
+
|
|
3
|
+
/** Why an email door refused: input the back end rejected, a weak password, a dead link, too many tries or the back end unreachable. */
|
|
4
|
+
export type EmailDoorFailure = "invalid" | "password" | "link" | "tooMany" | "unavailable"
|
|
5
|
+
|
|
6
|
+
/** The failure a door's wire code and kind stand for. */
|
|
7
|
+
export const failureOf = (failure: AuthFailure): EmailDoorFailure => {
|
|
8
|
+
if (failure.code === "IDENTITY_PASSWORD_INVALID") return "password"
|
|
9
|
+
if (failure.code === "IDENTITY_RESET_TOKEN_INVALID") return "link"
|
|
10
|
+
if (failure.code === "IDENTITY_TOO_MANY_ATTEMPTS" || failure.code === "429") return "tooMany"
|
|
11
|
+
return failure.kind === "invalid" ? "invalid" : "unavailable"
|
|
12
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import { useState, useTransition } from "react"
|
|
4
|
+
import { sendEmailDoor } from "@/modules/auth"
|
|
5
|
+
import type { EmailDoorName } from "@/modules/auth"
|
|
6
|
+
import { failureOf } from "./emailAuth.shared"
|
|
7
|
+
import type { EmailDoorFailure } from "./emailAuth.shared"
|
|
8
|
+
|
|
9
|
+
export type { EmailDoorFailure } from "./emailAuth.shared"
|
|
10
|
+
|
|
11
|
+
/** What an email door hook answers: whether a request runs, whether the back end accepted it, why it refused, the call itself and `reject`. */
|
|
12
|
+
export interface EmailDoor {
|
|
13
|
+
readonly failure: EmailDoorFailure | undefined
|
|
14
|
+
readonly done: boolean
|
|
15
|
+
readonly pending: boolean
|
|
16
|
+
readonly submit: (body: Readonly<Record<string, string | undefined>>) => void
|
|
17
|
+
readonly reject: () => void
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Calls one email door of the back end through modules/auth; a success sets `done`, a refusal its failure. */
|
|
21
|
+
export const useEmailDoor = (door: EmailDoorName): EmailDoor => {
|
|
22
|
+
const [failure, setFailure] = useState<EmailDoorFailure>()
|
|
23
|
+
const [done, setDone] = useState(false)
|
|
24
|
+
const [pending, startTransition] = useTransition()
|
|
25
|
+
const submit = (body: Readonly<Record<string, string | undefined>>): void =>
|
|
26
|
+
startTransition(async () => {
|
|
27
|
+
const failed = await sendEmailDoor(door, body)
|
|
28
|
+
setFailure(failed === undefined ? undefined : failureOf(failed))
|
|
29
|
+
setDone(failed === undefined)
|
|
30
|
+
})
|
|
31
|
+
return { failure, done, pending, submit, reject: (): void => setFailure("invalid") }
|
|
32
|
+
}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
/** The locale served at the unprefixed path
|
|
2
|
-
export { DEFAULT_LOCALE,
|
|
1
|
+
/** The web app's description of its languages and the locale served at the unprefixed path (the global error boundary). */
|
|
2
|
+
export { DEFAULT_LOCALE, i18n } from "./routing"
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
"brand": "{{project}}"
|
|
6
6
|
},
|
|
7
7
|
"home": {
|
|
8
|
-
"title": "Home"
|
|
8
|
+
"title": "Home",
|
|
9
|
+
"signup": "Create an account",
|
|
10
|
+
"forgot": "Forgot your password?"
|
|
9
11
|
},
|
|
10
12
|
"signIn": {
|
|
11
13
|
"title": "Sign in",
|
|
@@ -52,6 +54,53 @@
|
|
|
52
54
|
"title": "Getting started",
|
|
53
55
|
"description": "Sign in to use the application. You can close this guide and open it again at any time.",
|
|
54
56
|
"close": "Close"
|
|
57
|
+
},
|
|
58
|
+
"emailAuth": {
|
|
59
|
+
"email": "Email",
|
|
60
|
+
"password": "Password",
|
|
61
|
+
"newPassword": "New password",
|
|
62
|
+
"toSignIn": "Back to sign in",
|
|
63
|
+
"toResend": "Send the confirmation again",
|
|
64
|
+
"toForgot": "Request a new reset link",
|
|
65
|
+
"toSignup": "Create an account",
|
|
66
|
+
"toForgotPassword": "Forgot your password?",
|
|
67
|
+
"signup": {
|
|
68
|
+
"title": "Create an account",
|
|
69
|
+
"description": "Choose a password of at least 8 characters. We email a link to confirm your address.",
|
|
70
|
+
"submit": "Create account",
|
|
71
|
+
"done": "Check your inbox: we sent a link to confirm your email."
|
|
72
|
+
},
|
|
73
|
+
"resend": {
|
|
74
|
+
"title": "Confirm your email again",
|
|
75
|
+
"description": "Enter your address and we email a new confirmation link if the account is waiting for one.",
|
|
76
|
+
"submit": "Send the link",
|
|
77
|
+
"done": "If that address is waiting for confirmation, a new link is on its way."
|
|
78
|
+
},
|
|
79
|
+
"forgot": {
|
|
80
|
+
"title": "Forgot your password",
|
|
81
|
+
"description": "Enter your address and we email a link to choose a new password.",
|
|
82
|
+
"submit": "Send the link",
|
|
83
|
+
"done": "If that address has an account, a reset link is on its way."
|
|
84
|
+
},
|
|
85
|
+
"reset": {
|
|
86
|
+
"title": "Choose a new password",
|
|
87
|
+
"description": "The link works once. Every device is signed out when the password changes.",
|
|
88
|
+
"submit": "Save password",
|
|
89
|
+
"done": "Your password is changed. Sign in with the new one."
|
|
90
|
+
},
|
|
91
|
+
"confirm": {
|
|
92
|
+
"title": "Confirm your email",
|
|
93
|
+
"description": "Press the button to confirm your email address.",
|
|
94
|
+
"submit": "Confirm email",
|
|
95
|
+
"done": "Your email is confirmed. You can sign in."
|
|
96
|
+
},
|
|
97
|
+
"errors": {
|
|
98
|
+
"invalid": "Enter a valid value in every field.",
|
|
99
|
+
"password": "The password needs at least 8 characters and at most 72 bytes.",
|
|
100
|
+
"link": "The link is invalid or has expired. Request a new one.",
|
|
101
|
+
"tooMany": "Too many attempts. Try again later.",
|
|
102
|
+
"unavailable": "This is unavailable. Try again shortly."
|
|
103
|
+
}
|
|
55
104
|
}
|
|
56
105
|
}
|
|
57
106
|
}
|
|
@@ -5,7 +5,9 @@
|
|
|
5
5
|
"brand": "{{project}}"
|
|
6
6
|
},
|
|
7
7
|
"home": {
|
|
8
|
-
"title": "Trang chủ"
|
|
8
|
+
"title": "Trang chủ",
|
|
9
|
+
"signup": "Tạo tài khoản",
|
|
10
|
+
"forgot": "Quên mật khẩu?"
|
|
9
11
|
},
|
|
10
12
|
"signIn": {
|
|
11
13
|
"title": "Đăng nhập",
|
|
@@ -52,6 +54,53 @@
|
|
|
52
54
|
"title": "Bắt đầu",
|
|
53
55
|
"description": "Đăng nhập để sử dụng ứng dụng. Bạn có thể đóng hướng dẫn này và mở lại bất cứ lúc nào.",
|
|
54
56
|
"close": "Đóng"
|
|
57
|
+
},
|
|
58
|
+
"emailAuth": {
|
|
59
|
+
"email": "Email",
|
|
60
|
+
"password": "Mật khẩu",
|
|
61
|
+
"newPassword": "Mật khẩu mới",
|
|
62
|
+
"toSignIn": "Quay lại đăng nhập",
|
|
63
|
+
"toResend": "Gửi lại thư xác nhận",
|
|
64
|
+
"toForgot": "Yêu cầu liên kết đặt lại mới",
|
|
65
|
+
"toSignup": "Tạo tài khoản",
|
|
66
|
+
"toForgotPassword": "Quên mật khẩu?",
|
|
67
|
+
"signup": {
|
|
68
|
+
"title": "Tạo tài khoản",
|
|
69
|
+
"description": "Chọn mật khẩu ít nhất 8 ký tự. Chúng tôi gửi email một liên kết để xác nhận địa chỉ của bạn.",
|
|
70
|
+
"submit": "Tạo tài khoản",
|
|
71
|
+
"done": "Hãy kiểm tra hộp thư: chúng tôi đã gửi liên kết xác nhận email."
|
|
72
|
+
},
|
|
73
|
+
"resend": {
|
|
74
|
+
"title": "Xác nhận email lại",
|
|
75
|
+
"description": "Nhập địa chỉ của bạn, chúng tôi gửi liên kết xác nhận mới nếu tài khoản đang chờ xác nhận.",
|
|
76
|
+
"submit": "Gửi liên kết",
|
|
77
|
+
"done": "Nếu địa chỉ đó đang chờ xác nhận, một liên kết mới đang được gửi."
|
|
78
|
+
},
|
|
79
|
+
"forgot": {
|
|
80
|
+
"title": "Quên mật khẩu",
|
|
81
|
+
"description": "Nhập địa chỉ của bạn, chúng tôi gửi email một liên kết để chọn mật khẩu mới.",
|
|
82
|
+
"submit": "Gửi liên kết",
|
|
83
|
+
"done": "Nếu địa chỉ đó có tài khoản, một liên kết đặt lại đang được gửi."
|
|
84
|
+
},
|
|
85
|
+
"reset": {
|
|
86
|
+
"title": "Chọn mật khẩu mới",
|
|
87
|
+
"description": "Liên kết chỉ dùng được một lần. Mọi thiết bị sẽ bị đăng xuất khi đổi mật khẩu.",
|
|
88
|
+
"submit": "Lưu mật khẩu",
|
|
89
|
+
"done": "Mật khẩu đã được đổi. Hãy đăng nhập bằng mật khẩu mới."
|
|
90
|
+
},
|
|
91
|
+
"confirm": {
|
|
92
|
+
"title": "Xác nhận email",
|
|
93
|
+
"description": "Nhấn nút để xác nhận địa chỉ email của bạn.",
|
|
94
|
+
"submit": "Xác nhận email",
|
|
95
|
+
"done": "Email đã được xác nhận. Bạn có thể đăng nhập."
|
|
96
|
+
},
|
|
97
|
+
"errors": {
|
|
98
|
+
"invalid": "Hãy nhập giá trị hợp lệ ở mọi ô.",
|
|
99
|
+
"password": "Mật khẩu cần ít nhất 8 ký tự và tối đa 72 byte.",
|
|
100
|
+
"link": "Liên kết không hợp lệ hoặc đã hết hạn. Hãy yêu cầu liên kết mới.",
|
|
101
|
+
"tooMany": "Quá nhiều lần thử. Hãy thử lại sau.",
|
|
102
|
+
"unavailable": "Chưa thể thực hiện. Hãy thử lại sau."
|
|
103
|
+
}
|
|
55
104
|
}
|
|
56
105
|
}
|
|
57
106
|
}
|
|
@@ -1,35 +1,12 @@
|
|
|
1
|
-
import "
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import { getRequestConfig } from "next-intl/server"
|
|
5
|
-
import { headers } from "next/headers"
|
|
6
|
-
import { PRODUCT_TIME_ZONE, routing } from "./routing"
|
|
7
|
-
|
|
8
|
-
/** The message tree a catalog module exports as its default: the module is read as `unknown` and checked, never trusted as `any`. */
|
|
9
|
-
const isMessages = (value: unknown): value is AbstractIntlMessages => typeof value === "object" && value !== null
|
|
10
|
-
|
|
11
|
-
const messagesOf = (loaded: unknown): AbstractIntlMessages => {
|
|
12
|
-
const messages = typeof loaded === "object" && loaded !== null && "default" in loaded ? loaded.default : undefined
|
|
13
|
-
if (!isMessages(messages)) throw new TypeError("A message catalog module must export its messages as default")
|
|
14
|
-
return messages
|
|
15
|
-
}
|
|
16
|
-
|
|
17
|
-
/** The header the next-intl middleware sets on the request with the locale it negotiated. */
|
|
18
|
-
const LOCALE_HEADER = "x-next-intl-locale"
|
|
1
|
+
import { createRequestConfig } from "@starci/next-i18n/request"
|
|
2
|
+
import type { I18nRequestConfig } from "@starci/next-i18n/request"
|
|
3
|
+
import { i18n } from "./routing"
|
|
19
4
|
|
|
20
5
|
/**
|
|
21
|
-
* The request config next-intl's plugin loads
|
|
22
|
-
*
|
|
6
|
+
* The request config next-intl's plugin loads: the app's own catalogs over the shared stack. The locale is the one the proxy
|
|
7
|
+
* negotiated (a route the proxy does not negotiate reads the cookie, then the default); `next/root-params` can only be imported
|
|
8
|
+
* inside the app directory, so no route reader is passed.
|
|
23
9
|
*/
|
|
24
|
-
const requestConfig:
|
|
25
|
-
const requested = (await headers()).get(LOCALE_HEADER)
|
|
26
|
-
const locale = hasLocale(routing.locales, requested) ? requested : routing.defaultLocale
|
|
27
|
-
return {
|
|
28
|
-
locale,
|
|
29
|
-
timeZone: PRODUCT_TIME_ZONE,
|
|
30
|
-
messages: messagesOf(await import(`./messages/${locale}.json`)),
|
|
31
|
-
getMessageFallback: ({ namespace, key }) => `web:${namespace ?? ""}.${key}`,
|
|
32
|
-
}
|
|
33
|
-
})
|
|
10
|
+
const requestConfig: I18nRequestConfig = createRequestConfig(i18n, { app: "web", loadMessages: (locale) => import(`./messages/${locale}.json`) })
|
|
34
11
|
|
|
35
12
|
export default requestConfig
|
|
@@ -1,18 +1,17 @@
|
|
|
1
|
+
import { defineI18n } from "@starci/next-i18n"
|
|
2
|
+
import type { I18n } from "@starci/next-i18n"
|
|
3
|
+
|
|
1
4
|
const LOCALES = ["vi", "en"] as const
|
|
2
5
|
|
|
3
|
-
/** The
|
|
4
|
-
export
|
|
6
|
+
/** The description of the web app's languages: the locales and the `as-needed` prefix mode that `defineI18n` types. */
|
|
7
|
+
export type WebI18n = I18n<typeof LOCALES, "as-needed">
|
|
5
8
|
|
|
6
|
-
/** The
|
|
7
|
-
export const
|
|
9
|
+
/** The locale served at the unprefixed path. */
|
|
10
|
+
export const DEFAULT_LOCALE: WebI18n["defaultLocale"] = "vi"
|
|
8
11
|
|
|
9
|
-
/**
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
locales: LOCALES,
|
|
16
|
-
defaultLocale: DEFAULT_LOCALE,
|
|
17
|
-
localePrefix: "as-needed" as const,
|
|
18
|
-
}
|
|
12
|
+
/**
|
|
13
|
+
* The one description of the web app's languages, read by the proxy, the request config and the locale layout: vi default, a prefix
|
|
14
|
+
* only when the locale is not the default, and a fixed time zone so the server and the hydrated client format the same instant the
|
|
15
|
+
* same way. The stack that reads it is `@starci/next-i18n`'s (FE-I18N-1).
|
|
16
|
+
*/
|
|
17
|
+
export const i18n: WebI18n = defineI18n({ locales: LOCALES, defaultLocale: DEFAULT_LOCALE, timeZone: "Asia/Ho_Chi_Minh" })
|
|
@@ -1,25 +1,12 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {
|
|
3
|
-
import
|
|
4
|
-
import { routing } from "@/modules/i18n"
|
|
5
|
-
|
|
6
|
-
/** The paths outside locale routing: the liveness route and the framework's own assets. */
|
|
7
|
-
const UNNEGOTIATED_ROOTS: ReadonlySet<string> = new Set(["health", "_next", "_vercel"])
|
|
8
|
-
const negotiate = createMiddleware(routing)
|
|
9
|
-
|
|
10
|
-
const firstSegment = (pathname: string): string => pathname.split("/")[1] ?? ""
|
|
11
|
-
|
|
12
|
-
const isUnnegotiated = (pathname: string): boolean => {
|
|
13
|
-
const last = pathname.split("/").at(-1) ?? ""
|
|
14
|
-
return UNNEGOTIATED_ROOTS.has(firstSegment(pathname)) || (last.includes(".") && !last.endsWith("."))
|
|
15
|
-
}
|
|
1
|
+
import { createProxy } from "@starci/next-i18n/proxy"
|
|
2
|
+
import type { LocaleProxy } from "@starci/next-i18n/proxy"
|
|
3
|
+
import { i18n } from "@/modules/i18n"
|
|
16
4
|
|
|
17
5
|
/**
|
|
18
6
|
* Locale routing only. The session lives in the browser (modules/auth) and the back end verifies the access token of
|
|
19
7
|
* every call, so the proxy neither reads nor refreshes it.
|
|
20
8
|
*/
|
|
21
|
-
export const proxy = (
|
|
22
|
-
isUnnegotiated(request.nextUrl.pathname) ? NextResponse.next() : negotiate(request)
|
|
9
|
+
export const proxy: LocaleProxy = createProxy(i18n)
|
|
23
10
|
|
|
24
11
|
/** Routes that pass through locale negotiation. */
|
|
25
12
|
export const config = {
|
|
@@ -1,38 +0,0 @@
|
|
|
1
|
-
// Managed by starci app sync (slot be.identity.sign-in); do not edit: run "npx starci app sync --execute".
|
|
2
|
-
import { Column, Entity, PrimaryColumn } from "typeorm"
|
|
3
|
-
|
|
4
|
-
@Entity({ schema: "auth", name: "identities" })
|
|
5
|
-
/** One external identity of an account (a Google subject), mapped from auth.identities of db/bootstrap.sql. */
|
|
6
|
-
export class AuthIdentityEntity {
|
|
7
|
-
/** The row id. */
|
|
8
|
-
@PrimaryColumn({ name: "id", type: "uuid" })
|
|
9
|
-
id!: string
|
|
10
|
-
|
|
11
|
-
/** The provider's id of the person (the Google subject). */
|
|
12
|
-
@Column({ name: "provider_id", type: "text" })
|
|
13
|
-
providerId!: string
|
|
14
|
-
|
|
15
|
-
/** The account the identity belongs to. */
|
|
16
|
-
@Column({ name: "user_id", type: "uuid" })
|
|
17
|
-
userId!: string
|
|
18
|
-
|
|
19
|
-
/** What the provider told us, as JSON. */
|
|
20
|
-
@Column({ name: "identity_data", type: "jsonb" })
|
|
21
|
-
identityData!: Record<string, string | boolean | null>
|
|
22
|
-
|
|
23
|
-
/** The provider name. */
|
|
24
|
-
@Column({ name: "provider", type: "text" })
|
|
25
|
-
provider!: string
|
|
26
|
-
|
|
27
|
-
/** The last sign-in through this identity, or null. */
|
|
28
|
-
@Column({ name: "last_sign_in_at", type: "timestamptz", nullable: true })
|
|
29
|
-
lastSignInAt!: Date | null
|
|
30
|
-
|
|
31
|
-
/** When the identity was linked. */
|
|
32
|
-
@Column({ name: "created_at", type: "timestamptz", nullable: true })
|
|
33
|
-
createdAt!: Date | null
|
|
34
|
-
|
|
35
|
-
/** When the identity last changed. */
|
|
36
|
-
@Column({ name: "updated_at", type: "timestamptz", nullable: true })
|
|
37
|
-
updatedAt!: Date | null
|
|
38
|
-
}
|