hazo_auth 6.0.0 → 7.0.1

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 (256) hide show
  1. package/README.md +233 -8
  2. package/SETUP_CHECKLIST.md +240 -0
  3. package/cli-src/cli/validate.ts +4 -0
  4. package/cli-src/lib/auth/nextauth_config.ts +101 -1
  5. package/cli-src/lib/cookies_config.server.ts +1 -0
  6. package/cli-src/lib/email_verification_config.server.ts +0 -34
  7. package/cli-src/lib/forgot_password_config.server.ts +0 -34
  8. package/cli-src/lib/login_config.server.ts +14 -31
  9. package/cli-src/lib/my_settings_config.server.ts +0 -3
  10. package/cli-src/lib/oauth_config.server.ts +58 -0
  11. package/cli-src/lib/otp_config.server.ts +91 -0
  12. package/cli-src/lib/register_config.server.ts +11 -31
  13. package/cli-src/lib/reset_password_config.server.ts +0 -31
  14. package/cli-src/lib/services/email_service.ts +3 -1
  15. package/cli-src/lib/services/email_template_manifest.ts +17 -0
  16. package/cli-src/lib/services/email_templates/otp_signin_code.html +13 -0
  17. package/cli-src/lib/services/email_templates/otp_signin_code.txt +5 -0
  18. package/cli-src/lib/services/index.ts +8 -2
  19. package/cli-src/lib/services/oauth_service.ts +197 -0
  20. package/cli-src/lib/services/otp_service.ts +295 -0
  21. package/cli-src/lib/services/session_token_service.ts +4 -1
  22. package/config/hazo_auth_config.example.ini +76 -41
  23. package/dist/cli/validate.d.ts.map +1 -1
  24. package/dist/cli/validate.js +4 -0
  25. package/dist/client.d.ts +2 -0
  26. package/dist/client.d.ts.map +1 -1
  27. package/dist/client.js +1 -0
  28. package/dist/components/layouts/create_firm/index.d.ts +4 -8
  29. package/dist/components/layouts/create_firm/index.d.ts.map +1 -1
  30. package/dist/components/layouts/create_firm/index.js +3 -3
  31. package/dist/components/layouts/email_verification/index.d.ts +4 -5
  32. package/dist/components/layouts/email_verification/index.d.ts.map +1 -1
  33. package/dist/components/layouts/email_verification/index.js +4 -4
  34. package/dist/components/layouts/forgot_password/index.d.ts +4 -5
  35. package/dist/components/layouts/forgot_password/index.d.ts.map +1 -1
  36. package/dist/components/layouts/forgot_password/index.js +2 -2
  37. package/dist/components/layouts/login/index.d.ts +19 -9
  38. package/dist/components/layouts/login/index.d.ts.map +1 -1
  39. package/dist/components/layouts/login/index.js +12 -6
  40. package/dist/components/layouts/otp/index.d.ts +17 -0
  41. package/dist/components/layouts/otp/index.d.ts.map +1 -0
  42. package/dist/components/layouts/otp/index.js +16 -0
  43. package/dist/components/layouts/register/index.d.ts +11 -7
  44. package/dist/components/layouts/register/index.d.ts.map +1 -1
  45. package/dist/components/layouts/register/index.js +8 -4
  46. package/dist/components/layouts/reset_password/index.d.ts +4 -5
  47. package/dist/components/layouts/reset_password/index.d.ts.map +1 -1
  48. package/dist/components/layouts/reset_password/index.js +5 -5
  49. package/dist/components/layouts/shared/components/already_logged_in_guard.d.ts +3 -5
  50. package/dist/components/layouts/shared/components/already_logged_in_guard.d.ts.map +1 -1
  51. package/dist/components/layouts/shared/components/already_logged_in_guard.js +2 -2
  52. package/dist/components/layouts/shared/components/facebook_sign_in_button.d.ts +25 -0
  53. package/dist/components/layouts/shared/components/facebook_sign_in_button.d.ts.map +1 -0
  54. package/dist/components/layouts/shared/components/facebook_sign_in_button.js +49 -0
  55. package/dist/components/layouts/shared/components/sidebar_layout_wrapper.d.ts.map +1 -1
  56. package/dist/components/layouts/shared/components/sidebar_layout_wrapper.js +8 -3
  57. package/dist/components/layouts/shared/components/two_column_auth_layout.d.ts +3 -6
  58. package/dist/components/layouts/shared/components/two_column_auth_layout.d.ts.map +1 -1
  59. package/dist/components/layouts/shared/components/two_column_auth_layout.js +8 -5
  60. package/dist/components/otp/OTPRequestForm.d.ts +11 -0
  61. package/dist/components/otp/OTPRequestForm.d.ts.map +1 -0
  62. package/dist/components/otp/OTPRequestForm.js +42 -0
  63. package/dist/components/otp/OTPVerifyForm.d.ts +16 -0
  64. package/dist/components/otp/OTPVerifyForm.d.ts.map +1 -0
  65. package/dist/components/otp/OTPVerifyForm.js +75 -0
  66. package/dist/components/otp/index.d.ts +5 -0
  67. package/dist/components/otp/index.d.ts.map +1 -0
  68. package/dist/components/otp/index.js +2 -0
  69. package/dist/components/ui/input-otp.d.ts +35 -0
  70. package/dist/components/ui/input-otp.d.ts.map +1 -0
  71. package/dist/components/ui/input-otp.js +44 -0
  72. package/dist/consent/consent_state.d.ts +18 -0
  73. package/dist/consent/consent_state.d.ts.map +1 -0
  74. package/dist/consent/consent_state.js +29 -0
  75. package/dist/consent/cookie_consent_banner.d.ts +11 -0
  76. package/dist/consent/cookie_consent_banner.d.ts.map +1 -0
  77. package/dist/consent/cookie_consent_banner.js +40 -0
  78. package/dist/consent/gtm_mapping.d.ts +13 -0
  79. package/dist/consent/gtm_mapping.d.ts.map +1 -0
  80. package/dist/consent/gtm_mapping.js +30 -0
  81. package/dist/consent/index.d.ts +7 -0
  82. package/dist/consent/index.d.ts.map +1 -0
  83. package/dist/consent/index.js +7 -0
  84. package/dist/consent/manage_modal.d.ts +2 -0
  85. package/dist/consent/manage_modal.d.ts.map +1 -0
  86. package/dist/consent/manage_modal.js +33 -0
  87. package/dist/consent/read_consent.d.ts +15 -0
  88. package/dist/consent/read_consent.d.ts.map +1 -0
  89. package/dist/consent/read_consent.js +23 -0
  90. package/dist/consent/use_consent.d.ts +7 -0
  91. package/dist/consent/use_consent.d.ts.map +1 -0
  92. package/dist/consent/use_consent.js +55 -0
  93. package/dist/lib/auth/nextauth_config.d.ts +10 -0
  94. package/dist/lib/auth/nextauth_config.d.ts.map +1 -1
  95. package/dist/lib/auth/nextauth_config.js +80 -2
  96. package/dist/lib/cookies_config.server.d.ts +1 -0
  97. package/dist/lib/cookies_config.server.d.ts.map +1 -1
  98. package/dist/lib/cookies_config.server.js +1 -0
  99. package/dist/lib/email_verification_config.server.d.ts +0 -3
  100. package/dist/lib/email_verification_config.server.d.ts.map +1 -1
  101. package/dist/lib/email_verification_config.server.js +0 -15
  102. package/dist/lib/forgot_password_config.server.d.ts +0 -3
  103. package/dist/lib/forgot_password_config.server.d.ts.map +1 -1
  104. package/dist/lib/forgot_password_config.server.js +0 -15
  105. package/dist/lib/login_config.server.d.ts +6 -3
  106. package/dist/lib/login_config.server.d.ts.map +1 -1
  107. package/dist/lib/login_config.server.js +7 -13
  108. package/dist/lib/my_settings_config.server.d.ts +0 -1
  109. package/dist/lib/my_settings_config.server.d.ts.map +1 -1
  110. package/dist/lib/my_settings_config.server.js +0 -2
  111. package/dist/lib/oauth_config.server.d.ts +17 -0
  112. package/dist/lib/oauth_config.server.d.ts.map +1 -1
  113. package/dist/lib/oauth_config.server.js +25 -0
  114. package/dist/lib/otp_config.server.d.ts +49 -0
  115. package/dist/lib/otp_config.server.d.ts.map +1 -0
  116. package/dist/lib/otp_config.server.js +48 -0
  117. package/dist/lib/register_config.server.d.ts +2 -3
  118. package/dist/lib/register_config.server.d.ts.map +1 -1
  119. package/dist/lib/register_config.server.js +4 -13
  120. package/dist/lib/reset_password_config.server.d.ts +0 -3
  121. package/dist/lib/reset_password_config.server.d.ts.map +1 -1
  122. package/dist/lib/reset_password_config.server.js +0 -13
  123. package/dist/lib/services/email_service.d.ts +1 -1
  124. package/dist/lib/services/email_service.d.ts.map +1 -1
  125. package/dist/lib/services/email_service.js +2 -0
  126. package/dist/lib/services/email_template_manifest.d.ts.map +1 -1
  127. package/dist/lib/services/email_template_manifest.js +17 -0
  128. package/dist/lib/services/email_templates/otp_signin_code.html +13 -0
  129. package/dist/lib/services/email_templates/otp_signin_code.txt +5 -0
  130. package/dist/lib/services/index.d.ts +2 -0
  131. package/dist/lib/services/index.d.ts.map +1 -1
  132. package/dist/lib/services/index.js +1 -0
  133. package/dist/lib/services/oauth_service.d.ts +24 -0
  134. package/dist/lib/services/oauth_service.d.ts.map +1 -1
  135. package/dist/lib/services/oauth_service.js +155 -0
  136. package/dist/lib/services/otp_service.d.ts +46 -0
  137. package/dist/lib/services/otp_service.d.ts.map +1 -0
  138. package/dist/lib/services/otp_service.js +238 -0
  139. package/dist/lib/services/session_token_service.d.ts +3 -1
  140. package/dist/lib/services/session_token_service.d.ts.map +1 -1
  141. package/dist/lib/services/session_token_service.js +4 -2
  142. package/dist/page_components/create_firm.d.ts +13 -1
  143. package/dist/page_components/create_firm.d.ts.map +1 -1
  144. package/dist/page_components/create_firm.js +10 -6
  145. package/dist/page_components/forgot_password.d.ts +1 -4
  146. package/dist/page_components/forgot_password.d.ts.map +1 -1
  147. package/dist/page_components/forgot_password.js +2 -6
  148. package/dist/page_components/login.d.ts +1 -4
  149. package/dist/page_components/login.d.ts.map +1 -1
  150. package/dist/page_components/login.js +2 -6
  151. package/dist/page_components/otp.d.ts +4 -0
  152. package/dist/page_components/otp.d.ts.map +1 -0
  153. package/dist/page_components/otp.js +5 -0
  154. package/dist/page_components/register.d.ts +1 -4
  155. package/dist/page_components/register.d.ts.map +1 -1
  156. package/dist/page_components/register.js +2 -6
  157. package/dist/page_components/reset_password.d.ts +1 -4
  158. package/dist/page_components/reset_password.d.ts.map +1 -1
  159. package/dist/page_components/reset_password.js +2 -6
  160. package/dist/page_components/verify_email.d.ts +1 -4
  161. package/dist/page_components/verify_email.d.ts.map +1 -1
  162. package/dist/page_components/verify_email.js +2 -6
  163. package/dist/server/routes/index.d.ts +3 -0
  164. package/dist/server/routes/index.d.ts.map +1 -1
  165. package/dist/server/routes/index.js +4 -0
  166. package/dist/server/routes/me.d.ts.map +1 -1
  167. package/dist/server/routes/me.js +43 -1
  168. package/dist/server/routes/oauth_facebook_callback.d.ts +8 -0
  169. package/dist/server/routes/oauth_facebook_callback.d.ts.map +1 -0
  170. package/dist/server/routes/oauth_facebook_callback.js +157 -0
  171. package/dist/server/routes/oauth_google_callback.js +1 -1
  172. package/dist/server/routes/otp/request.d.ts +3 -0
  173. package/dist/server/routes/otp/request.d.ts.map +1 -0
  174. package/dist/server/routes/otp/request.js +33 -0
  175. package/dist/server/routes/otp/verify.d.ts +3 -0
  176. package/dist/server/routes/otp/verify.d.ts.map +1 -0
  177. package/dist/server/routes/otp/verify.js +58 -0
  178. package/dist/server-lib.d.ts +3 -0
  179. package/dist/server-lib.d.ts.map +1 -1
  180. package/dist/server-lib.js +2 -0
  181. package/dist/server_pages/forgot_password.d.ts +13 -17
  182. package/dist/server_pages/forgot_password.d.ts.map +1 -1
  183. package/dist/server_pages/forgot_password.js +12 -8
  184. package/dist/server_pages/forgot_password_client_wrapper.d.ts +7 -6
  185. package/dist/server_pages/forgot_password_client_wrapper.d.ts.map +1 -1
  186. package/dist/server_pages/forgot_password_client_wrapper.js +2 -2
  187. package/dist/server_pages/login.d.ts +22 -21
  188. package/dist/server_pages/login.d.ts.map +1 -1
  189. package/dist/server_pages/login.js +15 -19
  190. package/dist/server_pages/login_client_wrapper.d.ts +10 -6
  191. package/dist/server_pages/login_client_wrapper.d.ts.map +1 -1
  192. package/dist/server_pages/login_client_wrapper.js +2 -2
  193. package/dist/server_pages/my_settings.d.ts +2 -0
  194. package/dist/server_pages/my_settings.d.ts.map +1 -1
  195. package/dist/server_pages/my_settings.js +8 -2
  196. package/dist/server_pages/otp.d.ts +56 -0
  197. package/dist/server_pages/otp.d.ts.map +1 -0
  198. package/dist/server_pages/otp.js +45 -0
  199. package/dist/server_pages/register.d.ts +19 -16
  200. package/dist/server_pages/register.d.ts.map +1 -1
  201. package/dist/server_pages/register.js +15 -12
  202. package/dist/server_pages/register_client_wrapper.d.ts +10 -6
  203. package/dist/server_pages/register_client_wrapper.d.ts.map +1 -1
  204. package/dist/server_pages/register_client_wrapper.js +2 -2
  205. package/dist/server_pages/reset_password.d.ts +11 -16
  206. package/dist/server_pages/reset_password.d.ts.map +1 -1
  207. package/dist/server_pages/reset_password.js +11 -9
  208. package/dist/server_pages/reset_password_client_wrapper.d.ts +7 -6
  209. package/dist/server_pages/reset_password_client_wrapper.d.ts.map +1 -1
  210. package/dist/server_pages/reset_password_client_wrapper.js +2 -2
  211. package/dist/server_pages/verify_email.d.ts +11 -17
  212. package/dist/server_pages/verify_email.d.ts.map +1 -1
  213. package/dist/server_pages/verify_email.js +11 -8
  214. package/dist/server_pages/verify_email_client_wrapper.d.ts +7 -6
  215. package/dist/server_pages/verify_email_client_wrapper.d.ts.map +1 -1
  216. package/dist/server_pages/verify_email_client_wrapper.js +2 -2
  217. package/dist/strings/default_strings.d.ts +47 -0
  218. package/dist/strings/default_strings.d.ts.map +1 -0
  219. package/dist/strings/default_strings.js +18 -0
  220. package/dist/strings/index.d.ts +4 -0
  221. package/dist/strings/index.d.ts.map +1 -0
  222. package/dist/strings/index.js +3 -0
  223. package/dist/strings/strings_context.d.ts +12 -0
  224. package/dist/strings/strings_context.d.ts.map +1 -0
  225. package/dist/strings/strings_context.js +23 -0
  226. package/dist/strings/strings_provider.d.ts +26 -0
  227. package/dist/strings/strings_provider.d.ts.map +1 -0
  228. package/dist/strings/strings_provider.js +45 -0
  229. package/dist/theme/create_theme.d.ts +7 -0
  230. package/dist/theme/create_theme.d.ts.map +1 -0
  231. package/dist/theme/create_theme.js +97 -0
  232. package/dist/theme/hex_to_hsl.d.ts +16 -0
  233. package/dist/theme/hex_to_hsl.d.ts.map +1 -0
  234. package/dist/theme/hex_to_hsl.js +110 -0
  235. package/dist/theme/index.d.ts +4 -0
  236. package/dist/theme/index.d.ts.map +1 -0
  237. package/dist/theme/index.js +3 -0
  238. package/dist/theme/luminance.d.ts +11 -0
  239. package/dist/theme/luminance.d.ts.map +1 -0
  240. package/dist/theme/luminance.js +45 -0
  241. package/dist/theme/theme_provider.d.ts +14 -0
  242. package/dist/theme/theme_provider.d.ts.map +1 -0
  243. package/dist/theme/theme_provider.js +23 -0
  244. package/dist/theme/theme_types.d.ts +36 -0
  245. package/dist/theme/theme_types.d.ts.map +1 -0
  246. package/dist/theme/theme_types.js +1 -0
  247. package/dist/themes/index.d.ts +3 -0
  248. package/dist/themes/index.d.ts.map +1 -0
  249. package/dist/themes/index.js +2 -0
  250. package/dist/themes/preset_indigo_sunset.d.ts +3 -0
  251. package/dist/themes/preset_indigo_sunset.d.ts.map +1 -0
  252. package/dist/themes/preset_indigo_sunset.js +20 -0
  253. package/dist/themes/preset_neutral.d.ts +3 -0
  254. package/dist/themes/preset_neutral.d.ts.map +1 -0
  255. package/dist/themes/preset_neutral.js +14 -0
  256. package/package.json +36 -2
package/README.md CHANGED
@@ -2,6 +2,98 @@
2
2
 
3
3
  A reusable authentication UI component package powered by Next.js, TailwindCSS, and shadcn. It integrates `hazo_config` for configuration management and `hazo_connect` for data access, enabling future components to stay aligned with platform conventions.
4
4
 
5
+ ## What's New in v7.0.0
6
+
7
+ **Themes, cookie consent, text overrides, and Facebook OAuth.**
8
+
9
+ ### Highlights
10
+
11
+ - **Pluggable theme system** — `createTheme` + `<HazoAuthThemeProvider>` (zero FOUC, compiles to shadcn CSS variables). Use a preset or build from scratch.
12
+ - **Facebook OAuth** — `<FacebookSignInButton>` and `handle_facebook_oauth_login` service. Strict linking by default (verified accounts only).
13
+ - **Cookie consent banner** — `<CookieConsentBanner>` from `hazo_auth/consent`. GTM optional, 4 categories, theme-driven.
14
+ - **Text override system** — `<HazoAuthStringsProvider>` and per-page `title`/`subtitle`/`ctaText`/`legalText` props. INI text keys removed.
15
+ - **Preset themes** — `preset_neutral` (default look, no config needed) and `preset_indigo_sunset` from `hazo_auth/themes`.
16
+
17
+ ### Breaking Changes Summary
18
+
19
+ - Per-page `imageSrc`/`imageAlt`/`imageBackgroundColor` props removed — use `theme.brandPanel`.
20
+ - INI text keys (`title`, `subtitle`, `cta_text`, `legal_text`) removed from layout sections — use `HazoAuthStringsProvider` or per-page props.
21
+ - Facebook OAuth requires new env vars (`HAZO_AUTH_FACEBOOK_APP_ID`, `HAZO_AUTH_FACEBOOK_APP_SECRET`).
22
+
23
+ > Full upgrade guide: [MIGRATION.md](./MIGRATION.md)
24
+
25
+ ---
26
+
27
+ ## What's New in v6.1.0
28
+
29
+ **Email-OTP sign-in** — a passwordless, OAuth-free way to sign in via a 6-digit code emailed to the user. Targeted at single-operator deployments that don't want to wire Google OAuth or manage passwords.
30
+
31
+ ### Highlights
32
+
33
+ - **New routes** — `POST /api/hazo_auth/otp/request` and `POST /api/hazo_auth/otp/verify`
34
+ - **New components** — `<OTPRequestForm/>` and `<OTPVerifyForm/>` exported from `hazo_auth/client`
35
+ - **New page** — `/hazo_auth/otp` (zero-config) via `hazo_auth/pages/otp`
36
+ - **Sliding 7-day session** — OTP sessions auto-extend on `/me` when within 24h of expiry. OAuth/password sessions keep their existing 30-day fixed behaviour.
37
+ - **Auto-register (opt-in)** — set `otp_auto_register = true` to let unknown emails sign in on first successful /verify
38
+ - **Rate limited** — 3 requests/email/15min, 20 requests/IP/hour; per-code attempts capped at 5
39
+
40
+ ### Consumer example
41
+
42
+ Mount the routes:
43
+
44
+ ```ts
45
+ // app/api/hazo_auth/otp/request/route.ts
46
+ export { otpRequestPOST as POST } from "hazo_auth/server/routes";
47
+
48
+ // app/api/hazo_auth/otp/verify/route.ts
49
+ export { otpVerifyPOST as POST } from "hazo_auth/server/routes";
50
+ ```
51
+
52
+ Render the zero-config page:
53
+
54
+ ```tsx
55
+ // app/hazo_auth/otp/page.tsx
56
+ export { default } from "hazo_auth/pages/otp";
57
+ ```
58
+
59
+ Or compose components yourself:
60
+
61
+ ```tsx
62
+ "use client";
63
+ import { OTPRequestForm, OTPVerifyForm } from "hazo_auth/client";
64
+ import { useState } from "react";
65
+
66
+ export default function CustomOtp() {
67
+ const [sent_to, set_sent_to] = useState<string | null>(null);
68
+ return sent_to
69
+ ? <OTPVerifyForm email={sent_to} redirect_url="/" />
70
+ : <OTPRequestForm on_sent={set_sent_to} />;
71
+ }
72
+ ```
73
+
74
+ ### Required setup
75
+
76
+ 1. Apply migration: `npm run migrate migrations/015_email_otp.sql`
77
+ 2. Add `[hazo_auth__otp]` section to `config/hazo_auth_config.ini` (template in `hazo_auth_config.example.ini`)
78
+ 3. `hazo_notify ^3.0.0` (existing peer dep) must be configured to deliver email
79
+ 4. New peer dep: `input-otp` (optional; required only if you render `<OTPVerifyForm/>`)
80
+ 5. **Add OTP routes to your middleware/proxy `public_routes`** — unauthenticated users land on the OTP page, so the page route and its API routes must bypass auth guards:
81
+
82
+ ```typescript
83
+ // middleware.ts / proxy.ts (in your consuming app)
84
+ const public_routes = [
85
+ // ... existing entries ...
86
+ "/hazo_auth/otp", // OTP sign-in page (public — users arrive unauthenticated)
87
+ "/api/hazo_auth/otp", // OTP request + verify API routes
88
+ ];
89
+ ```
90
+
91
+ Without this your middleware redirects unauthenticated users away from the sign-in page before they can authenticate.
92
+
93
+ See `MIGRATION.md` "v6.0.x → v6.1" for the full upgrade walkthrough.
94
+
95
+ ---
96
+
5
97
  ### What's New in v6.0.0 🚨 BREAKING CHANGE
6
98
 
7
99
  **`TenantAuthResult.organization` / `.organization_id` renamed to `.selected_scope` / `.selected_scope_id`.**
@@ -380,23 +472,30 @@ export default function Page() {
380
472
  }
381
473
  ```
382
474
 
383
- **Customizing Visual Appearance (Optional):**
475
+ **Customizing Visual Appearance (v7.0+):**
384
476
 
385
- ```typescript
386
- // All pages accept optional visual props
477
+ Use `HazoAuthThemeProvider` instead of per-page image props (which were removed in v7.0):
478
+
479
+ ```tsx
480
+ import { createTheme, HazoAuthThemeProvider } from "hazo_auth/theme";
387
481
  import { LoginPage } from "hazo_auth/pages/login";
388
482
 
483
+ const theme = createTheme({
484
+ layout: "split",
485
+ brandPanel: { logoSrc: "/logo.svg", tagline: "Welcome.", backgroundGradient: "linear-gradient(135deg, #3730A3, #F59E0B)" },
486
+ });
487
+
389
488
  export default function Page() {
390
489
  return (
391
- <LoginPage
392
- image_src="/custom-login-image.jpg"
393
- image_alt="My company logo"
394
- image_background_color="#f0f0f0"
395
- />
490
+ <HazoAuthThemeProvider theme={theme}>
491
+ <LoginPage theme={theme} />
492
+ </HazoAuthThemeProvider>
396
493
  );
397
494
  }
398
495
  ```
399
496
 
497
+ See the [Theming](#theming-v700) section for all options.
498
+
400
499
  **Embedding MySettings in Your Dashboard:**
401
500
 
402
501
  ```typescript
@@ -586,6 +685,132 @@ The dark class is typically added by next-themes or similar theme providers.
586
685
 
587
686
  ---
588
687
 
688
+ ## Theming (v7.0.0+)
689
+
690
+ hazo_auth v7 ships a pluggable theme system. `<HazoAuthThemeProvider>` is a Server Component that injects CSS variables at `:root` with zero FOUC.
691
+
692
+ ### Option 1: Use a preset directly
693
+
694
+ ```tsx
695
+ import { HazoAuthThemeProvider } from "hazo_auth/theme";
696
+ import { preset_indigo_sunset } from "hazo_auth/themes";
697
+
698
+ // app/layout.tsx
699
+ export default function RootLayout({ children }) {
700
+ return (
701
+ <html>
702
+ <body>
703
+ <HazoAuthThemeProvider theme={preset_indigo_sunset}>
704
+ {children}
705
+ </HazoAuthThemeProvider>
706
+ </body>
707
+ </html>
708
+ );
709
+ }
710
+ ```
711
+
712
+ `preset_neutral` is the default look (slate-700 palette, centered layout). Auth pages render acceptably with no theme configuration at all.
713
+
714
+ ### Option 2: Customize a preset
715
+
716
+ ```tsx
717
+ import { createTheme, HazoAuthThemeProvider } from "hazo_auth/theme";
718
+ import { preset_indigo_sunset } from "hazo_auth/themes";
719
+
720
+ const theme = createTheme({
721
+ ...preset_indigo_sunset,
722
+ brandPanel: { logoSrc: "/logo.svg", tagline: "Welcome." },
723
+ });
724
+
725
+ <HazoAuthThemeProvider theme={theme}>{children}</HazoAuthThemeProvider>
726
+ ```
727
+
728
+ ### Option 3: Build from scratch
729
+
730
+ ```tsx
731
+ import { createTheme, HazoAuthThemeProvider } from "hazo_auth/theme";
732
+
733
+ const theme = createTheme({
734
+ colors: { primary: "#2563EB", primaryForeground: "#ffffff" },
735
+ layout: "split",
736
+ brandPanel: {
737
+ logoSrc: "/logo.svg",
738
+ tagline: "Welcome to MyApp.",
739
+ backgroundGradient: "linear-gradient(135deg, #2563EB, #7C3AED)",
740
+ },
741
+ });
742
+
743
+ <HazoAuthThemeProvider theme={theme}>{children}</HazoAuthThemeProvider>
744
+ ```
745
+
746
+ **Note:** `preset_indigo_sunset` uses `var(--font-display)` and `var(--font-body)`. Wire these in `app/layout.tsx` using `next/font`. See [MIGRATION.md §3](./MIGRATION.md).
747
+
748
+ ---
749
+
750
+ ## Cookie Consent (v7.0.0+)
751
+
752
+ ```tsx
753
+ import { CookieConsentBanner } from "hazo_auth/consent";
754
+
755
+ // In your root layout or a page:
756
+ <CookieConsentBanner />
757
+
758
+ // With GTM:
759
+ <CookieConsentBanner enableGTM gtmContainerId="GTM-XXXX" />
760
+ ```
761
+
762
+ Read consent server-side:
763
+
764
+ ```tsx
765
+ import { read_consent } from "hazo_auth/consent";
766
+
767
+ export async function GET(request: NextRequest) {
768
+ const consent = read_consent(request.headers);
769
+ if (consent.analytics) {
770
+ // Track event
771
+ }
772
+ }
773
+ ```
774
+
775
+ Use the `useConsent` hook client-side:
776
+
777
+ ```tsx
778
+ "use client";
779
+ import { useConsent } from "hazo_auth/consent";
780
+
781
+ export function MyComponent() {
782
+ const { analytics, marketing } = useConsent();
783
+ // Syncs across tabs via custom event
784
+ }
785
+ ```
786
+
787
+ ---
788
+
789
+ ## Strings & Text Overrides (v7.0.0+)
790
+
791
+ INI text keys (`title`, `subtitle`, `cta_text`, `legal_text`) are removed in v7. Use `<HazoAuthStringsProvider>` or per-page props instead.
792
+
793
+ ### Global overrides (app/layout.tsx)
794
+
795
+ ```tsx
796
+ import { HazoAuthStringsProvider } from "hazo_auth/strings";
797
+
798
+ <HazoAuthStringsProvider strings={{ login: { title: "Sign in to MyApp" } }}>
799
+ {children}
800
+ </HazoAuthStringsProvider>
801
+ ```
802
+
803
+ ### Per-page overrides
804
+
805
+ ```tsx
806
+ <LoginPage title="Sign in to MyApp" subtitle="Access your workspace" />
807
+ <RegisterPage title="Create your account" ctaText="Get started" />
808
+ ```
809
+
810
+ `DEFAULT_STRINGS` is exported from `hazo_auth/strings` for reference.
811
+
812
+ ---
813
+
589
814
  ## Configuration Setup
590
815
 
591
816
  After installing the package, you need to set up configuration files in your project root:
@@ -1443,6 +1443,246 @@ export { POST } from "hazo_auth/server/routes/pin_login";
1443
1443
 
1444
1444
  ---
1445
1445
 
1446
+ ## Phase 5.3: Email-OTP Sign-in Setup (Optional)
1447
+
1448
+ Email-OTP sign-in lets users authenticate with a 6-digit code sent to their email — no password or Google OAuth required. Skip this phase if your app uses only password or OAuth authentication.
1449
+
1450
+ ### Step 5.3.1: Apply the OTP migration
1451
+
1452
+ ```bash
1453
+ npm run migrate migrations/015_email_otp.sql
1454
+ ```
1455
+
1456
+ This creates the `hazo_email_otps` table (stores hashed codes with expiry).
1457
+
1458
+ **Verify the table exists:**
1459
+ ```bash
1460
+ sqlite3 data/hazo_auth.sqlite ".tables" | tr ' ' '\n' | grep hazo_email_otps
1461
+ # Expected: hazo_email_otps
1462
+ ```
1463
+
1464
+ ### Step 5.3.2: Add OTP config section
1465
+
1466
+ Add to `config/hazo_auth_config.ini`:
1467
+
1468
+ ```ini
1469
+ [hazo_auth__otp]
1470
+ # Whether OTP sign-in is enabled
1471
+ enabled = true
1472
+
1473
+ # Auto-register unknown emails on first successful verify (default: false)
1474
+ otp_auto_register = false
1475
+
1476
+ # Code expiry in minutes (default: 10)
1477
+ code_expiry_minutes = 10
1478
+ ```
1479
+
1480
+ ### Step 5.3.3: Create OTP API routes
1481
+
1482
+ ```bash
1483
+ npx hazo_auth generate-routes
1484
+ ```
1485
+
1486
+ Or manually:
1487
+
1488
+ `app/api/hazo_auth/otp/request/route.ts`:
1489
+ ```typescript
1490
+ export { otpRequestPOST as POST } from "hazo_auth/server/routes";
1491
+ ```
1492
+
1493
+ `app/api/hazo_auth/otp/verify/route.ts`:
1494
+ ```typescript
1495
+ export { otpVerifyPOST as POST } from "hazo_auth/server/routes";
1496
+ ```
1497
+
1498
+ ### Step 5.3.4: Create the OTP sign-in page
1499
+
1500
+ `app/hazo_auth/otp/page.tsx`:
1501
+ ```typescript
1502
+ export { default } from "hazo_auth/pages/otp";
1503
+ ```
1504
+
1505
+ ### Step 5.3.5: Add OTP routes to middleware/proxy public_routes (REQUIRED)
1506
+
1507
+ **This step is critical.** Unauthenticated users arrive at the OTP sign-in page before they have auth cookies. If the OTP routes are not in `public_routes`, your middleware will redirect them to `/login` in a loop.
1508
+
1509
+ Edit your `middleware.ts` or `proxy.ts`:
1510
+
1511
+ ```typescript
1512
+ const public_routes = [
1513
+ // ... existing entries ...
1514
+ "/hazo_auth/otp", // OTP sign-in page (public — users arrive unauthenticated)
1515
+ "/api/hazo_auth/otp", // OTP request + verify API routes
1516
+ ];
1517
+ ```
1518
+
1519
+ ### Step 5.3.6: Install optional peer dep (if using OTPVerifyForm)
1520
+
1521
+ If you render `<OTPVerifyForm/>` directly (rather than using the zero-config page), install:
1522
+
1523
+ ```bash
1524
+ npm install input-otp
1525
+ ```
1526
+
1527
+ **OTP Setup Checklist:**
1528
+ - [ ] `hazo_email_otps` table created (`npm run migrate migrations/015_email_otp.sql`)
1529
+ - [ ] `[hazo_auth__otp]` section added to `hazo_auth_config.ini`
1530
+ - [ ] OTP API routes created (`/api/hazo_auth/otp/request` and `/api/hazo_auth/otp/verify`)
1531
+ - [ ] OTP page created (`/hazo_auth/otp`)
1532
+ - [ ] `/hazo_auth/otp` and `/api/hazo_auth/otp` added to middleware `public_routes`
1533
+ - [ ] `input-otp` installed (only if using `<OTPVerifyForm/>` directly)
1534
+ - [ ] Email delivery configured (`hazo_notify` + valid `ZEPTOMAIL_API_KEY`)
1535
+
1536
+ ---
1537
+
1538
+ ## Phase 5.4: Theme Setup (v7.0.0+)
1539
+
1540
+ ### Step 5.4.1: Mount HazoAuthThemeProvider (Required for themed auth pages)
1541
+
1542
+ Mount `<HazoAuthThemeProvider>` in `app/layout.tsx`. Use `preset_neutral` for the default look, or configure a custom theme.
1543
+
1544
+ **Option A: Default look (no config needed)**
1545
+ ```tsx
1546
+ import { HazoAuthThemeProvider } from "hazo_auth/theme";
1547
+ import { preset_neutral } from "hazo_auth/themes";
1548
+
1549
+ export default function RootLayout({ children }) {
1550
+ return (
1551
+ <html>
1552
+ <body>
1553
+ <HazoAuthThemeProvider theme={preset_neutral}>
1554
+ {children}
1555
+ </HazoAuthThemeProvider>
1556
+ </body>
1557
+ </html>
1558
+ );
1559
+ }
1560
+ ```
1561
+
1562
+ **Option B: Custom theme**
1563
+ ```tsx
1564
+ import { createTheme, HazoAuthThemeProvider } from "hazo_auth/theme";
1565
+
1566
+ const theme = createTheme({
1567
+ colors: { primary: "#2563EB" },
1568
+ layout: "split",
1569
+ brandPanel: { logoSrc: "/logo.svg", tagline: "Welcome." },
1570
+ });
1571
+
1572
+ <HazoAuthThemeProvider theme={theme}>{children}</HazoAuthThemeProvider>
1573
+ ```
1574
+
1575
+ **If using `preset_indigo_sunset` (custom fonts required):**
1576
+
1577
+ Wire `next/font` in `app/layout.tsx` to expose `--font-display` and `--font-body` CSS variables. See [MIGRATION.md §3](./MIGRATION.md).
1578
+
1579
+ ```tsx
1580
+ import { Crimson_Pro, DM_Sans } from "next/font/google";
1581
+ const display = Crimson_Pro({ subsets: ["latin"], variable: "--font-display" });
1582
+ const body = DM_Sans({ subsets: ["latin"], variable: "--font-body" });
1583
+
1584
+ export default function Layout({ children }) {
1585
+ return <html className={`${display.variable} ${body.variable}`}>{children}</html>;
1586
+ }
1587
+ ```
1588
+
1589
+ **Checklist:**
1590
+ - [ ] `<HazoAuthThemeProvider>` mounted in `app/layout.tsx`
1591
+ - [ ] Preset or custom theme configured
1592
+ - [ ] If using `preset_indigo_sunset`: `next/font` wired for `--font-display` and `--font-body`
1593
+
1594
+ ---
1595
+
1596
+ ## Phase 5.5: Facebook OAuth Setup (Optional, v7.0.0+)
1597
+
1598
+ ### Step 5.5.1: Get Facebook App credentials
1599
+
1600
+ 1. Go to [Meta Developers](https://developers.facebook.com/)
1601
+ 2. Create or select an app
1602
+ 3. Add the **Facebook Login** product
1603
+ 4. Set authorized redirect URIs: `http://localhost:3000/api/auth/callback/facebook` (dev) and your production URL
1604
+ 5. Copy **App ID** and **App Secret**
1605
+
1606
+ ### Step 5.5.2: Add Facebook OAuth environment variables
1607
+
1608
+ Add to your `.env.local`:
1609
+ ```env
1610
+ HAZO_AUTH_FACEBOOK_APP_ID=your_app_id
1611
+ HAZO_AUTH_FACEBOOK_APP_SECRET=your_app_secret
1612
+ ```
1613
+
1614
+ The Facebook sign-in button is hidden when these vars are not set.
1615
+
1616
+ ### Step 5.5.3: Enable Facebook in config
1617
+
1618
+ Add to `config/hazo_auth_config.ini`:
1619
+ ```ini
1620
+ [hazo_auth__oauth]
1621
+ enable_facebook = true
1622
+
1623
+ ; Facebook: default false — stricter security posture
1624
+ ; Set true only if you want to auto-link to unverified email accounts
1625
+ auto_link_unverified_accounts_facebook = false
1626
+ ```
1627
+
1628
+ ### Step 5.5.4: Apply Facebook migration
1629
+
1630
+ ```bash
1631
+ npm run migrate migrations/016_facebook_oauth.sql
1632
+ ```
1633
+
1634
+ ### Step 5.5.5: Create Facebook OAuth callback route
1635
+
1636
+ ```typescript
1637
+ // app/api/hazo_auth/oauth/facebook/callback/route.ts
1638
+ export { GET } from "hazo_auth/server/routes/oauth_facebook_callback";
1639
+ ```
1640
+
1641
+ **Facebook OAuth Checklist:**
1642
+ - [ ] Facebook App credentials obtained
1643
+ - [ ] `HAZO_AUTH_FACEBOOK_APP_ID` and `HAZO_AUTH_FACEBOOK_APP_SECRET` set in `.env.local`
1644
+ - [ ] `enable_facebook = true` in `[hazo_auth__oauth]`
1645
+ - [ ] Migration `016_facebook_oauth.sql` applied
1646
+ - [ ] Facebook callback route created
1647
+
1648
+ ---
1649
+
1650
+ ## Phase 5.6: Cookie Consent Setup (Optional, v7.0.0+)
1651
+
1652
+ ### Step 5.6.1: Mount CookieConsentBanner
1653
+
1654
+ Mount `<CookieConsentBanner>` in your root layout:
1655
+
1656
+ ```tsx
1657
+ import { CookieConsentBanner } from "hazo_auth/consent";
1658
+
1659
+ export default function RootLayout({ children }) {
1660
+ return (
1661
+ <html>
1662
+ <body>
1663
+ <HazoAuthThemeProvider theme={preset_neutral}>
1664
+ {children}
1665
+ <CookieConsentBanner />
1666
+ </HazoAuthThemeProvider>
1667
+ </body>
1668
+ </html>
1669
+ );
1670
+ }
1671
+ ```
1672
+
1673
+ **With GTM (optional):**
1674
+ ```tsx
1675
+ <CookieConsentBanner enableGTM gtmContainerId="GTM-XXXX" />
1676
+ ```
1677
+
1678
+ See the README [Cookie Consent](#cookie-consent-v700) section for server-side `read_consent` usage.
1679
+
1680
+ **Checklist:**
1681
+ - [ ] `<CookieConsentBanner>` mounted in root layout
1682
+ - [ ] GTM container ID configured (if using GTM)
1683
+
1684
+ ---
1685
+
1446
1686
  ## Phase 6: Verification Tests
1447
1687
 
1448
1688
  Run these tests to verify your setup is working correctly.
@@ -64,6 +64,9 @@ const REQUIRED_API_ROUTES = [
64
64
  { path: "api/hazo_auth/user_management/users/roles", method: "GET" },
65
65
  { path: "api/hazo_auth/user_management/users/roles", method: "POST" },
66
66
  { path: "api/hazo_auth/user_management/users/roles", method: "PUT" },
67
+ // OTP routes
68
+ { path: "api/hazo_auth/otp/request", method: "POST" },
69
+ { path: "api/hazo_auth/otp/verify", method: "POST" },
67
70
  ];
68
71
 
69
72
  // section: helpers
@@ -534,6 +537,7 @@ const REQUIRED_TABLES = [
534
537
  "hazo_role_permissions",
535
538
  "hazo_invitations",
536
539
  "hazo_refresh_tokens",
540
+ "hazo_email_otps",
537
541
  ];
538
542
 
539
543
  const TEXT_ID_TABLES = [
@@ -5,8 +5,10 @@ import type { JWT } from "next-auth/jwt";
5
5
  // ESM/CJS interop: next-auth providers are CommonJS, handle both export scenarios
6
6
  import GoogleProviderImport from "next-auth/providers/google";
7
7
  const GoogleProvider = (GoogleProviderImport as any).default || GoogleProviderImport;
8
+ import FacebookProviderImport from "next-auth/providers/facebook";
9
+ const FacebookProvider = (FacebookProviderImport as any).default || FacebookProviderImport;
8
10
  import { get_oauth_config } from "../oauth_config.server.js";
9
- import { handle_google_oauth_login } from "../services/oauth_service.js";
11
+ import { handle_google_oauth_login, handle_facebook_oauth_login } from "../services/oauth_service.js";
10
12
  import { get_hazo_connect_instance } from "../hazo_connect_instance.server.js";
11
13
  import { create_app_logger } from "../app_logger.js";
12
14
 
@@ -35,6 +37,13 @@ export type NextAuthCallbackProfile = {
35
37
  email_verified?: boolean;
36
38
  };
37
39
 
40
+ export type FacebookCallbackProfile = {
41
+ id?: string;
42
+ name?: string;
43
+ email?: string;
44
+ picture?: { data?: { url: string } } | string;
45
+ };
46
+
38
47
  // section: config
39
48
  /**
40
49
  * Gets NextAuth.js configuration with enabled OAuth providers
@@ -67,6 +76,21 @@ export function get_nextauth_config(): AuthOptions {
67
76
  }
68
77
  }
69
78
 
79
+ // Add Facebook provider if enabled and credentials are present
80
+ if (oauth_config.enable_facebook && oauth_config.facebook_client_id && oauth_config.facebook_client_secret) {
81
+ providers.push(
82
+ FacebookProvider({
83
+ clientId: oauth_config.facebook_client_id,
84
+ clientSecret: oauth_config.facebook_client_secret,
85
+ authorization: {
86
+ params: {
87
+ scope: "email,public_profile",
88
+ },
89
+ },
90
+ })
91
+ );
92
+ }
93
+
70
94
  return {
71
95
  providers,
72
96
  pages: {
@@ -88,6 +112,9 @@ export function get_nextauth_config(): AuthOptions {
88
112
  if (url.includes("/api/hazo_auth/oauth/google/callback")) {
89
113
  return url;
90
114
  }
115
+ if (url.includes("/api/hazo_auth/oauth/facebook/callback")) {
116
+ return url;
117
+ }
91
118
 
92
119
  // If URL is relative or same origin, allow it
93
120
  if (url.startsWith("/")) {
@@ -162,6 +189,75 @@ export function get_nextauth_config(): AuthOptions {
162
189
  return false;
163
190
  }
164
191
  }
192
+
193
+ if (account?.provider === "facebook" && profile) {
194
+ try {
195
+ const fbProfile = profile as FacebookCallbackProfile;
196
+ const hazoConnect = get_hazo_connect_instance();
197
+ const current_oauth_config = get_oauth_config();
198
+
199
+ // Resolve profile picture URL from Facebook's nested structure
200
+ let fb_picture_url: string | undefined;
201
+ if (fbProfile.picture) {
202
+ if (typeof fbProfile.picture === "string") {
203
+ fb_picture_url = fbProfile.picture;
204
+ } else if (fbProfile.picture?.data?.url) {
205
+ fb_picture_url = fbProfile.picture.data.url;
206
+ }
207
+ }
208
+ if (!fb_picture_url && user.image) {
209
+ fb_picture_url = user.image ?? undefined;
210
+ }
211
+
212
+ logger.info("nextauth_facebook_signin_attempt", {
213
+ email: user.email,
214
+ facebook_id: fbProfile.id,
215
+ name: user.name,
216
+ });
217
+
218
+ const result = await handle_facebook_oauth_login(
219
+ hazoConnect,
220
+ {
221
+ facebook_id: fbProfile.id || account.providerAccountId,
222
+ email: user.email ?? fbProfile.email ?? null,
223
+ name: user.name || fbProfile.name || undefined,
224
+ profile_picture_url: fb_picture_url,
225
+ },
226
+ { auto_link_unverified: current_oauth_config.auto_link_unverified_accounts_facebook }
227
+ );
228
+
229
+ if (!result.success) {
230
+ logger.error("nextauth_facebook_signin_failed", {
231
+ email: user.email,
232
+ error: result.error,
233
+ });
234
+ if (result.error === "link_blocked_unverified") {
235
+ return `/hazo_auth/login?error=link_blocked_unverified`;
236
+ }
237
+ return false;
238
+ }
239
+
240
+ logger.info("nextauth_facebook_signin_success", {
241
+ user_id: result.user_id,
242
+ email: result.email,
243
+ is_new_user: result.is_new_user,
244
+ was_linked: result.was_linked,
245
+ });
246
+
247
+ // Store user_id in account for the JWT callback to pick up
248
+ (account as Record<string, unknown>).hazo_user_id = result.user_id;
249
+
250
+ return true;
251
+ } catch (error) {
252
+ const errorMessage = error instanceof Error ? error.message : "Unknown error";
253
+ logger.error("nextauth_facebook_signin_exception", {
254
+ email: user.email,
255
+ error: errorMessage,
256
+ });
257
+ return false;
258
+ }
259
+ }
260
+
165
261
  return true;
166
262
  },
167
263
  /**
@@ -225,5 +321,9 @@ export function has_oauth_providers(): boolean {
225
321
  if (has_google_credentials) return true;
226
322
  }
227
323
 
324
+ if (oauth_config.enable_facebook && oauth_config.facebook_client_id && oauth_config.facebook_client_secret) {
325
+ return true;
326
+ }
327
+
228
328
  return false;
229
329
  }
@@ -27,6 +27,7 @@ export const BASE_COOKIE_NAMES = {
27
27
  USER_ID: "hazo_auth_user_id",
28
28
  USER_EMAIL: "hazo_auth_user_email",
29
29
  SESSION: "hazo_auth_session",
30
+ SESSION_KIND: "hazo_auth_session_kind", // v6.1: marks OTP-issued sessions so /me can apply sliding expiry
30
31
  DEV_LOCK: "hazo_auth_dev_lock",
31
32
  SCOPE_ID: "hazo_auth_scope_id", // v5.2: Tenant context cookie for multi-tenancy
32
33
  ANON_ID: "hazo_auth_anon_id", // v5.2: Stable opaque per-visitor ID for anonymous flows (e.g. hazo_feedback)