@awsmbiz/create-inbis-app 0.0.0-stage → 0.1.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.
Files changed (222) hide show
  1. package/README.md +28 -2
  2. package/dist/cli.mjs +850 -0
  3. package/dist/cli.mjs.map +1 -0
  4. package/package.json +43 -3
  5. package/template/.dev.vars.example +4 -0
  6. package/template/README.md +115 -0
  7. package/template/e2e/accessibility.spec.ts +33 -0
  8. package/template/e2e/axe.ts +47 -0
  9. package/template/e2e/shell.spec.ts +33 -0
  10. package/template/e2e/tsconfig.json +8 -0
  11. package/template/eslint.config.js +7 -0
  12. package/template/migrations/0001_app_initial.sql +8 -0
  13. package/template/modules/README.md +6 -0
  14. package/template/modules/access-control/files/src/access-control-routes.ts +36 -0
  15. package/template/modules/access-control/files/src/access-control.ts +46 -0
  16. package/template/modules/access-control/files/test/access-control.test.ts +78 -0
  17. package/template/modules/access-control/module.json +54 -0
  18. package/template/modules/access-control/wiring/admin/admin.imports.tsx +1 -0
  19. package/template/modules/access-control/wiring/admin/admin.nav.tsx +2 -0
  20. package/template/modules/access-control/wiring/admin/admin.pages.tsx +4 -0
  21. package/template/modules/access-control/wiring/admin/admin.userSections.tsx +1 -0
  22. package/template/modules/access-control/wiring/admin/readme.modules.md +3 -0
  23. package/template/modules/access-control/wiring/admin/web.messages.ts +4 -0
  24. package/template/modules/access-control/wiring/audit/audit.catalogues.imports.ts +1 -0
  25. package/template/modules/access-control/wiring/audit/audit.catalogues.ts +1 -0
  26. package/template/modules/access-control/wiring/audit/readme.modules.md +4 -0
  27. package/template/modules/access-control/wiring/audit/web.messages.ts +6 -0
  28. package/template/modules/access-control/wiring/d1.jsonc +8 -0
  29. package/template/modules/access-control/wiring/env.bindings.ts +2 -0
  30. package/template/modules/access-control/wiring/readme.modules.md +20 -0
  31. package/template/modules/access-control/wiring/test.env.ts +1 -0
  32. package/template/modules/access-control/wiring/test.migrate.ts +1 -0
  33. package/template/modules/access-control/wiring/vitest.bindings.ts +1 -0
  34. package/template/modules/access-control/wiring/worker.imports.ts +1 -0
  35. package/template/modules/access-control/wiring/worker.routes.ts +2 -0
  36. package/template/modules/access-control/with-admin/e2e/access-roles.spec.ts +51 -0
  37. package/template/modules/access-control/with-admin/web/src/admin-roles.tsx +38 -0
  38. package/template/modules/access-control/with-audit/test/access-control-audit.test.ts +25 -0
  39. package/template/modules/admin/files/e2e/admin-accessibility.spec.ts +86 -0
  40. package/template/modules/admin/files/e2e/admin-area.spec.ts +109 -0
  41. package/template/modules/admin/files/e2e/admin-helpers.ts +39 -0
  42. package/template/modules/admin/files/src/admin.ts +46 -0
  43. package/template/modules/admin/files/test/admin.test.ts +47 -0
  44. package/template/modules/admin/files/web/src/admin-area.tsx +140 -0
  45. package/template/modules/admin/files/web/src/admin-client.ts +49 -0
  46. package/template/modules/admin/files/web/src/admin-example.tsx +62 -0
  47. package/template/modules/admin/files/web/src/admin-gate.tsx +112 -0
  48. package/template/modules/admin/module.json +50 -0
  49. package/template/modules/admin/wiring/account.sections.tsx +16 -0
  50. package/template/modules/admin/wiring/audit/admin.authAudit.ts +2 -0
  51. package/template/modules/admin/wiring/audit/admin.imports.tsx +1 -0
  52. package/template/modules/admin/wiring/audit/admin.nav.tsx +1 -0
  53. package/template/modules/admin/wiring/audit/admin.pages.tsx +1 -0
  54. package/template/modules/admin/wiring/audit/admin.server.imports.ts +3 -0
  55. package/template/modules/admin/wiring/audit/admin.stats.ts +3 -0
  56. package/template/modules/admin/wiring/audit/admin.userSections.tsx +1 -0
  57. package/template/modules/admin/wiring/audit/readme.modules.md +4 -0
  58. package/template/modules/admin/wiring/audit/web.messages.ts +1 -0
  59. package/template/modules/admin/wiring/readme.modules.md +26 -0
  60. package/template/modules/admin/wiring/web.messages.ts +22 -0
  61. package/template/modules/admin/wiring/web.routes.imports.tsx +1 -0
  62. package/template/modules/admin/wiring/web.routes.tsx +2 -0
  63. package/template/modules/admin/wiring/web.styles.css +1 -0
  64. package/template/modules/admin/wiring/worker.imports.ts +1 -0
  65. package/template/modules/admin/wiring/worker.routes.ts +2 -0
  66. package/template/modules/admin/with-audit/e2e/admin-activity.spec.ts +37 -0
  67. package/template/modules/admin/with-audit/test/admin-audit.test.ts +37 -0
  68. package/template/modules/admin/with-audit/web/src/admin-activity.tsx +21 -0
  69. package/template/modules/audit/files/src/audit-actions.ts +20 -0
  70. package/template/modules/audit/files/src/audit-log.ts +33 -0
  71. package/template/modules/audit/files/test/audit.test.ts +47 -0
  72. package/template/modules/audit/module.json +36 -0
  73. package/template/modules/audit/wiring/audit.imports.ts +1 -0
  74. package/template/modules/audit/wiring/audit.sink.ts +2 -0
  75. package/template/modules/audit/wiring/auth/audit.catalogues.imports.ts +1 -0
  76. package/template/modules/audit/wiring/auth/audit.catalogues.ts +1 -0
  77. package/template/modules/audit/wiring/auth/readme.modules.md +7 -0
  78. package/template/modules/audit/wiring/auth/web.messages.ts +26 -0
  79. package/template/modules/audit/wiring/auth/web.routes.imports.tsx +1 -0
  80. package/template/modules/audit/wiring/auth/web.routes.tsx +1 -0
  81. package/template/modules/audit/wiring/auth/worker.imports.ts +1 -0
  82. package/template/modules/audit/wiring/auth/worker.routes.ts +2 -0
  83. package/template/modules/audit/wiring/d1.jsonc +8 -0
  84. package/template/modules/audit/wiring/env.bindings.ts +4 -0
  85. package/template/modules/audit/wiring/index.imports.ts +1 -0
  86. package/template/modules/audit/wiring/index.setup.ts +2 -0
  87. package/template/modules/audit/wiring/readme.modules.md +16 -0
  88. package/template/modules/audit/wiring/test.env.ts +1 -0
  89. package/template/modules/audit/wiring/test.migrate.ts +1 -0
  90. package/template/modules/audit/wiring/vars.deployed.jsonc +2 -0
  91. package/template/modules/audit/wiring/vars.dev.jsonc +2 -0
  92. package/template/modules/audit/wiring/vitest.bindings.ts +1 -0
  93. package/template/modules/audit/with-auth/e2e/my-activity.spec.ts +89 -0
  94. package/template/modules/audit/with-auth/src/audit-routes.ts +21 -0
  95. package/template/modules/audit/with-auth/test/audit-routes.test.ts +17 -0
  96. package/template/modules/audit/with-auth/web/src/my-activity.tsx +168 -0
  97. package/template/modules/auth/files/e2e/auth-accessibility.spec.ts +68 -0
  98. package/template/modules/auth/files/e2e/auth-bootstrap-admin.spec.ts +58 -0
  99. package/template/modules/auth/files/e2e/auth-helpers.ts +74 -0
  100. package/template/modules/auth/files/e2e/auth-invitations.spec.ts +51 -0
  101. package/template/modules/auth/files/e2e/auth-phone.spec.ts +41 -0
  102. package/template/modules/auth/files/e2e/auth-seeded.ts +97 -0
  103. package/template/modules/auth/files/e2e/auth-sign-in.spec.ts +57 -0
  104. package/template/modules/auth/files/e2e/auth-two-factor.spec.ts +44 -0
  105. package/template/modules/auth/files/scripts/seed-user.mjs +64 -0
  106. package/template/modules/auth/files/src/auth-choices.ts +33 -0
  107. package/template/modules/auth/files/src/auth-config.ts +55 -0
  108. package/template/modules/auth/files/src/auth.ts +68 -0
  109. package/template/modules/auth/files/src/local-test-mode.ts +49 -0
  110. package/template/modules/auth/files/src/notifications.ts +94 -0
  111. package/template/modules/auth/files/test/auth-fixtures.ts +135 -0
  112. package/template/modules/auth/files/test/auth.test.ts +210 -0
  113. package/template/modules/auth/files/test/totp.ts +49 -0
  114. package/template/modules/auth/files/test/turnstile-fake.ts +42 -0
  115. package/template/modules/auth/files/web/src/account-menu.tsx +25 -0
  116. package/template/modules/auth/files/web/src/account.tsx +87 -0
  117. package/template/modules/auth/files/web/src/auth-client.ts +60 -0
  118. package/template/modules/auth/files/web/src/auth-pages.tsx +152 -0
  119. package/template/modules/auth/module.json +56 -0
  120. package/template/modules/auth/wiring/bindings.deployed.jsonc +2 -0
  121. package/template/modules/auth/wiring/d1.jsonc +7 -0
  122. package/template/modules/auth/wiring/devvars.txt +12 -0
  123. package/template/modules/auth/wiring/env.bindings.ts +26 -0
  124. package/template/modules/auth/wiring/index.deps.ts +1 -0
  125. package/template/modules/auth/wiring/index.imports.ts +4 -0
  126. package/template/modules/auth/wiring/index.setup.ts +7 -0
  127. package/template/modules/auth/wiring/playwright.imports.ts +1 -0
  128. package/template/modules/auth/wiring/playwright.servers.ts +12 -0
  129. package/template/modules/auth/wiring/playwright.setup.ts +2 -0
  130. package/template/modules/auth/wiring/playwright.vars.ts +2 -0
  131. package/template/modules/auth/wiring/readme.modules.md +30 -0
  132. package/template/modules/auth/wiring/readme.provisioning.md +11 -0
  133. package/template/modules/auth/wiring/secrets.jsonc +2 -0
  134. package/template/modules/auth/wiring/test.env.ts +1 -0
  135. package/template/modules/auth/wiring/test.migrate.ts +1 -0
  136. package/template/modules/auth/wiring/vars.dev.jsonc +19 -0
  137. package/template/modules/auth/wiring/vars.production.jsonc +13 -0
  138. package/template/modules/auth/wiring/vars.staging.jsonc +13 -0
  139. package/template/modules/auth/wiring/vitest.bindings.ts +6 -0
  140. package/template/modules/auth/wiring/web.header.tsx +1 -0
  141. package/template/modules/auth/wiring/web.layout.imports.tsx +1 -0
  142. package/template/modules/auth/wiring/web.messages.ts +17 -0
  143. package/template/modules/auth/wiring/web.routes.imports.tsx +12 -0
  144. package/template/modules/auth/wiring/web.routes.tsx +11 -0
  145. package/template/modules/auth/wiring/web.styles.css +1 -0
  146. package/template/modules/auth/wiring/worker.deps.ts +2 -0
  147. package/template/modules/auth/wiring/worker.imports.ts +1 -0
  148. package/template/modules/auth/wiring/worker.routes.ts +2 -0
  149. package/template/modules/example/files/e2e/example-notes.spec.ts +39 -0
  150. package/template/modules/example/files/migrations/0002_example_notes.sql +12 -0
  151. package/template/modules/example/files/src/example-audit-actions.ts +12 -0
  152. package/template/modules/example/files/src/example-notes.ts +136 -0
  153. package/template/modules/example/files/test/example-notes-fixtures.ts +30 -0
  154. package/template/modules/example/files/test/example-notes.test.ts +52 -0
  155. package/template/modules/example/files/web/src/example-notes.tsx +176 -0
  156. package/template/modules/example/module.json +61 -0
  157. package/template/modules/example/wiring/access-control/access.permissions.ts +3 -0
  158. package/template/modules/example/wiring/access-control/access.roles.ts +2 -0
  159. package/template/modules/example/wiring/access-control/example.e2e.imports.ts +2 -0
  160. package/template/modules/example/wiring/access-control/example.e2e.setup.ts +18 -0
  161. package/template/modules/example/wiring/access-control/example.imports.ts +3 -0
  162. package/template/modules/example/wiring/access-control/example.policy.ts +10 -0
  163. package/template/modules/example/wiring/access-control/example.test.caller.ts +17 -0
  164. package/template/modules/example/wiring/access-control/example.test.imports.ts +1 -0
  165. package/template/modules/example/wiring/access-control/readme.modules.md +5 -0
  166. package/template/modules/example/wiring/admin/admin.imports.tsx +1 -0
  167. package/template/modules/example/wiring/admin/admin.nav.tsx +2 -0
  168. package/template/modules/example/wiring/admin/admin.pages.tsx +2 -0
  169. package/template/modules/example/wiring/admin/readme.modules.md +4 -0
  170. package/template/modules/example/wiring/admin/web.messages.ts +11 -0
  171. package/template/modules/example/wiring/admin/worker.imports.ts +1 -0
  172. package/template/modules/example/wiring/admin/worker.routes.ts +2 -0
  173. package/template/modules/example/wiring/audit/audit.catalogues.imports.ts +1 -0
  174. package/template/modules/example/wiring/audit/audit.catalogues.ts +2 -0
  175. package/template/modules/example/wiring/audit/readme.modules.md +4 -0
  176. package/template/modules/example/wiring/readme.modules.md +16 -0
  177. package/template/modules/example/wiring/web.messages.ts +19 -0
  178. package/template/modules/example/wiring/web.routes.imports.tsx +1 -0
  179. package/template/modules/example/wiring/web.routes.tsx +2 -0
  180. package/template/modules/example/wiring/worker.imports.ts +1 -0
  181. package/template/modules/example/wiring/worker.routes.ts +2 -0
  182. package/template/modules/example/with-access-control/test/example-notes-access.test.ts +51 -0
  183. package/template/modules/example/with-admin/e2e/example-notes-admin.spec.ts +55 -0
  184. package/template/modules/example/with-admin/src/example-notes-admin.ts +62 -0
  185. package/template/modules/example/with-admin/test/example-notes-admin.test.ts +42 -0
  186. package/template/modules/example/with-admin/web/src/admin-notes.tsx +99 -0
  187. package/template/modules/example/with-audit/test/example-notes-audit.test.ts +22 -0
  188. package/template/package.json +57 -0
  189. package/template/playwright.config.ts +38 -0
  190. package/template/renovate.json +12 -0
  191. package/template/src/app.ts +46 -0
  192. package/template/src/audit-catalogues.ts +9 -0
  193. package/template/src/audit-sink.ts +21 -0
  194. package/template/src/env.d.ts +21 -0
  195. package/template/src/index.ts +18 -0
  196. package/template/src/v1.ts +24 -0
  197. package/template/standalone/.github/workflows/ci.yml +21 -0
  198. package/template/standalone/.github/workflows/deploy.yml +32 -0
  199. package/template/standalone/.nvmrc +1 -0
  200. package/template/standalone/_gitignore +9 -0
  201. package/template/standalone/pnpm-workspace.yaml +11 -0
  202. package/template/template.json +56 -0
  203. package/template/test/env.d.ts +11 -0
  204. package/template/test/migrate.ts +11 -0
  205. package/template/test/migrations.test.ts +22 -0
  206. package/template/test/worker.test.ts +65 -0
  207. package/template/tsconfig.json +7 -0
  208. package/template/vitest.config.ts +24 -0
  209. package/template/web/index.html +12 -0
  210. package/template/web/src/app.tsx +44 -0
  211. package/template/web/src/home.tsx +51 -0
  212. package/template/web/src/layout.tsx +48 -0
  213. package/template/web/src/main.tsx +16 -0
  214. package/template/web/src/messages.ts +38 -0
  215. package/template/web/src/navigation.tsx +34 -0
  216. package/template/web/src/route-focus.ts +49 -0
  217. package/template/web/src/routes.tsx +38 -0
  218. package/template/web/src/styles.css +11 -0
  219. package/template/web/src/system-theme.ts +16 -0
  220. package/template/web/tsconfig.json +7 -0
  221. package/template/web/vite.config.ts +15 -0
  222. package/template/wrangler.jsonc +96 -0
@@ -0,0 +1,25 @@
1
+ import { useMe } from "./auth-client.js";
2
+ import { t } from "./messages.js";
3
+ import { Link, type Navigate } from "./navigation.js";
4
+
5
+ /** The header's account link: "Sign in" for visitors, the user's name (to the account page) once signed in. */
6
+ export function AccountMenu({ path, navigate }: { path: string; navigate: Navigate }) {
7
+ const [state] = useMe();
8
+ const className =
9
+ "text-sm text-muted-foreground underline-offset-4 hover:text-foreground hover:underline aria-[current=page]:font-medium aria-[current=page]:text-foreground";
10
+ if (state.status === "loading") return null;
11
+ if (state.status === "signed-out") {
12
+ return (
13
+ <Link href={`/sign-in?next=${encodeURIComponent(path)}`} navigate={navigate} className={className}>
14
+ {t("account.signIn")}
15
+ </Link>
16
+ );
17
+ }
18
+ const label = state.status === "signed-in" ? state.me.user.name : t("account.title");
19
+ return (
20
+ <Link href="/account" navigate={navigate} aria-current={path === "/account" ? "page" : undefined} className={className}>
21
+ <span className="sr-only">{t("account.title")}: </span>
22
+ {label}
23
+ </Link>
24
+ );
25
+ }
@@ -0,0 +1,87 @@
1
+ import { StepUpScreen, usePublicAuthConfig } from "@awsmbiz/inbis-auth-ui";
2
+ import { Button, Card, CardContent, CardDescription, CardHeader, CardTitle } from "@awsmbiz/inbis-ui";
3
+ import { useEffect } from "react";
4
+ import { client, identityOf, useMe } from "./auth-client.js";
5
+ import { t } from "./messages.js";
6
+ import { Link } from "./navigation.js";
7
+ import type { RouteContext } from "./routes.js";
8
+
9
+ /**
10
+ * The signed-in user's account page: who is signed in, and links to two-factor
11
+ * authentication and sessions. Signed-out visitors are sent to sign-in and come back.
12
+ * A page of your own that needs a session follows the same pattern (`useMe`).
13
+ */
14
+ export function AccountPage({ navigate }: RouteContext) {
15
+ const [state, refresh] = useMe();
16
+ const publicConfig = usePublicAuthConfig(client);
17
+
18
+ useEffect(() => {
19
+ if (state.status === "signed-out") navigate(`/sign-in?next=${encodeURIComponent("/account")}`, { replace: true });
20
+ }, [state, navigate]);
21
+
22
+ if (state.status === "requirement") {
23
+ // The session needs a second factor (or another sign-in method) before it may be used.
24
+ return (
25
+ <StepUpScreen
26
+ client={client}
27
+ requirement={state.requirement}
28
+ onSatisfied={refresh}
29
+ onSignInAgain={() => {
30
+ void client.signOut().then(() => {
31
+ navigate("/sign-in?next=%2Faccount");
32
+ });
33
+ }}
34
+ />
35
+ );
36
+ }
37
+ if (state.status !== "signed-in") return <p className="text-sm text-muted-foreground">{t("common.loading")}</p>;
38
+
39
+ const { me } = state;
40
+ const twoFactor = publicConfig.status === "ready" && publicConfig.config.twoFactor !== undefined;
41
+ const linkClass = "text-sm font-medium text-primary underline underline-offset-4";
42
+ return (
43
+ <div className="flex flex-col gap-6">
44
+ <div className="flex flex-col gap-2">
45
+ <h1 className="text-2xl font-semibold tracking-tight">{t("account.heading")}</h1>
46
+ <p className="text-muted-foreground">{t("account.signedInAs", { name: me.user.name, identity: identityOf(me) })}</p>
47
+ </div>
48
+ <Card>
49
+ <CardHeader>
50
+ <CardTitle>
51
+ <h2>{t("account.security")}</h2>
52
+ </CardTitle>
53
+ <CardDescription>{t("account.securityDescription")}</CardDescription>
54
+ </CardHeader>
55
+ <CardContent>
56
+ <ul className="flex flex-col gap-2">
57
+ {twoFactor ? (
58
+ <li>
59
+ <Link href="/account/security" navigate={navigate} className={linkClass}>
60
+ {t("account.twoFactor")}
61
+ </Link>
62
+ </li>
63
+ ) : null}
64
+ <li>
65
+ <Link href="/account/sessions" navigate={navigate} className={linkClass}>
66
+ {t("account.sessions")}
67
+ </Link>
68
+ </li>
69
+ </ul>
70
+ </CardContent>
71
+ </Card>
72
+ {/* inbis:wire account.sections */}
73
+ <div>
74
+ <Button
75
+ variant="outline"
76
+ onClick={() => {
77
+ void client.signOut().then(() => {
78
+ navigate("/sign-in");
79
+ });
80
+ }}
81
+ >
82
+ {t("account.signOut")}
83
+ </Button>
84
+ </div>
85
+ </div>
86
+ );
87
+ }
@@ -0,0 +1,60 @@
1
+ import { accessRequirementOf, createInbisAuthClient, type AccessRequirement } from "@awsmbiz/inbis-auth/client";
2
+ import { useEffect, useState } from "react";
3
+
4
+ // Same origin as the Worker serving this SPA.
5
+ export const client = createInbisAuthClient();
6
+
7
+ /** Where to go after signing in: `?next=` when it is a path on this site, else home. */
8
+ export function nextPath(search: string): string {
9
+ const next = new URLSearchParams(search).get("next");
10
+ return next?.startsWith("/") === true && !next.startsWith("//") ? next : "/";
11
+ }
12
+
13
+ /** The signed-in user, as `GET /api/me` returns it. */
14
+ export interface Me {
15
+ user: { id: string; name: string; email: string; phoneNumber: string | null; emailIsPlaceholder: boolean; role: "admin" | "user" };
16
+ }
17
+
18
+ export type MeState =
19
+ | { status: "loading" }
20
+ | { status: "signed-out" }
21
+ /** Signed in, but the session needs a second factor or a different sign-in first. */
22
+ | { status: "requirement"; requirement: AccessRequirement }
23
+ | { status: "signed-in"; me: Me };
24
+
25
+ /** Asks `GET /api/me` who is signed in. `refresh` asks again (after signing in or out). */
26
+ export function useMe(): [MeState, () => void] {
27
+ const [state, setState] = useState<MeState>({ status: "loading" });
28
+ const [attempt, setAttempt] = useState(0);
29
+ useEffect(() => {
30
+ let active = true;
31
+ void fetch("/api/me", { credentials: "include" })
32
+ .then(async (response): Promise<MeState> => {
33
+ if (response.status === 401) return { status: "signed-out" };
34
+ const requirement = await accessRequirementOf(response.clone());
35
+ if (requirement !== null) return { status: "requirement", requirement };
36
+ if (!response.ok) return { status: "signed-out" };
37
+ return { status: "signed-in", me: (await response.json()) as Me };
38
+ })
39
+ .then((next) => {
40
+ if (active) setState(next);
41
+ })
42
+ .catch(() => {
43
+ if (active) setState({ status: "signed-out" });
44
+ });
45
+ return () => {
46
+ active = false;
47
+ };
48
+ }, [attempt]);
49
+ return [
50
+ state,
51
+ () => {
52
+ setAttempt((n) => n + 1);
53
+ },
54
+ ];
55
+ }
56
+
57
+ /** How to show who is signed in: a phone-only user's email is a placeholder. */
58
+ export function identityOf(me: Me): string {
59
+ return (me.user.emailIsPlaceholder ? me.user.phoneNumber : me.user.email) ?? "";
60
+ }
@@ -0,0 +1,152 @@
1
+ import {
2
+ AcceptInvitationScreen,
3
+ ForgotPasswordScreen,
4
+ PhoneSignInScreen,
5
+ ResetPasswordScreen,
6
+ SessionsScreen,
7
+ SignInScreen,
8
+ SignUpScreen,
9
+ TwoFactorSettingsScreen,
10
+ VerifyEmailScreen,
11
+ } from "@awsmbiz/inbis-auth-ui";
12
+ import { client, nextPath } from "./auth-client.js";
13
+ import type { RouteContext } from "./routes.js";
14
+
15
+ // The sign-in screens of @awsmbiz/inbis-auth-ui, each a full page of its own (bare
16
+ // routes in routes.tsx). Each screen reads the server's public auth config, so it only
17
+ // offers what the app has switched on (sign-up when registration is open, phone sign-in,
18
+ // password reset, 2FA).
19
+
20
+ /** Signs out, then opens sign-in, coming back to `next` afterwards. */
21
+ function signInAgain({ navigate }: RouteContext, next: string) {
22
+ void client.signOut().then(() => {
23
+ navigate(`/sign-in?next=${encodeURIComponent(next)}`);
24
+ });
25
+ }
26
+
27
+ export function SignInPage({ navigate, search }: RouteContext) {
28
+ return (
29
+ <SignInScreen
30
+ client={client}
31
+ onSignedIn={() => {
32
+ navigate(nextPath(search));
33
+ }}
34
+ onCreateAccount={() => {
35
+ navigate("/sign-up");
36
+ }}
37
+ onForgotPassword={() => {
38
+ navigate("/forgot-password");
39
+ }}
40
+ onVerifyEmail={() => {
41
+ navigate("/verify-email");
42
+ }}
43
+ onPhoneSignIn={() => {
44
+ navigate(`/sign-in/phone${search}`);
45
+ }}
46
+ />
47
+ );
48
+ }
49
+
50
+ export function PhoneSignInPage({ navigate, search }: RouteContext) {
51
+ return (
52
+ <PhoneSignInScreen
53
+ client={client}
54
+ onSignedIn={() => {
55
+ navigate(nextPath(search));
56
+ }}
57
+ onSignIn={() => {
58
+ navigate(`/sign-in${search}`);
59
+ }}
60
+ />
61
+ );
62
+ }
63
+
64
+ export function SignUpPage({ navigate }: RouteContext) {
65
+ return (
66
+ <SignUpScreen
67
+ client={client}
68
+ // Email verification is required: the new account signs in after verifying.
69
+ callbackURL="/verify-email?verified=1"
70
+ onSignedUp={() => {
71
+ navigate("/verify-email?sent=1");
72
+ }}
73
+ onSignIn={() => {
74
+ navigate("/sign-in");
75
+ }}
76
+ />
77
+ );
78
+ }
79
+
80
+ export function ForgotPasswordPage({ navigate }: RouteContext) {
81
+ return (
82
+ <ForgotPasswordScreen
83
+ client={client}
84
+ onSignIn={() => {
85
+ navigate("/sign-in");
86
+ }}
87
+ />
88
+ );
89
+ }
90
+
91
+ export function ResetPasswordPage({ navigate }: RouteContext) {
92
+ return (
93
+ <ResetPasswordScreen
94
+ client={client}
95
+ onSignIn={() => {
96
+ navigate("/sign-in");
97
+ }}
98
+ onRequestNewLink={() => {
99
+ navigate("/forgot-password");
100
+ }}
101
+ />
102
+ );
103
+ }
104
+
105
+ export function VerifyEmailPage({ navigate, search }: RouteContext) {
106
+ return (
107
+ <VerifyEmailScreen
108
+ client={client}
109
+ search={search}
110
+ onSignIn={() => {
111
+ navigate("/sign-in");
112
+ }}
113
+ />
114
+ );
115
+ }
116
+
117
+ /** Invitation links (`registration.invitations.acceptPath`, default /accept-invitation). */
118
+ export function AcceptInvitationPage({ navigate }: RouteContext) {
119
+ return (
120
+ <AcceptInvitationScreen
121
+ client={client}
122
+ onSignIn={() => {
123
+ navigate("/sign-in");
124
+ }}
125
+ onSignedIn={() => {
126
+ navigate("/account");
127
+ }}
128
+ />
129
+ );
130
+ }
131
+
132
+ export function SecurityPage(context: RouteContext) {
133
+ return (
134
+ <TwoFactorSettingsScreen
135
+ client={client}
136
+ onReauthenticate={() => {
137
+ signInAgain(context, "/account/security");
138
+ }}
139
+ />
140
+ );
141
+ }
142
+
143
+ export function SessionsPage(context: RouteContext) {
144
+ return (
145
+ <SessionsScreen
146
+ client={client}
147
+ onReauthenticate={() => {
148
+ signInAgain(context, "/account/sessions");
149
+ }}
150
+ />
151
+ );
152
+ }
@@ -0,0 +1,56 @@
1
+ {
2
+ "name": "auth",
3
+ "description": "Sign-in: email/password, phone OTP, 2FA, invitations, registration modes, bootstrap-admin, auth emails/SMS and the auth-ui screens.",
4
+ "order": 10,
5
+ "requires": [],
6
+ "bindings": {
7
+ "AUTH_DB": "the auth tables (@awsmbiz/inbis-auth migrations, same database)"
8
+ },
9
+ "dependencies": {
10
+ "@awsmbiz/inbis-auth": "workspace:*",
11
+ "@awsmbiz/inbis-auth-ui": "workspace:*",
12
+ "@awsmbiz/inbis-notifications": "workspace:*"
13
+ },
14
+ "devDependencies": {
15
+ "better-auth": "1.7.14"
16
+ },
17
+ "scripts": {
18
+ "db:seed": "node scripts/seed-user.mjs",
19
+ "auth:bootstrap-admin": "inbis-auth bootstrap-admin"
20
+ },
21
+ "points": {
22
+ "account.sections": {
23
+ "file": "web/src/account.tsx",
24
+ "description": "JSX after the account page's Security card (e.g. a Card); `me` (the signed-in user), `navigate` and `linkClass` are in scope."
25
+ }
26
+ },
27
+ "wiring": {
28
+ "worker.imports": "wiring/worker.imports.ts",
29
+ "worker.deps": "wiring/worker.deps.ts",
30
+ "worker.routes": "wiring/worker.routes.ts",
31
+ "index.imports": "wiring/index.imports.ts",
32
+ "index.setup": "wiring/index.setup.ts",
33
+ "index.deps": "wiring/index.deps.ts",
34
+ "env.bindings": "wiring/env.bindings.ts",
35
+ "wrangler.vars": { "dev": "wiring/vars.dev.jsonc", "staging": "wiring/vars.staging.jsonc", "production": "wiring/vars.production.jsonc" },
36
+ "wrangler.secrets": "wiring/secrets.jsonc",
37
+ "wrangler.bindings": { "staging": "wiring/bindings.deployed.jsonc", "production": "wiring/bindings.deployed.jsonc" },
38
+ "wrangler.d1": "wiring/d1.jsonc",
39
+ "devvars": "wiring/devvars.txt",
40
+ "test.env": "wiring/test.env.ts",
41
+ "test.migrate": "wiring/test.migrate.ts",
42
+ "vitest.bindings": "wiring/vitest.bindings.ts",
43
+ "playwright.imports": "wiring/playwright.imports.ts",
44
+ "playwright.setup": "wiring/playwright.setup.ts",
45
+ "playwright.vars": "wiring/playwright.vars.ts",
46
+ "playwright.servers": "wiring/playwright.servers.ts",
47
+ "web.routes.imports": "wiring/web.routes.imports.tsx",
48
+ "web.routes": "wiring/web.routes.tsx",
49
+ "web.layout.imports": "wiring/web.layout.imports.tsx",
50
+ "web.header": "wiring/web.header.tsx",
51
+ "web.messages": "wiring/web.messages.ts",
52
+ "web.styles": "wiring/web.styles.css",
53
+ "readme.modules": "wiring/readme.modules.md",
54
+ "readme.provisioning": "wiring/readme.provisioning.md"
55
+ }
56
+ }
@@ -0,0 +1,2 @@
1
+ // The shared communication service (auth email and SMS), an RPC Worker.
2
+ "services": [{ "binding": "COMMUNICATION_SERVICE", "service": "biz-cloud-communication-service" }],
@@ -0,0 +1,7 @@
1
+ {
2
+ "binding": "AUTH_DB",
3
+ "database_name": "{{database_name}}",
4
+ "database_id": "{{database_id}}",
5
+ "migrations_dir": "node_modules/@awsmbiz/inbis-auth/migrations",
6
+ "migrations_table": "_inbis_auth_migrations"
7
+ },
@@ -0,0 +1,12 @@
1
+ # Auth: signs sessions and encrypts 2FA secrets. Any random string of 32+ characters,
2
+ # e.g. `openssl rand -base64 32`.
3
+ AUTH_SECRET=change-me-to-a-random-string-of-32-plus-chars
4
+ # Turnstile secret key. Locally, Cloudflare's testing secret that accepts any token
5
+ # (pairs with the testing TURNSTILE_SITE_KEY below).
6
+ TURNSTILE_SECRET_KEY=1x0000000000000000000000000000000AA
7
+ # Local-only overrides, so a deployed environment's config also starts on this machine
8
+ # (`wrangler dev --env staging`, e.g. the deploy workflow's dry-run smoke test): log
9
+ # transports instead of the communication service, and Cloudflare's Turnstile testing site key.
10
+ EMAIL_TRANSPORT=log
11
+ SMS_TRANSPORT=log
12
+ TURNSTILE_SITE_KEY=1x00000000000000000000AA
@@ -0,0 +1,26 @@
1
+ /** The auth tables (@awsmbiz/inbis-auth migrations); same database as DB. */
2
+ AUTH_DB: D1Database;
3
+ /** Public origin of this Worker (auth links, cookies; https outside localhost). */
4
+ AUTH_BASE_URL: string;
5
+ /** Secret: signs sessions and encrypts 2FA secrets (32+ random characters). */
6
+ AUTH_SECRET: string;
7
+ /** Secret: the Turnstile widget's secret key. */
8
+ TURNSTILE_SECRET_KEY: string;
9
+ /** The Turnstile widget's public site key. */
10
+ TURNSTILE_SITE_KEY: string;
11
+ /** Sender of auth emails, `Name <address>`, on a domain onboarded to the communication service. */
12
+ EMAIL_FROM: string;
13
+ /** "log" (local) or "communication-service". */
14
+ EMAIL_TRANSPORT: string;
15
+ /** "log" (local) or "communication-service". */
16
+ SMS_TRANSPORT: string;
17
+ /** The app's ID in the communication service. */
18
+ APP_ID: string;
19
+ /** SMS sender header and the registered template IDs (communication-service SMS). */
20
+ SMS_HEADER: string;
21
+ SMS_OTP_TEMPLATE_ID: string;
22
+ SMS_INVITATION_TEMPLATE_ID: string;
23
+ /** "true": local test mode (src/local-test-mode.ts); ignored off local hosts. */
24
+ LOCAL_TEST_MODE?: string;
25
+ /** The shared communication service (deployed environments only). */
26
+ COMMUNICATION_SERVICE?: Fetcher;
@@ -0,0 +1 @@
1
+ auth: { authConfig, ...notifications, ...(localTest ? { fetch: stubbedFetch((input, init) => fetch(input, init)) } : {}) },
@@ -0,0 +1,4 @@
1
+ import { validateAuthConfig } from "@awsmbiz/inbis-auth";
2
+ import { appAuthConfig } from "./auth-config.js";
3
+ import { localTestMode, stubbedFetch } from "./local-test-mode.js";
4
+ import { appNotifications } from "./notifications.js";
@@ -0,0 +1,7 @@
1
+ const authConfig = appAuthConfig(env);
2
+ validateAuthConfig(authConfig);
3
+ const notifications = appNotifications(env, authConfig);
4
+ // Playwright runs (LOCAL_TEST_MODE, local hosts only): no calls to Turnstile or Pwned Passwords.
5
+ // Better Auth's captcha plugin calls the global fetch, so that is wrapped too.
6
+ const localTest = localTestMode(env);
7
+ if (localTest) globalThis.fetch = stubbedFetch(globalThis.fetch.bind(globalThis));
@@ -0,0 +1 @@
1
+ import { authDevVars, authSeedCommands, BOOTSTRAP_PORT, BOOTSTRAP_STATE, BOOTSTRAP_URL } from "./e2e/auth-seeded.js";
@@ -0,0 +1,12 @@
1
+ // Auth: a fresh installation with no users, for the first administrator
2
+ // (e2e/auth-bootstrap-admin.spec.ts runs `inbis-auth bootstrap-admin` against it).
3
+ {
4
+ command: [
5
+ `rm -rf ${BOOTSTRAP_STATE}`,
6
+ `inbis-db migrate --local --persist-to ${BOOTSTRAP_STATE}`,
7
+ `wrangler dev --port ${BOOTSTRAP_PORT} --persist-to ${BOOTSTRAP_STATE} ${authDevVars(BOOTSTRAP_URL)}`,
8
+ ].join(" && "),
9
+ url: `${BOOTSTRAP_URL}/health`,
10
+ reuseExistingServer: false,
11
+ timeout: 120_000,
12
+ },
@@ -0,0 +1,2 @@
1
+ // Auth: the users the specs sign in as (e2e/auth-seeded.ts).
2
+ ...authSeedCommands(STATE),
@@ -0,0 +1,2 @@
1
+ // Auth: test secrets; local test mode answers Turnstile and Pwned Passwords locally.
2
+ authDevVars(BASE_URL),
@@ -0,0 +1,30 @@
1
+ ## Sign-in (auth module)
2
+
3
+ `@awsmbiz/inbis-auth` (Better Auth on D1), `@awsmbiz/inbis-auth-ui` (the screens) and
4
+ `@awsmbiz/inbis-notifications` (auth emails and SMS).
5
+
6
+ - **Choices** — `src/auth-choices.ts`: the sign-in methods, the registration mode and the
7
+ phone country allow/block lists chosen when the app was created. Change them there;
8
+ `src/auth-config.ts` turns them into the auth config (every other option is the
9
+ platform default; see the `@awsmbiz/inbis-auth` README). Registration modes: `open`
10
+ (anyone signs up), `invite-only` (administrators create accounts or invite),
11
+ `admin-only` (administrators create accounts; invitations are refused), `closed`
12
+ (only the first administrator). Phone countries are calling-code prefixes; there is
13
+ no default.
14
+ - **Routes** — `src/auth.ts` mounts `/api/auth/*`, `GET /api/me` and, locally, the outboxes.
15
+ Protect your own routes with `requireAuth()` from `@awsmbiz/inbis-auth/hono`.
16
+ - **Screens** — `/sign-in`, `/sign-in/phone`, `/sign-up`, `/forgot-password`,
17
+ `/reset-password`, `/verify-email`, `/accept-invitation`, `/account` (with
18
+ `/account/security` for 2FA and `/account/sessions`). The header links to sign-in or
19
+ the account page.
20
+ - **Email and SMS** — locally (`EMAIL_TRANSPORT`/`SMS_TRANSPORT` = `log`) nothing is sent:
21
+ messages are printed and kept in a dev outbox, `GET /api/dev/outbox` and
22
+ `GET /api/dev/sms-outbox` (local hosts only). Deployed environments send through the
23
+ shared communication service (see Provisioning).
24
+ - **First administrator** — `pnpm auth:bootstrap-admin --email you@example.com --url <app URL>`
25
+ prints a one-time invitation link (add `--remote --env production` for a deployed
26
+ database, `--send-email` to email it). Administrators set up 2FA on first use.
27
+ - **Local users** — `pnpm db:seed [--email a@b.test] [--password '...'] [--role admin]`
28
+ adds a verified email + password user to the local database.
29
+ - **Tests** — `test/auth.test.ts` (Workers) and `e2e/auth-*.spec.ts` (Playwright + axe)
30
+ read `src/auth-choices.ts` and skip the journeys the app has switched off.
@@ -0,0 +1,11 @@
1
+ Sign-in (auth module):
2
+ - `AUTH_SECRET`: 32+ random characters (`openssl rand -base64 32`); `TURNSTILE_SECRET_KEY`:
3
+ the secret of a [Turnstile widget](https://developers.cloudflare.com/turnstile/get-started/)
4
+ for the app's domains, whose site key goes in `TURNSTILE_SITE_KEY` in `wrangler.jsonc`.
5
+ - Email and SMS go through the shared communication service (`biz-cloud-communication-service`,
6
+ bound as `COMMUNICATION_SERVICE` in each deployed environment). Register the app with it
7
+ under `APP_ID` and onboard the sender domain of `EMAIL_FROM`. With phone sign-in on, register
8
+ the SMS texts in `src/notifications.ts` (`SMS_TEXTS`) and set `SMS_HEADER`,
9
+ `SMS_OTP_TEMPLATE_ID` and `SMS_INVITATION_TEMPLATE_ID`; startup fails while they are blank.
10
+ - After migrating and deploying (step 4), create the first administrator:
11
+ `pnpm auth:bootstrap-admin --email you@example.com --url https://<domain> --remote --env <env>`.
@@ -0,0 +1,2 @@
1
+ "AUTH_SECRET",
2
+ "TURNSTILE_SECRET_KEY",
@@ -0,0 +1 @@
1
+ AUTH_MIGRATIONS: D1Migration[];
@@ -0,0 +1 @@
1
+ await applyD1Migrations(env.AUTH_DB, env.AUTH_MIGRATIONS, "_inbis_auth_migrations");
@@ -0,0 +1,19 @@
1
+ // Auth: public origin of this Worker; https gives production cookies (Secure, __Secure- prefix).
2
+ "AUTH_BASE_URL": "http://localhost:8787",
3
+ // Sender of auth emails (verification, password reset, invitations).
4
+ "EMAIL_FROM": "React Hono App <no-reply@react-hono-app.example.com>",
5
+ // "log": print emails/SMS and keep a local outbox (GET /api/dev/outbox, /api/dev/sms-outbox;
6
+ // local hosts only). Deployed environments use "communication-service".
7
+ "EMAIL_TRANSPORT": "log",
8
+ "SMS_TRANSPORT": "log",
9
+ // The app's ID in the communication service, and the SMS sender header and template IDs
10
+ // (communication-service SMS only; see README). Unused locally.
11
+ "APP_ID": "",
12
+ "SMS_HEADER": "",
13
+ "SMS_OTP_TEMPLATE_ID": "",
14
+ "SMS_INVITATION_TEMPLATE_ID": "",
15
+ // Cloudflare's Turnstile testing site key (always passes), paired with the testing secret
16
+ // in .dev.vars.example: https://developers.cloudflare.com/turnstile/troubleshooting/testing/
17
+ "TURNSTILE_SITE_KEY": "1x00000000000000000000AA",
18
+ // "true": local test mode (src/local-test-mode.ts); the Playwright suite sets it.
19
+ "LOCAL_TEST_MODE": "",
@@ -0,0 +1,13 @@
1
+ // Auth: this environment's public origin.
2
+ "AUTH_BASE_URL": "https://react-hono-app.example.com",
3
+ "EMAIL_FROM": "React Hono App <no-reply@react-hono-app.example.com>",
4
+ "EMAIL_TRANSPORT": "communication-service",
5
+ "SMS_TRANSPORT": "communication-service",
6
+ // Registered with the communication service before the first send (see README).
7
+ "APP_ID": "react-hono-app",
8
+ // From the app's SMS registration. Startup fails while these are blank and phone sign-in is on.
9
+ "SMS_HEADER": "",
10
+ "SMS_OTP_TEMPLATE_ID": "",
11
+ "SMS_INVITATION_TEMPLATE_ID": "",
12
+ // The deployed Turnstile widget's site key (its secret: TURNSTILE_SECRET_KEY). Startup fails while blank.
13
+ "TURNSTILE_SITE_KEY": "",
@@ -0,0 +1,13 @@
1
+ // Auth: this environment's public origin.
2
+ "AUTH_BASE_URL": "https://staging.react-hono-app.example.com",
3
+ "EMAIL_FROM": "React Hono App <no-reply@react-hono-app.example.com>",
4
+ "EMAIL_TRANSPORT": "communication-service",
5
+ "SMS_TRANSPORT": "communication-service",
6
+ // Registered with the communication service before the first send (see README).
7
+ "APP_ID": "react-hono-app",
8
+ // From the app's SMS registration. Startup fails while these are blank and phone sign-in is on.
9
+ "SMS_HEADER": "",
10
+ "SMS_OTP_TEMPLATE_ID": "",
11
+ "SMS_INVITATION_TEMPLATE_ID": "",
12
+ // The deployed Turnstile widget's site key (its secret: TURNSTILE_SECRET_KEY). Startup fails while blank.
13
+ "TURNSTILE_SITE_KEY": "",
@@ -0,0 +1,6 @@
1
+ // Auth: a production-like https origin (Secure cookies), test-only secrets and
2
+ // Cloudflare's Turnstile testing secret (siteverify is faked in test/auth-fixtures.ts).
3
+ AUTH_BASE_URL: "https://app.example.test",
4
+ AUTH_SECRET: "test-only-secret-0123456789abcdefghijklmnop",
5
+ TURNSTILE_SECRET_KEY: "1x0000000000000000000000000000000AA",
6
+ AUTH_MIGRATIONS: await readD1Migrations("./node_modules/@awsmbiz/inbis-auth/migrations"),
@@ -0,0 +1 @@
1
+ <AccountMenu path={path} navigate={navigate} />
@@ -0,0 +1 @@
1
+ import { AccountMenu } from "./account-menu.js";
@@ -0,0 +1,17 @@
1
+ // Sign-in (auth module). The screens' own text comes from @awsmbiz/inbis-auth-ui.
2
+ "auth.signIn.title": "Sign in",
3
+ "auth.phoneSignIn.title": "Sign in with your phone",
4
+ "auth.signUp.title": "Create an account",
5
+ "auth.forgotPassword.title": "Forgot password",
6
+ "auth.resetPassword.title": "Reset password",
7
+ "auth.verifyEmail.title": "Verify your email",
8
+ "auth.acceptInvitation.title": "Accept your invitation",
9
+ "account.title": "Account",
10
+ "account.heading": "Your account",
11
+ "account.signedInAs": "Signed in as {name} ({identity}).",
12
+ "account.security": "Security",
13
+ "account.securityDescription": "Protect your account and see where you are signed in.",
14
+ "account.twoFactor": "Two-factor authentication",
15
+ "account.sessions": "Signed-in devices",
16
+ "account.signIn": "Sign in",
17
+ "account.signOut": "Sign out",
@@ -0,0 +1,12 @@
1
+ import { AccountPage } from "./account.js";
2
+ import {
3
+ AcceptInvitationPage,
4
+ ForgotPasswordPage,
5
+ PhoneSignInPage,
6
+ ResetPasswordPage,
7
+ SecurityPage,
8
+ SessionsPage,
9
+ SignInPage,
10
+ SignUpPage,
11
+ VerifyEmailPage,
12
+ } from "./auth-pages.js";
@@ -0,0 +1,11 @@
1
+ // Sign-in (auth module): full-page screens, then the signed-in user's pages.
2
+ { path: "/sign-in", title: "auth.signIn.title", bare: true, render: (c) => <SignInPage {...c} /> },
3
+ { path: "/sign-in/phone", title: "auth.phoneSignIn.title", bare: true, render: (c) => <PhoneSignInPage {...c} /> },
4
+ { path: "/sign-up", title: "auth.signUp.title", bare: true, render: (c) => <SignUpPage {...c} /> },
5
+ { path: "/forgot-password", title: "auth.forgotPassword.title", bare: true, render: (c) => <ForgotPasswordPage {...c} /> },
6
+ { path: "/reset-password", title: "auth.resetPassword.title", bare: true, render: (c) => <ResetPasswordPage {...c} /> },
7
+ { path: "/verify-email", title: "auth.verifyEmail.title", bare: true, render: (c) => <VerifyEmailPage {...c} /> },
8
+ { path: "/accept-invitation", title: "auth.acceptInvitation.title", bare: true, render: (c) => <AcceptInvitationPage {...c} /> },
9
+ { path: "/account/security", title: "account.twoFactor", bare: true, render: (c) => <SecurityPage {...c} /> },
10
+ { path: "/account/sessions", title: "account.sessions", bare: true, render: (c) => <SessionsPage {...c} /> },
11
+ { path: "/account", title: "account.title", render: (c) => <AccountPage {...c} /> },
@@ -0,0 +1 @@
1
+ @import "@awsmbiz/inbis-auth-ui/styles.css";