hazo_auth 10.7.1 → 10.8.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 (35) hide show
  1. package/README.md +7 -0
  2. package/cli-src/cli/generate.ts +2 -0
  3. package/cli-src/lib/services/branding_service.ts +64 -0
  4. package/cli-src/lib/services/invitation_service.ts +23 -0
  5. package/cli-src/lib/services/scope_service.ts +28 -1
  6. package/dist/cli/generate.d.ts.map +1 -1
  7. package/dist/cli/generate.js +2 -0
  8. package/dist/components/layouts/create_firm/index.d.ts +7 -1
  9. package/dist/components/layouts/create_firm/index.d.ts.map +1 -1
  10. package/dist/components/layouts/create_firm/index.js +2 -2
  11. package/dist/components/layouts/scope_management/components/branding_editor.d.ts +6 -0
  12. package/dist/components/layouts/scope_management/components/branding_editor.d.ts.map +1 -1
  13. package/dist/components/layouts/scope_management/components/branding_editor.js +41 -2
  14. package/dist/components/layouts/shared/firm_branding_to_brand_identity.d.ts +17 -0
  15. package/dist/components/layouts/shared/firm_branding_to_brand_identity.d.ts.map +1 -0
  16. package/dist/components/layouts/shared/firm_branding_to_brand_identity.js +52 -0
  17. package/dist/components/layouts/shared/hooks/use_firm_branding.d.ts +6 -0
  18. package/dist/components/layouts/shared/hooks/use_firm_branding.d.ts.map +1 -1
  19. package/dist/components/ui/button.d.ts +1 -1
  20. package/dist/components/ui/input.js +1 -1
  21. package/dist/lib/services/branding_service.d.ts +6 -0
  22. package/dist/lib/services/branding_service.d.ts.map +1 -1
  23. package/dist/lib/services/branding_service.js +60 -0
  24. package/dist/lib/services/invitation_service.d.ts +3 -0
  25. package/dist/lib/services/invitation_service.d.ts.map +1 -1
  26. package/dist/lib/services/invitation_service.js +18 -0
  27. package/dist/lib/services/scope_service.d.ts +21 -0
  28. package/dist/lib/services/scope_service.d.ts.map +1 -1
  29. package/dist/lib/services/scope_service.js +27 -1
  30. package/dist/page_components/create_firm.d.ts.map +1 -1
  31. package/dist/page_components/create_firm.js +4 -3
  32. package/dist/server_pages/index.d.ts +2 -0
  33. package/dist/server_pages/index.d.ts.map +1 -1
  34. package/dist/server_pages/index.js +1 -0
  35. package/package.json +10 -5
package/README.md CHANGED
@@ -2,6 +2,13 @@
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 v10.8.0
6
+
7
+ - **Firm branding gained `app_title` + `footer`.** `FirmBranding` (from `use_firm_branding`) now carries `app_title?: string` and `footer?: { public?: FooterConfig; app?: FooterConfig }` alongside the existing logo/color fields — migration `022_add_app_title_and_footer_to_hazo_scopes.sql` adds the backing `app_title`/`footer_config` columns to `hazo_scopes`. The scope-management branding editor exposes both as new form inputs.
8
+ - **`firm_branding_to_brand_identity()`** (`hazo_auth/src/components/layouts/shared/firm_branding_to_brand_identity.ts`) — maps a `FirmBranding` record (hex colors) into a `hazo_theme` `BrandIdentity` (HSL channel strings), for consumers rendering hazo_ui's `Logo`/`AppHeader`/`AppFooter` chrome components from stored firm branding.
9
+ - Server-side `validate_branding` now rejects footer link/social hrefs that resolve to an unsafe scheme (`javascript:`, `data:`, etc.) before persisting, as defense-in-depth alongside hazo_ui's client-side `safeHref()`.
10
+ - `hazo_theme` optional peer dependency range bumped to `^0.2.0`.
11
+
5
12
  ### What's New in v10.4.13
6
13
 
7
14
  - **`validate_session_secret_config()`** exported from `hazo_auth/server-lib` — hard startup validator that throws `HazoConfigError` (code `HAZO_AUTH_CONFIG`) when `JWT_SECRET` is unset or shorter than 32 chars. Call it from a Next.js `instrumentation.ts` `register()` hook to fail boot loudly instead of hitting the silent split-brain a bad/missing secret otherwise causes (edge proxy bounces to `/login` while server-side auth falls back to unsigned cookies and still considers the user authenticated).
@@ -64,6 +64,7 @@ const ROUTES: RouteDefinition[] = [
64
64
  { name: "nextauth_post", path: "api/auth/[...nextauth]", method: "POST", export_name: "nextauthPOST" },
65
65
  { name: "oauth_google_callback", path: "api/hazo_auth/oauth/google/callback", method: "GET", export_name: "oauthGoogleCallbackGET" },
66
66
  { name: "set_password", path: "api/hazo_auth/set_password", method: "POST", export_name: "setPasswordPOST" },
67
+ { name: "create_firm", path: "api/hazo_auth/create_firm", method: "POST", export_name: "createFirmPOST" },
67
68
  // Static assets (default auth-page images served from the package's dist/assets/images).
68
69
  // Without this route the default image_src (/api/hazo_auth/assets/login_default.jpg) 404s
69
70
  // and login/register/etc. render with an empty image panel.
@@ -77,6 +78,7 @@ const PAGES: PageDefinition[] = [
77
78
  { name: "reset_password", path: "hazo_auth/reset_password", component_name: "ResetPasswordPage", import_path: "hazo_auth/pages/reset_password" },
78
79
  { name: "verify_email", path: "hazo_auth/verify_email", component_name: "VerifyEmailPage", import_path: "hazo_auth/pages/verify_email" },
79
80
  { name: "my_settings", path: "hazo_auth/my_settings", component_name: "MySettingsPage", import_path: "hazo_auth/pages/my_settings" },
81
+ { name: "create_firm", path: "hazo_auth/create_firm", component_name: "CreateFirmPage", import_path: "hazo_auth/pages/create_firm" },
80
82
  ];
81
83
  // Note: Using barrel export "hazo_auth/pages" which maps to dist/server_pages/index.js in package.json exports
82
84
 
@@ -1,6 +1,7 @@
1
1
  // file_description: service for firm branding operations with inheritance support
2
2
  // section: imports
3
3
  import type { HazoConnectAdapter } from "hazo_connect";
4
+ import type { FooterConfig } from "hazo_theme";
4
5
  import {
5
6
  type FirmBranding,
6
7
  type ScopeRecord,
@@ -28,6 +29,8 @@ export type UpdateBrandingData = {
28
29
  primary_color?: string | null;
29
30
  secondary_color?: string | null;
30
31
  tagline?: string | null;
32
+ app_title?: string | null;
33
+ footer?: { public?: FooterConfig; app?: FooterConfig } | null;
31
34
  };
32
35
 
33
36
  // section: validation
@@ -39,6 +42,47 @@ function is_valid_hex_color(color: string): boolean {
39
42
  return /^#[0-9A-Fa-f]{6}$/.test(color);
40
43
  }
41
44
 
45
+ /**
46
+ * Validates an admin-supplied href against a scheme allowlist. Blocks
47
+ * `javascript:`/`data:`/`vbscript:` URIs that would become stored XSS when the
48
+ * value is later rendered into an `<a href>` (defense-in-depth alongside the
49
+ * render-time `safeHref` filter in hazo_ui's chrome components). Allows
50
+ * http(s), mailto, and same-origin absolute paths (`/…` but not `//…`).
51
+ */
52
+ function is_safe_href(url: string): boolean {
53
+ const t = url.trim().toLowerCase();
54
+ if (t.startsWith("http://") || t.startsWith("https://") || t.startsWith("mailto:")) {
55
+ return true;
56
+ }
57
+ return t.startsWith("/") && !t.startsWith("//");
58
+ }
59
+
60
+ /**
61
+ * Walks every admin-configurable href in a footer config (column links + social
62
+ * links across both public/app variants) and returns the first unsafe one, or
63
+ * null when all are safe.
64
+ */
65
+ function first_unsafe_footer_href(
66
+ footer: UpdateBrandingData["footer"],
67
+ ): string | null {
68
+ if (!footer) return null;
69
+ for (const variant of ["public", "app"] as const) {
70
+ const cfg: FooterConfig | undefined = footer[variant];
71
+ if (!cfg) continue;
72
+ for (const column of cfg.columns ?? []) {
73
+ for (const link of column.links ?? []) {
74
+ if (link?.href && !is_safe_href(link.href)) return link.href;
75
+ }
76
+ }
77
+ if (cfg.social) {
78
+ for (const handle of Object.values(cfg.social)) {
79
+ if (typeof handle === "string" && handle && !is_safe_href(handle)) return handle;
80
+ }
81
+ }
82
+ }
83
+ return null;
84
+ }
85
+
42
86
  /**
43
87
  * Validates branding data
44
88
  */
@@ -66,6 +110,20 @@ export function validate_branding(data: UpdateBrandingData): {
66
110
  return { valid: false, error: "Logo URL must be 500 characters or less" };
67
111
  }
68
112
 
113
+ if (data.app_title && typeof data.app_title === "string" && data.app_title.length > 200) {
114
+ return { valid: false, error: "App title must be 200 characters or less" };
115
+ }
116
+
117
+ // Reject unsafe footer link/social hrefs (javascript:/data: etc.) before they
118
+ // are persisted to footer_config and later rendered into <a href>.
119
+ const bad_href = first_unsafe_footer_href(data.footer);
120
+ if (bad_href) {
121
+ return {
122
+ valid: false,
123
+ error: "Footer links must use http(s), mailto, or same-origin (/…) URLs",
124
+ };
125
+ }
126
+
69
127
  return { valid: true };
70
128
  }
71
129
 
@@ -240,6 +298,8 @@ export async function update_branding(
240
298
  const updated_primary = data.primary_color !== undefined ? data.primary_color : current.primary_color;
241
299
  const updated_secondary = data.secondary_color !== undefined ? data.secondary_color : current.secondary_color;
242
300
  const updated_tagline = data.tagline !== undefined ? data.tagline : current.tagline;
301
+ const updated_app_title = data.app_title !== undefined ? data.app_title : current.app_title;
302
+ const updated_footer = data.footer !== undefined ? data.footer : current.footer;
243
303
 
244
304
  // Update the scope with individual fields
245
305
  const update_result = await update_scope(adapter, scope_id, {
@@ -247,6 +307,8 @@ export async function update_branding(
247
307
  primary_color: updated_primary || null,
248
308
  secondary_color: updated_secondary || null,
249
309
  tagline: updated_tagline || null,
310
+ app_title: updated_app_title || null,
311
+ footer: updated_footer || null,
250
312
  });
251
313
 
252
314
  if (!update_result.success) {
@@ -317,6 +379,8 @@ export async function replace_branding(
317
379
  primary_color: branding?.primary_color || null,
318
380
  secondary_color: branding?.secondary_color || null,
319
381
  tagline: branding?.tagline || null,
382
+ app_title: branding?.app_title || null,
383
+ footer: branding?.footer || null,
320
384
  });
321
385
 
322
386
  if (!update_result.success) {
@@ -2,6 +2,7 @@
2
2
  // section: imports
3
3
  import type { HazoConnectAdapter } from "hazo_connect";
4
4
  import { createCrudService } from "hazo_connect/server";
5
+ import { randomBytes } from "crypto";
5
6
  import { create_app_logger } from "../app_logger.js";
6
7
  import { sanitize_error_for_user } from "../utils/error_sanitizer.js";
7
8
  import { assign_user_scope } from "./user_scope_service.js";
@@ -14,7 +15,10 @@ export type InvitationStatus = "PENDING" | "ACCEPTED" | "EXPIRED" | "REVOKED";
14
15
  export type InvitationRecord = {
15
16
  id: string;
16
17
  email_address: string;
18
+ /** Opaque unique token identifying this invitation (e.g. for an accept link). */
19
+ token: string;
17
20
  scope_id: string;
21
+ root_scope_id: string;
18
22
  role_id: string;
19
23
  status: InvitationStatus;
20
24
  invited_by: string | null;
@@ -48,6 +52,15 @@ const MAX_PENDING_PER_EMAIL = 5;
48
52
 
49
53
  // section: helpers
50
54
 
55
+ /**
56
+ * Generates an opaque, unique invitation token.
57
+ * `hazo_invitations.token` is `NOT NULL UNIQUE` in the schema (migration 011 /
58
+ * db_setup_sqlite.sql / db_setup_postgres.sql) — every insert must supply one.
59
+ */
60
+ function generate_invitation_token(): string {
61
+ return randomBytes(32).toString("hex");
62
+ }
63
+
51
64
  /**
52
65
  * Creates a new invitation
53
66
  */
@@ -85,9 +98,19 @@ export async function create_invitation(
85
98
  const expires_hours = data.expires_in_hours || DEFAULT_EXPIRY_HOURS;
86
99
  const expires_at = new Date(now.getTime() + expires_hours * 60 * 60 * 1000);
87
100
 
101
+ // `hazo_invitations.root_scope_id` is NOT NULL — same computation
102
+ // `assign_user_scope` (user_scope_service.ts) uses for the analogous
103
+ // `hazo_user_scopes.root_scope_id` column: resolve the scope's root
104
+ // ancestor, falling back to the scope itself if it IS the root (or the
105
+ // computation fails).
106
+ const computed_root_scope_id = await get_root_scope_id(adapter, data.scope_id);
107
+ const root_scope_id = computed_root_scope_id || data.scope_id;
108
+
88
109
  const inserted = await invitation_service.insert({
89
110
  email_address: data.email_address.toLowerCase(),
111
+ token: generate_invitation_token(),
90
112
  scope_id: data.scope_id,
113
+ root_scope_id,
91
114
  role_id: data.role_id,
92
115
  status: "PENDING",
93
116
  invited_by: data.invited_by || null,
@@ -2,6 +2,7 @@
2
2
  // section: imports
3
3
  import type { HazoConnectAdapter } from "hazo_connect";
4
4
  import { createCrudService } from "hazo_connect/server";
5
+ import type { FooterConfig } from "hazo_theme";
5
6
  import { create_app_logger } from "../app_logger.js";
6
7
  import { sanitize_error_for_user } from "../utils/error_sanitizer.js";
7
8
 
@@ -24,6 +25,8 @@ export type FirmBranding = {
24
25
  primary_color?: string | null;
25
26
  secondary_color?: string | null;
26
27
  tagline?: string | null;
28
+ app_title?: string | null;
29
+ footer?: { public?: FooterConfig; app?: FooterConfig } | null;
27
30
  };
28
31
 
29
32
  export type ScopeRecord = {
@@ -36,6 +39,8 @@ export type ScopeRecord = {
36
39
  primary_color: string | null;
37
40
  secondary_color: string | null;
38
41
  tagline: string | null;
42
+ app_title: string | null;
43
+ footer: { public?: FooterConfig; app?: FooterConfig } | null;
39
44
  created_at: string;
40
45
  changed_at: string;
41
46
  };
@@ -56,6 +61,8 @@ export type CreateScopeData = {
56
61
  primary_color?: string | null;
57
62
  secondary_color?: string | null;
58
63
  tagline?: string | null;
64
+ app_title?: string | null;
65
+ footer?: { public?: FooterConfig; app?: FooterConfig } | null;
59
66
  };
60
67
 
61
68
  export type UpdateScopeData = {
@@ -67,6 +74,8 @@ export type UpdateScopeData = {
67
74
  primary_color?: string | null;
68
75
  secondary_color?: string | null;
69
76
  tagline?: string | null;
77
+ app_title?: string | null;
78
+ footer?: { public?: FooterConfig; app?: FooterConfig } | null;
70
79
  };
71
80
 
72
81
  export type ScopeTreeNode = ScopeRecord & {
@@ -88,6 +97,12 @@ function normalize_scope_record(raw: Record<string, unknown>): ScopeRecord {
88
97
  primary_color: (raw.primary_color as string | null) || null,
89
98
  secondary_color: (raw.secondary_color as string | null) || null,
90
99
  tagline: (raw.tagline as string | null) || null,
100
+ app_title: (raw.app_title as string | null) || null,
101
+ footer: (() => {
102
+ const raw_fc = raw.footer_config;
103
+ if (typeof raw_fc !== "string" || raw_fc.length === 0) return null;
104
+ try { return JSON.parse(raw_fc); } catch { return null; }
105
+ })(),
91
106
  created_at: raw.created_at as string,
92
107
  changed_at: raw.changed_at as string,
93
108
  };
@@ -103,6 +118,8 @@ export function extract_branding(scope: ScopeRecord): FirmBranding | null {
103
118
  if (scope.primary_color) branding.primary_color = scope.primary_color;
104
119
  if (scope.secondary_color) branding.secondary_color = scope.secondary_color;
105
120
  if (scope.tagline) branding.tagline = scope.tagline;
121
+ if (scope.app_title) branding.app_title = scope.app_title;
122
+ if (scope.footer) branding.footer = scope.footer;
106
123
  return Object.keys(branding).length > 0 ? branding : null;
107
124
  }
108
125
 
@@ -110,7 +127,7 @@ export function extract_branding(scope: ScopeRecord): FirmBranding | null {
110
127
  * Checks if a scope has any branding set
111
128
  */
112
129
  export function has_branding(scope: ScopeRecord): boolean {
113
- return !!(scope.logo_url || scope.primary_color || scope.secondary_color || scope.tagline);
130
+ return !!(scope.logo_url || scope.primary_color || scope.secondary_color || scope.tagline || scope.app_title || scope.footer);
114
131
  }
115
132
 
116
133
  /**
@@ -303,6 +320,8 @@ export async function create_scope(
303
320
  primary_color: data.primary_color || null,
304
321
  secondary_color: data.secondary_color || null,
305
322
  tagline: data.tagline || null,
323
+ app_title: data.app_title || null,
324
+ footer_config: data.footer ? JSON.stringify(data.footer) : null,
306
325
  created_at: now,
307
326
  changed_at: now,
308
327
  };
@@ -427,6 +446,12 @@ export async function update_scope(
427
446
  if (data.tagline !== undefined) {
428
447
  update_data.tagline = data.tagline;
429
448
  }
449
+ if (data.app_title !== undefined) {
450
+ update_data.app_title = data.app_title;
451
+ }
452
+ if (data.footer !== undefined) {
453
+ update_data.footer_config = data.footer ? JSON.stringify(data.footer) : null;
454
+ }
430
455
 
431
456
  const updated = await scope_service.updateById(scope_id, update_data);
432
457
 
@@ -794,6 +819,8 @@ export async function ensure_default_system_scope(
794
819
  primary_color: null,
795
820
  secondary_color: null,
796
821
  tagline: null,
822
+ app_title: null,
823
+ footer_config: null,
797
824
  created_at: now,
798
825
  changed_at: now,
799
826
  });
@@ -1 +1 @@
1
- {"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["../../src/cli/generate.ts"],"names":[],"mappings":"AAqBA,MAAM,MAAM,eAAe,GAAG;IAC5B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,GAAG,CAAC,EAAE,OAAO,CAAC;CACf,CAAC;AAqMF,wBAAgB,eAAe,CAAC,OAAO,GAAE,eAAoB,GAAG,IAAI,CA8DnE"}
1
+ {"version":3,"file":"generate.d.ts","sourceRoot":"","sources":["../../src/cli/generate.ts"],"names":[],"mappings":"AAqBA,MAAM,MAAM,eAAe,GAAG;IAC5B,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,GAAG,CAAC,EAAE,OAAO,CAAC;CACf,CAAC;AAuMF,wBAAgB,eAAe,CAAC,OAAO,GAAE,eAAoB,GAAG,IAAI,CA8DnE"}
@@ -41,6 +41,7 @@ const ROUTES = [
41
41
  { name: "nextauth_post", path: "api/auth/[...nextauth]", method: "POST", export_name: "nextauthPOST" },
42
42
  { name: "oauth_google_callback", path: "api/hazo_auth/oauth/google/callback", method: "GET", export_name: "oauthGoogleCallbackGET" },
43
43
  { name: "set_password", path: "api/hazo_auth/set_password", method: "POST", export_name: "setPasswordPOST" },
44
+ { name: "create_firm", path: "api/hazo_auth/create_firm", method: "POST", export_name: "createFirmPOST" },
44
45
  // Static assets (default auth-page images served from the package's dist/assets/images).
45
46
  // Without this route the default image_src (/api/hazo_auth/assets/login_default.jpg) 404s
46
47
  // and login/register/etc. render with an empty image panel.
@@ -53,6 +54,7 @@ const PAGES = [
53
54
  { name: "reset_password", path: "hazo_auth/reset_password", component_name: "ResetPasswordPage", import_path: "hazo_auth/pages/reset_password" },
54
55
  { name: "verify_email", path: "hazo_auth/verify_email", component_name: "VerifyEmailPage", import_path: "hazo_auth/pages/verify_email" },
55
56
  { name: "my_settings", path: "hazo_auth/my_settings", component_name: "MySettingsPage", import_path: "hazo_auth/pages/my_settings" },
57
+ { name: "create_firm", path: "hazo_auth/create_firm", component_name: "CreateFirmPage", import_path: "hazo_auth/pages/create_firm" },
56
58
  ];
57
59
  // Note: Using barrel export "hazo_auth/pages" which maps to dist/server_pages/index.js in package.json exports
58
60
  // section: helpers
@@ -34,6 +34,12 @@ export type CreateFirmLayoutProps = {
34
34
  org_structure_placeholder?: string;
35
35
  /** Default value for org structure */
36
36
  default_org_structure?: string;
37
+ /**
38
+ * Show the org structure field. When false, the field is hidden from the
39
+ * form but {@link default_org_structure} is still submitted (the API
40
+ * requires org_structure to be non-empty).
41
+ */
42
+ show_org_structure?: boolean;
37
43
  /** Label for submit button */
38
44
  submit_button_label?: string;
39
45
  /** Success message shown after firm creation */
@@ -52,6 +58,6 @@ export type CreateFirmLayoutProps = {
52
58
  error: (message: string, data?: Record<string, unknown>) => void;
53
59
  };
54
60
  };
55
- export default function CreateFirmLayout({ image_src, keyword, keyword_lower, verb, image_alt, image_background_color, banner_body, heading, sub_heading, firm_name_label, firm_name_placeholder, org_structure_label, org_structure_placeholder, default_org_structure, submit_button_label, success_message, redirect_route, apiBasePath, onSuccess, button_colors, logger, }: CreateFirmLayoutProps): React.JSX.Element;
61
+ export default function CreateFirmLayout({ image_src, keyword, keyword_lower, verb, image_alt, image_background_color, banner_body, heading, sub_heading, firm_name_label, firm_name_placeholder, org_structure_label, org_structure_placeholder, default_org_structure, show_org_structure, submit_button_label, success_message, redirect_route, apiBasePath, onSuccess, button_colors, logger, }: CreateFirmLayoutProps): React.JSX.Element;
56
62
  export { CreateFirmLayout };
57
63
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/layouts/create_firm/index.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAOlD,OAAO,EACL,KAAK,sBAAsB,EAC5B,MAAM,uCAAuC,CAAC;AAQ/C,MAAM,MAAM,qBAAqB,GAAG;IAClC,sCAAsC;IACtC,SAAS,EAAE,MAAM,GAAG,eAAe,CAAC;IACpC;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yEAAyE;IACzE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,wEAAwE;IACxE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,6BAA6B;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2CAA2C;IAC3C,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,iDAAiD;IACjD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mBAAmB;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,uBAAuB;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gCAAgC;IAChC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,sCAAsC;IACtC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,oCAAoC;IACpC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,0CAA0C;IAC1C,yBAAyB,CAAC,EAAE,MAAM,CAAC;IACnC,sCAAsC;IACtC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,8BAA8B;IAC9B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,gDAAgD;IAChD,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,sCAAsC;IACtC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,4CAA4C;IAC5C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iDAAiD;IACjD,SAAS,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,6BAA6B;IAC7B,aAAa,CAAC,EAAE,sBAAsB,CAAC;IACvC,2BAA2B;IAC3B,MAAM,CAAC,EAAE;QACP,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;QAChE,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;KAClE,CAAC;CACH,CAAC;AAGF,MAAM,CAAC,OAAO,UAAU,gBAAgB,CAAC,EACvC,SAAS,EACT,OAAgB,EAChB,aAAsB,EACtB,IAAe,EACf,SAA2C,EAC3C,sBAAkC,EAClC,WAAyK,EACzK,OAAmC,EACnC,WAAuD,EACvD,eAAmC,EACnC,qBAA0D,EAC1D,mBAA8C,EAC9C,yBAA6D,EAC7D,qBAAsC,EACtC,mBAA0C,EAC1C,eAAwE,EACxE,cAAoB,EACpB,WAAW,EACX,SAAS,EACT,aAAa,EACb,MAAM,GACP,EAAE,qBAAqB,qBAuJvB;AAGD,OAAO,EAAE,gBAAgB,EAAE,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/layouts/create_firm/index.tsx"],"names":[],"mappings":"AAKA,OAAO,KAAK,MAAM,OAAO,CAAC;AAC1B,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAOlD,OAAO,EACL,KAAK,sBAAsB,EAC5B,MAAM,uCAAuC,CAAC;AAQ/C,MAAM,MAAM,qBAAqB,GAAG;IAClC,sCAAsC;IACtC,SAAS,EAAE,MAAM,GAAG,eAAe,CAAC;IACpC;;;;OAIG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,yEAAyE;IACzE,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,wEAAwE;IACxE,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,6BAA6B;IAC7B,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,2CAA2C;IAC3C,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,iDAAiD;IACjD,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,mBAAmB;IACnB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,uBAAuB;IACvB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,gCAAgC;IAChC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,sCAAsC;IACtC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,oCAAoC;IACpC,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,0CAA0C;IAC1C,yBAAyB,CAAC,EAAE,MAAM,CAAC;IACnC,sCAAsC;IACtC,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B;;;;OAIG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;IAC7B,8BAA8B;IAC9B,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,gDAAgD;IAChD,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,sCAAsC;IACtC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,4CAA4C;IAC5C,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,iDAAiD;IACjD,SAAS,CAAC,EAAE,CAAC,QAAQ,EAAE,MAAM,KAAK,IAAI,CAAC;IACvC,6BAA6B;IAC7B,aAAa,CAAC,EAAE,sBAAsB,CAAC;IACvC,2BAA2B;IAC3B,MAAM,CAAC,EAAE;QACP,IAAI,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;QAChE,KAAK,EAAE,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,KAAK,IAAI,CAAC;KAClE,CAAC;CACH,CAAC;AAGF,MAAM,CAAC,OAAO,UAAU,gBAAgB,CAAC,EACvC,SAAS,EACT,OAAgB,EAChB,aAAsB,EACtB,IAAe,EACf,SAA2C,EAC3C,sBAAkC,EAClC,WAAyK,EACzK,OAAmC,EACnC,WAAuD,EACvD,eAAmC,EACnC,qBAA0D,EAC1D,mBAA8C,EAC9C,yBAA6D,EAC7D,qBAAsC,EACtC,kBAAyB,EACzB,mBAA0C,EAC1C,eAAwE,EACxE,cAAoB,EACpB,WAAW,EACX,SAAS,EACT,aAAa,EACb,MAAM,GACP,EAAE,qBAAqB,qBAyJvB;AAGD,OAAO,EAAE,gBAAgB,EAAE,CAAC"}
@@ -11,7 +11,7 @@ import { CheckCircle, Building2 } from "lucide-react";
11
11
  import { use_create_firm_form, } from "./hooks/use_create_firm_form.js";
12
12
  import { useHazoAuthConfig } from "../../../contexts/hazo_auth_provider.js";
13
13
  // section: component
14
- export default function CreateFirmLayout({ image_src, keyword = "Firm", keyword_lower = "firm", verb = "Create", image_alt = `${verb} your ${keyword_lower}`, image_background_color = "#f1f5f9", banner_body = `${verb} your ${keyword_lower} to start using the application. You'll become the administrator of your ${keyword_lower} and can invite team members later.`, heading = `${verb} Your ${keyword}`, sub_heading = "Set up your organisation to get started", firm_name_label = `${keyword} Name`, firm_name_placeholder = `Enter your ${keyword_lower} name`, org_structure_label = "Organisation Structure", org_structure_placeholder = "e.g., Headquarters, Head Office", default_org_structure = "Headquarters", submit_button_label = `${verb} ${keyword}`, success_message = `Your ${keyword_lower} has been created successfully!`, redirect_route = "/", apiBasePath, onSuccess, button_colors, logger, }) {
14
+ export default function CreateFirmLayout({ image_src, keyword = "Firm", keyword_lower = "firm", verb = "Create", image_alt = `${verb} your ${keyword_lower}`, image_background_color = "#f1f5f9", banner_body = `${verb} your ${keyword_lower} to start using the application. You'll become the administrator of your ${keyword_lower} and can invite team members later.`, heading = `${verb} Your ${keyword}`, sub_heading = "Set up your organisation to get started", firm_name_label = `${keyword} Name`, firm_name_placeholder = `Enter your ${keyword_lower} name`, org_structure_label = "Organisation Structure", org_structure_placeholder = "e.g., Headquarters, Head Office", default_org_structure = "Headquarters", show_org_structure = true, submit_button_label = `${verb} ${keyword}`, success_message = `Your ${keyword_lower} has been created successfully!`, redirect_route = "/", apiBasePath, onSuccess, button_colors, logger, }) {
15
15
  const { apiBasePath: contextApiBasePath } = useHazoAuthConfig();
16
16
  const resolvedApiBasePath = apiBasePath || contextApiBasePath;
17
17
  const form = use_create_firm_form({
@@ -33,7 +33,7 @@ export default function CreateFirmLayout({ image_src, keyword = "Firm", keyword_
33
33
  const value = e.target.value;
34
34
  formState.handleFieldChange(field, value);
35
35
  };
36
- return (_jsxs(_Fragment, { children: [_jsx(FormFieldWrapper, { fieldId: "firm_name", label: firm_name_label, input: _jsx(Input, { id: "firm_name", ref: formState.firmNameRef, type: "text", value: formState.values.firm_name, onChange: (e) => handleInput("firm_name", e), onInput: (e) => handleInput("firm_name", e), placeholder: firm_name_placeholder, "aria-label": firm_name_label, className: "cls_create_firm_layout_field_input hazo-autofill-detect", autoComplete: "organization" }), errorMessage: formState.errors.firm_name }), _jsx(FormFieldWrapper, { fieldId: "org_structure", label: org_structure_label, input: _jsx(Input, { id: "org_structure", ref: formState.orgStructureRef, type: "text", value: formState.values.org_structure, onChange: (e) => handleInput("org_structure", e), onInput: (e) => handleInput("org_structure", e), placeholder: org_structure_placeholder, "aria-label": org_structure_label, className: "cls_create_firm_layout_field_input hazo-autofill-detect" }), errorMessage: formState.errors.org_structure })] }));
36
+ return (_jsxs(_Fragment, { children: [_jsx(FormFieldWrapper, { fieldId: "firm_name", label: firm_name_label, input: _jsx(Input, { id: "firm_name", ref: formState.firmNameRef, type: "text", value: formState.values.firm_name, onChange: (e) => handleInput("firm_name", e), onInput: (e) => handleInput("firm_name", e), placeholder: firm_name_placeholder, "aria-label": firm_name_label, className: "cls_create_firm_layout_field_input hazo-autofill-detect", autoComplete: "organization" }), errorMessage: formState.errors.firm_name }), show_org_structure && (_jsx(FormFieldWrapper, { fieldId: "org_structure", label: org_structure_label, input: _jsx(Input, { id: "org_structure", ref: formState.orgStructureRef, type: "text", value: formState.values.org_structure, onChange: (e) => handleInput("org_structure", e), onInput: (e) => handleInput("org_structure", e), placeholder: org_structure_placeholder, "aria-label": org_structure_label, className: "cls_create_firm_layout_field_input hazo-autofill-detect" }), errorMessage: formState.errors.org_structure }))] }));
37
37
  };
38
38
  // Show success message after firm creation
39
39
  if (form.isSuccess) {
@@ -1,8 +1,14 @@
1
+ import type { FooterConfig } from "hazo_theme";
1
2
  export type FirmBranding = {
2
3
  logo_url?: string;
3
4
  primary_color?: string;
4
5
  secondary_color?: string;
5
6
  tagline?: string;
7
+ app_title?: string;
8
+ footer?: {
9
+ public?: FooterConfig;
10
+ app?: FooterConfig;
11
+ };
6
12
  };
7
13
  export type BrandingEditorProps = {
8
14
  scopeId: string;
@@ -1 +1 @@
1
- {"version":3,"file":"branding_editor.d.ts","sourceRoot":"","sources":["../../../../../src/components/layouts/scope_management/components/branding_editor.tsx"],"names":[],"mappings":"AA8BA,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,YAAY,GAAG,IAAI,KAAK,IAAI,CAAC;IACjD,eAAe,CAAC,EAAE,YAAY,GAAG,IAAI,CAAC;CACvC,CAAC;AAQF,wBAAgB,cAAc,CAAC,EAC7B,OAAO,EACP,SAAS,EACT,MAAM,EACN,OAAO,EACP,MAAM,EACN,eAAe,GAChB,EAAE,mBAAmB,+BA2ZrB"}
1
+ {"version":3,"file":"branding_editor.d.ts","sourceRoot":"","sources":["../../../../../src/components/layouts/scope_management/components/branding_editor.tsx"],"names":[],"mappings":"AA6BA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAG/C,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,YAAY,CAAC;QAAC,GAAG,CAAC,EAAE,YAAY,CAAA;KAAE,CAAC;CACxD,CAAC;AAQF,MAAM,MAAM,mBAAmB,GAAG;IAChC,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,OAAO,CAAC;IAChB,OAAO,EAAE,MAAM,IAAI,CAAC;IACpB,MAAM,CAAC,EAAE,CAAC,QAAQ,EAAE,YAAY,GAAG,IAAI,KAAK,IAAI,CAAC;IACjD,eAAe,CAAC,EAAE,YAAY,GAAG,IAAI,CAAC;CACvC,CAAC;AAQF,wBAAgB,cAAc,CAAC,EAC7B,OAAO,EACP,SAAS,EACT,MAAM,EACN,OAAO,EACP,MAAM,EACN,eAAe,GAChB,EAAE,mBAAmB,+BA2jBrB"}
@@ -8,7 +8,7 @@ import { Button } from "../../../ui/button.js";
8
8
  import { Input } from "../../../ui/input.js";
9
9
  import { Label } from "../../../ui/label.js";
10
10
  import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "../../../ui/dialog.js";
11
- import { Loader2, Upload, Trash2, CircleCheck, CircleX, Image as ImageIcon, Palette, } from "lucide-react";
11
+ import { Loader2, Upload, Trash2, CircleCheck, CircleX, Image as ImageIcon, Palette, Plus, } from "lucide-react";
12
12
  import { toast } from "sonner";
13
13
  import { useHazoAuthConfig } from "../../../../contexts/hazo_auth_provider.js";
14
14
  // section: helpers
@@ -17,6 +17,7 @@ function isValidHexColor(color) {
17
17
  }
18
18
  // section: component
19
19
  export function BrandingEditor({ scopeId, scopeName, isOpen, onClose, onSave, initialBranding, }) {
20
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
20
21
  const { apiBasePath } = useHazoAuthConfig();
21
22
  // State
22
23
  const [loading, setLoading] = useState(false);
@@ -27,6 +28,14 @@ export function BrandingEditor({ scopeId, scopeName, isOpen, onClose, onSave, in
27
28
  const [primaryColor, setPrimaryColor] = useState((initialBranding === null || initialBranding === void 0 ? void 0 : initialBranding.primary_color) || "");
28
29
  const [secondaryColor, setSecondaryColor] = useState((initialBranding === null || initialBranding === void 0 ? void 0 : initialBranding.secondary_color) || "");
29
30
  const [tagline, setTagline] = useState((initialBranding === null || initialBranding === void 0 ? void 0 : initialBranding.tagline) || "");
31
+ const [appTitle, setAppTitle] = useState((initialBranding === null || initialBranding === void 0 ? void 0 : initialBranding.app_title) || "");
32
+ // Footer state
33
+ const [publicCopyright, setPublicCopyright] = useState(((_b = (_a = initialBranding === null || initialBranding === void 0 ? void 0 : initialBranding.footer) === null || _a === void 0 ? void 0 : _a.public) === null || _b === void 0 ? void 0 : _b.copyright) || "");
34
+ const [publicLinks, setPublicLinks] = useState(((_g = (_f = (_e = (_d = (_c = initialBranding === null || initialBranding === void 0 ? void 0 : initialBranding.footer) === null || _c === void 0 ? void 0 : _c.public) === null || _d === void 0 ? void 0 : _d.columns) === null || _e === void 0 ? void 0 : _e[0]) === null || _f === void 0 ? void 0 : _f.links) === null || _g === void 0 ? void 0 : _g.map((l) => ({
35
+ label: l.label,
36
+ href: l.href,
37
+ }))) || []);
38
+ const [appCopyright, setAppCopyright] = useState(((_j = (_h = initialBranding === null || initialBranding === void 0 ? void 0 : initialBranding.footer) === null || _h === void 0 ? void 0 : _h.app) === null || _j === void 0 ? void 0 : _j.copyright) || "");
30
39
  // File input ref
31
40
  const fileInputRef = useRef(null);
32
41
  // Load current branding when dialog opens
@@ -37,6 +46,7 @@ export function BrandingEditor({ scopeId, scopeName, isOpen, onClose, onSave, in
37
46
  }, [isOpen, scopeId]);
38
47
  // Load branding from API
39
48
  const loadBranding = useCallback(async () => {
49
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j;
40
50
  setLoading(true);
41
51
  try {
42
52
  const params = new URLSearchParams({
@@ -51,6 +61,10 @@ export function BrandingEditor({ scopeId, scopeName, isOpen, onClose, onSave, in
51
61
  setPrimaryColor(branding.primary_color || "");
52
62
  setSecondaryColor(branding.secondary_color || "");
53
63
  setTagline(branding.tagline || "");
64
+ setAppTitle(branding.app_title || "");
65
+ setPublicCopyright(((_b = (_a = branding.footer) === null || _a === void 0 ? void 0 : _a.public) === null || _b === void 0 ? void 0 : _b.copyright) || "");
66
+ setPublicLinks(((_g = (_f = (_e = (_d = (_c = branding.footer) === null || _c === void 0 ? void 0 : _c.public) === null || _d === void 0 ? void 0 : _d.columns) === null || _e === void 0 ? void 0 : _e[0]) === null || _f === void 0 ? void 0 : _f.links) === null || _g === void 0 ? void 0 : _g.map((l) => ({ label: l.label, href: l.href }))) || []);
67
+ setAppCopyright(((_j = (_h = branding.footer) === null || _h === void 0 ? void 0 : _h.app) === null || _j === void 0 ? void 0 : _j.copyright) || "");
54
68
  }
55
69
  }
56
70
  catch (error) {
@@ -122,6 +136,17 @@ export function BrandingEditor({ scopeId, scopeName, isOpen, onClose, onSave, in
122
136
  branding.secondary_color = secondaryColor;
123
137
  if (tagline)
124
138
  branding.tagline = tagline;
139
+ if (appTitle)
140
+ branding.app_title = appTitle;
141
+ const cleanedLinks = publicLinks.filter((l) => l.label.trim() && l.href.trim());
142
+ const publicFooter = publicCopyright.trim() || cleanedLinks.length > 0
143
+ ? Object.assign(Object.assign({}, (publicCopyright.trim() ? { copyright: publicCopyright.trim() } : {})), (cleanedLinks.length > 0 ? { columns: [{ links: cleanedLinks }] } : {})) : undefined;
144
+ const appFooter = appCopyright.trim()
145
+ ? { copyright: appCopyright.trim() }
146
+ : undefined;
147
+ if (publicFooter || appFooter) {
148
+ branding.footer = Object.assign(Object.assign({}, (publicFooter ? { public: publicFooter } : {})), (appFooter ? { app: appFooter } : {}));
149
+ }
125
150
  const hasValues = Object.keys(branding).length > 0;
126
151
  const response = await fetch(`${apiBasePath}/scope_management/branding`, {
127
152
  method: "PUT",
@@ -162,6 +187,10 @@ export function BrandingEditor({ scopeId, scopeName, isOpen, onClose, onSave, in
162
187
  setPrimaryColor("");
163
188
  setSecondaryColor("");
164
189
  setTagline("");
190
+ setAppTitle("");
191
+ setPublicCopyright("");
192
+ setPublicLinks([]);
193
+ setAppCopyright("");
165
194
  toast.success("Branding cleared");
166
195
  onSave === null || onSave === void 0 ? void 0 : onSave(null);
167
196
  }
@@ -180,7 +209,17 @@ export function BrandingEditor({ scopeId, scopeName, isOpen, onClose, onSave, in
180
209
  const handleRemoveLogo = () => {
181
210
  setLogoUrl("");
182
211
  };
183
- return (_jsx(Dialog, { open: isOpen, onOpenChange: (open) => !open && onClose(), children: _jsxs(DialogContent, { className: "cls_branding_editor_dialog max-w-lg", children: [_jsxs(DialogHeader, { children: [_jsxs(DialogTitle, { className: "flex items-center gap-2", children: [_jsx(Palette, { className: "h-5 w-5" }), "Firm Branding"] }), _jsxs(DialogDescription, { children: ["Customize branding for \"", scopeName, "\""] })] }), loading ? (_jsx("div", { className: "flex items-center justify-center p-8", children: _jsx(Loader2, { className: "h-6 w-6 animate-spin text-slate-400" }) })) : (_jsxs("div", { className: "flex flex-col gap-6 py-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Logo" }), _jsxs("div", { className: "flex items-start gap-4", children: [_jsx("div", { className: "flex-shrink-0 w-24 h-24 border rounded-lg overflow-hidden bg-muted flex items-center justify-center", children: logoUrl ? (_jsx("img", { src: logoUrl, alt: "Firm logo", className: "w-full h-full object-contain" })) : (_jsx(ImageIcon, { className: "h-8 w-8 text-muted-foreground" })) }), _jsxs("div", { className: "flex flex-col gap-2 flex-1", children: [_jsx("input", { type: "file", ref: fileInputRef, onChange: handleFileChange, accept: "image/png,image/jpeg,image/svg+xml,image/webp", className: "hidden" }), _jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: handleUploadClick, disabled: uploading, children: uploading ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "h-4 w-4 mr-2 animate-spin" }), "Uploading..."] })) : (_jsxs(_Fragment, { children: [_jsx(Upload, { className: "h-4 w-4 mr-2" }), "Upload Logo"] })) }), logoUrl && (_jsxs(Button, { type: "button", variant: "ghost", size: "sm", onClick: handleRemoveLogo, className: "text-destructive hover:text-destructive", children: [_jsx(Trash2, { className: "h-4 w-4 mr-2" }), "Remove"] })), _jsx("p", { className: "text-xs text-muted-foreground", children: "PNG, JPG, SVG or WebP. Max 500KB." })] })] })] }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { htmlFor: "primary_color", children: "Primary Color" }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Input, { id: "primary_color", type: "text", value: primaryColor, onChange: (e) => setPrimaryColor(e.target.value), placeholder: "#1a73e8", className: "flex-1" }), _jsx("input", { type: "color", value: primaryColor || "#000000", onChange: (e) => setPrimaryColor(e.target.value), className: "w-10 h-10 rounded border cursor-pointer", title: "Pick color" })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { htmlFor: "secondary_color", children: "Secondary Color" }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Input, { id: "secondary_color", type: "text", value: secondaryColor, onChange: (e) => setSecondaryColor(e.target.value), placeholder: "#4285f4", className: "flex-1" }), _jsx("input", { type: "color", value: secondaryColor || "#000000", onChange: (e) => setSecondaryColor(e.target.value), className: "w-10 h-10 rounded border cursor-pointer", title: "Pick color" })] })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { htmlFor: "tagline", children: "Tagline" }), _jsx(Input, { id: "tagline", type: "text", value: tagline, onChange: (e) => setTagline(e.target.value), placeholder: "Your trusted partner", maxLength: 200 }), _jsx("p", { className: "text-xs text-muted-foreground", children: "A short company tagline (max 200 characters)" })] }), (logoUrl || primaryColor || tagline) && (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Preview" }), _jsx("div", { className: "p-4 rounded-lg border", style: {
212
+ // Footer link row helpers
213
+ const handleAddFooterLink = () => {
214
+ setPublicLinks((prev) => [...prev, { label: "", href: "" }]);
215
+ };
216
+ const handleRemoveFooterLink = (index) => {
217
+ setPublicLinks((prev) => prev.filter((_, i) => i !== index));
218
+ };
219
+ const handleFooterLinkChange = (index, field, value) => {
220
+ setPublicLinks((prev) => prev.map((link, i) => (i === index ? Object.assign(Object.assign({}, link), { [field]: value }) : link)));
221
+ };
222
+ return (_jsx(Dialog, { open: isOpen, onOpenChange: (open) => !open && onClose(), children: _jsxs(DialogContent, { className: "cls_branding_editor_dialog max-w-lg", children: [_jsxs(DialogHeader, { children: [_jsxs(DialogTitle, { className: "flex items-center gap-2", children: [_jsx(Palette, { className: "h-5 w-5" }), "Firm Branding"] }), _jsxs(DialogDescription, { children: ["Customize branding for \"", scopeName, "\""] })] }), loading ? (_jsx("div", { className: "flex items-center justify-center p-8", children: _jsx(Loader2, { className: "h-6 w-6 animate-spin text-slate-400" }) })) : (_jsxs("div", { className: "flex flex-col gap-6 py-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Logo" }), _jsxs("div", { className: "flex items-start gap-4", children: [_jsx("div", { className: "flex-shrink-0 w-24 h-24 border rounded-lg overflow-hidden bg-muted flex items-center justify-center", children: logoUrl ? (_jsx("img", { src: logoUrl, alt: "Firm logo", className: "w-full h-full object-contain" })) : (_jsx(ImageIcon, { className: "h-8 w-8 text-muted-foreground" })) }), _jsxs("div", { className: "flex flex-col gap-2 flex-1", children: [_jsx("input", { type: "file", ref: fileInputRef, onChange: handleFileChange, accept: "image/png,image/jpeg,image/svg+xml,image/webp", className: "hidden" }), _jsx(Button, { type: "button", variant: "outline", size: "sm", onClick: handleUploadClick, disabled: uploading, children: uploading ? (_jsxs(_Fragment, { children: [_jsx(Loader2, { className: "h-4 w-4 mr-2 animate-spin" }), "Uploading..."] })) : (_jsxs(_Fragment, { children: [_jsx(Upload, { className: "h-4 w-4 mr-2" }), "Upload Logo"] })) }), logoUrl && (_jsxs(Button, { type: "button", variant: "ghost", size: "sm", onClick: handleRemoveLogo, className: "text-destructive hover:text-destructive", children: [_jsx(Trash2, { className: "h-4 w-4 mr-2" }), "Remove"] })), _jsx("p", { className: "text-xs text-muted-foreground", children: "PNG, JPG, SVG or WebP. Max 500KB." })] })] })] }), _jsxs("div", { className: "grid grid-cols-2 gap-4", children: [_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { htmlFor: "primary_color", children: "Primary Color" }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Input, { id: "primary_color", type: "text", value: primaryColor, onChange: (e) => setPrimaryColor(e.target.value), placeholder: "#1a73e8", className: "flex-1" }), _jsx("input", { type: "color", value: primaryColor || "#000000", onChange: (e) => setPrimaryColor(e.target.value), className: "w-10 h-10 rounded border cursor-pointer", title: "Pick color" })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { htmlFor: "secondary_color", children: "Secondary Color" }), _jsxs("div", { className: "flex gap-2", children: [_jsx(Input, { id: "secondary_color", type: "text", value: secondaryColor, onChange: (e) => setSecondaryColor(e.target.value), placeholder: "#4285f4", className: "flex-1" }), _jsx("input", { type: "color", value: secondaryColor || "#000000", onChange: (e) => setSecondaryColor(e.target.value), className: "w-10 h-10 rounded border cursor-pointer", title: "Pick color" })] })] })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { htmlFor: "tagline", children: "Tagline" }), _jsx(Input, { id: "tagline", type: "text", value: tagline, onChange: (e) => setTagline(e.target.value), placeholder: "Your trusted partner", maxLength: 200 }), _jsx("p", { className: "text-xs text-muted-foreground", children: "A short company tagline (max 200 characters)" })] }), _jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { htmlFor: "app_title", children: "App Title" }), _jsx(Input, { id: "app_title", type: "text", value: appTitle, onChange: (e) => setAppTitle(e.target.value), placeholder: "Acme Portal", maxLength: 200 }), _jsx("p", { className: "text-xs text-muted-foreground", children: "Title shown in the app chrome (browser tab, header)" })] }), _jsxs("div", { className: "flex flex-col gap-4", children: [_jsx(Label, { children: "Footer" }), _jsxs("div", { className: "flex flex-col gap-2 rounded-lg border p-3", children: [_jsx(Label, { htmlFor: "public_footer_copyright", className: "text-xs text-muted-foreground", children: "Public Footer Copyright" }), _jsx(Input, { id: "public_footer_copyright", type: "text", value: publicCopyright, onChange: (e) => setPublicCopyright(e.target.value), placeholder: "\u00A9 2026 Acme Inc." }), _jsxs("div", { className: "flex flex-col gap-2 mt-2", children: [_jsx(Label, { className: "text-xs text-muted-foreground", children: "Public Footer Links" }), publicLinks.map((link, index) => (_jsxs("div", { className: "flex gap-2 items-center", children: [_jsx(Input, { type: "text", value: link.label, onChange: (e) => handleFooterLinkChange(index, "label", e.target.value), placeholder: "Label", className: "flex-1" }), _jsx(Input, { type: "text", value: link.href, onChange: (e) => handleFooterLinkChange(index, "href", e.target.value), placeholder: "/privacy", className: "flex-1" }), _jsx(Button, { type: "button", variant: "ghost", size: "sm", onClick: () => handleRemoveFooterLink(index), className: "text-destructive hover:text-destructive", children: _jsx(Trash2, { className: "h-4 w-4" }) })] }, index))), _jsxs(Button, { type: "button", variant: "outline", size: "sm", onClick: handleAddFooterLink, className: "self-start", children: [_jsx(Plus, { className: "h-4 w-4 mr-2" }), "Add Link"] })] })] }), _jsxs("div", { className: "flex flex-col gap-2 rounded-lg border p-3", children: [_jsx(Label, { htmlFor: "app_footer_copyright", className: "text-xs text-muted-foreground", children: "App Footer Copyright" }), _jsx(Input, { id: "app_footer_copyright", type: "text", value: appCopyright, onChange: (e) => setAppCopyright(e.target.value), placeholder: "\u00A9 2026 Acme Inc." })] })] }), (logoUrl || primaryColor || tagline) && (_jsxs("div", { className: "flex flex-col gap-2", children: [_jsx(Label, { children: "Preview" }), _jsx("div", { className: "p-4 rounded-lg border", style: {
184
223
  backgroundColor: primaryColor
185
224
  ? `${primaryColor}10`
186
225
  : undefined,
@@ -0,0 +1,17 @@
1
+ import type { BrandIdentity } from "hazo_theme";
2
+ import type { FirmBranding } from "./hooks/use_firm_branding";
3
+ /**
4
+ * Maps a hazo_auth `FirmBranding` record into a hazo_theme `BrandIdentity`.
5
+ * Returns `{}` for null/undefined input.
6
+ *
7
+ * - `logo_url` → `logoUrl`
8
+ * - `app_title` → `appTitle`
9
+ * - `footer` → `footer` (passed through unchanged)
10
+ * - `primary_color` (hex) → `primary` (HSL channels)
11
+ * - `secondary_color` (hex) → `accent` (HSL channels)
12
+ *
13
+ * Does NOT set `primaryForeground`, `accentForeground`, fonts, or `themeSet` —
14
+ * `FirmBranding` has no source data for those.
15
+ */
16
+ export declare function firm_branding_to_brand_identity(fb: FirmBranding | null | undefined): BrandIdentity;
17
+ //# sourceMappingURL=firm_branding_to_brand_identity.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"firm_branding_to_brand_identity.d.ts","sourceRoot":"","sources":["../../../../src/components/layouts/shared/firm_branding_to_brand_identity.ts"],"names":[],"mappings":"AAgBA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAChD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAY9D;;;;;;;;;;;;GAYG;AACH,wBAAgB,+BAA+B,CAC7C,EAAE,EAAE,YAAY,GAAG,IAAI,GAAG,SAAS,GAClC,aAAa,CAkBf"}
@@ -0,0 +1,52 @@
1
+ // file_description: Pure mapper from hazo_auth's per-scope FirmBranding into hazo_theme's BrandIdentity contract
2
+ //
3
+ // Direction: hazo_auth owns branding STORAGE (per-scope, hex colors, DB-backed via
4
+ // scope_service/branding_service). hazo_theme owns the RUNTIME whitelabel CONTRACT
5
+ // (BrandIdentity — HSL channel strings + identity data) consumed by `resolveTenantVars`
6
+ // and chrome components. This mapper is a one-way translation: hazo_auth FirmBranding →
7
+ // hazo_theme BrandIdentity. It never flows the other way.
8
+ //
9
+ // Color handling: FirmBranding stores colors as hex strings (e.g. "#1e293b"), matching
10
+ // the `<input type="color">` editor UX. BrandIdentity expects HSL *channel* strings
11
+ // (e.g. "222 47% 11%", no hsl() wrapper) so they can be injected as CSS custom properties.
12
+ // This mapper converts hex → HSL channels via the existing `hex_to_hsl` util. Invalid/
13
+ // malformed hex values are skipped silently (never thrown) so a bad stored value can't
14
+ // crash a page render.
15
+ import { hex_to_hsl } from "../../../theme/hex_to_hsl.js";
16
+ // section: helpers
17
+ const HEX_COLOR_RE = /^#?[0-9a-fA-F]{3}([0-9a-fA-F]{3})?$/;
18
+ function is_valid_hex_color(value) {
19
+ return HEX_COLOR_RE.test(value);
20
+ }
21
+ // section: mapper
22
+ /**
23
+ * Maps a hazo_auth `FirmBranding` record into a hazo_theme `BrandIdentity`.
24
+ * Returns `{}` for null/undefined input.
25
+ *
26
+ * - `logo_url` → `logoUrl`
27
+ * - `app_title` → `appTitle`
28
+ * - `footer` → `footer` (passed through unchanged)
29
+ * - `primary_color` (hex) → `primary` (HSL channels)
30
+ * - `secondary_color` (hex) → `accent` (HSL channels)
31
+ *
32
+ * Does NOT set `primaryForeground`, `accentForeground`, fonts, or `themeSet` —
33
+ * `FirmBranding` has no source data for those.
34
+ */
35
+ export function firm_branding_to_brand_identity(fb) {
36
+ if (!fb)
37
+ return {};
38
+ const identity = {};
39
+ if (fb.logo_url)
40
+ identity.logoUrl = fb.logo_url;
41
+ if (fb.app_title)
42
+ identity.appTitle = fb.app_title;
43
+ if (fb.footer)
44
+ identity.footer = fb.footer;
45
+ if (fb.primary_color && is_valid_hex_color(fb.primary_color)) {
46
+ identity.primary = hex_to_hsl(fb.primary_color);
47
+ }
48
+ if (fb.secondary_color && is_valid_hex_color(fb.secondary_color)) {
49
+ identity.accent = hex_to_hsl(fb.secondary_color);
50
+ }
51
+ return identity;
52
+ }
@@ -1,3 +1,4 @@
1
+ import type { FooterConfig } from "hazo_theme";
1
2
  /**
2
3
  * Firm branding data structure
3
4
  */
@@ -6,6 +7,11 @@ export type FirmBranding = {
6
7
  primary_color?: string;
7
8
  secondary_color?: string;
8
9
  tagline?: string;
10
+ app_title?: string;
11
+ footer?: {
12
+ public?: FooterConfig;
13
+ app?: FooterConfig;
14
+ };
9
15
  };
10
16
  /**
11
17
  * Options for use_firm_branding hook
@@ -1 +1 @@
1
- {"version":3,"file":"use_firm_branding.d.ts","sourceRoot":"","sources":["../../../../../src/components/layouts/shared/hooks/use_firm_branding.ts"],"names":[],"mappings":"AAUA;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,sBAAsB,GAAG;IACnC;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B;;OAEG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAClC;;OAEG;IACH,QAAQ,EAAE,YAAY,GAAG,IAAI,CAAC;IAC9B;;OAEG;IACH,OAAO,EAAE,OAAO,CAAC;IACjB;;OAEG;IACH,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB;;OAEG;IACH,YAAY,EAAE,OAAO,CAAC;IACtB;;OAEG;IACH,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B,CAAC;AAIF;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,CAAC,EAAE,sBAAsB,GAC/B,qBAAqB,CAuEvB;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,IAAI,qBAAqB,CAqHjE"}
1
+ {"version":3,"file":"use_firm_branding.d.ts","sourceRoot":"","sources":["../../../../../src/components/layouts/shared/hooks/use_firm_branding.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAI/C;;GAEG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,YAAY,CAAC;QAAC,GAAG,CAAC,EAAE,YAAY,CAAA;KAAE,CAAC;CACxD,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,sBAAsB,GAAG;IACnC;;;OAGG;IACH,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB;;;OAGG;IACH,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B;;OAEG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;CAChB,CAAC;AAEF;;GAEG;AACH,MAAM,MAAM,qBAAqB,GAAG;IAClC;;OAEG;IACH,QAAQ,EAAE,YAAY,GAAG,IAAI,CAAC;IAC9B;;OAEG;IACH,OAAO,EAAE,OAAO,CAAC;IACjB;;OAEG;IACH,KAAK,EAAE,KAAK,GAAG,IAAI,CAAC;IACpB;;OAEG;IACH,YAAY,EAAE,OAAO,CAAC;IACtB;;OAEG;IACH,OAAO,EAAE,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC;CAC9B,CAAC;AAIF;;;;GAIG;AACH,wBAAgB,iBAAiB,CAC/B,OAAO,CAAC,EAAE,sBAAsB,GAC/B,qBAAqB,CAuEvB;AAED;;;;GAIG;AACH,wBAAgB,yBAAyB,IAAI,qBAAqB,CAqHjE"}
@@ -1,7 +1,7 @@
1
1
  import * as React from "react";
2
2
  import { type VariantProps } from "class-variance-authority";
3
3
  declare const buttonVariants: (props?: ({
4
- variant?: "default" | "link" | "destructive" | "outline" | "secondary" | "ghost" | null | undefined;
4
+ variant?: "link" | "default" | "destructive" | "outline" | "secondary" | "ghost" | null | undefined;
5
5
  size?: "default" | "sm" | "lg" | "icon" | null | undefined;
6
6
  } & import("class-variance-authority/types").ClassProp) | undefined) => string;
7
7
  export interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement>, VariantProps<typeof buttonVariants> {
@@ -14,7 +14,7 @@ import * as React from "react";
14
14
  import { cn } from "../../lib/utils.js";
15
15
  const Input = React.forwardRef((_a, ref) => {
16
16
  var { className, type } = _a, props = __rest(_a, ["className", "type"]);
17
- return (_jsx("input", Object.assign({ type: type, className: cn("flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", className), ref: ref }, props)));
17
+ return (_jsx("input", Object.assign({ type: type, className: cn("flex h-9 w-full rounded-md border border-input bg-transparent px-3 py-1 text-base text-slate-900 shadow-sm transition-colors file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-slate-900 placeholder:text-slate-400 focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:cursor-not-allowed disabled:opacity-50 md:text-sm", className), ref: ref }, props)));
18
18
  });
19
19
  Input.displayName = "Input";
20
20
  export { Input };
@@ -1,4 +1,5 @@
1
1
  import type { HazoConnectAdapter } from "hazo_connect";
2
+ import type { FooterConfig } from "hazo_theme";
2
3
  import { type FirmBranding, type ScopeRecord } from "./scope_service.js";
3
4
  export type BrandingServiceResult = {
4
5
  success: boolean;
@@ -11,6 +12,11 @@ export type UpdateBrandingData = {
11
12
  primary_color?: string | null;
12
13
  secondary_color?: string | null;
13
14
  tagline?: string | null;
15
+ app_title?: string | null;
16
+ footer?: {
17
+ public?: FooterConfig;
18
+ app?: FooterConfig;
19
+ } | null;
14
20
  };
15
21
  /**
16
22
  * Validates branding data
@@ -1 +1 @@
1
- {"version":3,"file":"branding_service.d.ts","sourceRoot":"","sources":["../../../src/lib/services/branding_service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AACvD,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,WAAW,EAOjB,MAAM,iBAAiB,CAAC;AAMzB,MAAM,MAAM,qBAAqB,GAAG;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,YAAY,GAAG,IAAI,CAAC;IAC/B,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;CACzB,CAAC;AAWF;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,kBAAkB,GAAG;IAC3D,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAsBA;AAID;;;GAGG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,kBAAkB,EAC3B,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,qBAAqB,CAAC,CAmChC;AAED;;;GAGG;AACH,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,kBAAkB,EAC3B,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,qBAAqB,CAAC,CA0EhC;AAED;;;GAGG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,kBAAkB,EAC3B,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,kBAAkB,GACvB,OAAO,CAAC,qBAAqB,CAAC,CA2EhC;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,kBAAkB,EAC3B,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,YAAY,GAAG,IAAI,GAC5B,OAAO,CAAC,qBAAqB,CAAC,CA4DhC;AAED;;GAEG;AACH,wBAAsB,cAAc,CAClC,OAAO,EAAE,kBAAkB,EAC3B,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,qBAAqB,CAAC,CAEhC;AAED;;;GAGG;AACH,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,kBAAkB,EAC3B,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,qBAAqB,CAAC,CAiChC"}
1
+ {"version":3,"file":"branding_service.d.ts","sourceRoot":"","sources":["../../../src/lib/services/branding_service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AACvD,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AAC/C,OAAO,EACL,KAAK,YAAY,EACjB,KAAK,WAAW,EAOjB,MAAM,iBAAiB,CAAC;AAMzB,MAAM,MAAM,qBAAqB,GAAG;IAClC,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,YAAY,GAAG,IAAI,CAAC;IAC/B,KAAK,CAAC,EAAE,WAAW,CAAC;IACpB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACzB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC9B,eAAe,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAChC,OAAO,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,MAAM,CAAC,EAAE;QAAE,MAAM,CAAC,EAAE,YAAY,CAAC;QAAC,GAAG,CAAC,EAAE,YAAY,CAAA;KAAE,GAAG,IAAI,CAAC;CAC/D,CAAC;AAoDF;;GAEG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,kBAAkB,GAAG;IAC3D,KAAK,EAAE,OAAO,CAAC;IACf,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAoCA;AAID;;;GAGG;AACH,wBAAsB,kBAAkB,CACtC,OAAO,EAAE,kBAAkB,EAC3B,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,qBAAqB,CAAC,CAmChC;AAED;;;GAGG;AACH,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,kBAAkB,EAC3B,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,qBAAqB,CAAC,CA0EhC;AAED;;;GAGG;AACH,wBAAsB,eAAe,CACnC,OAAO,EAAE,kBAAkB,EAC3B,QAAQ,EAAE,MAAM,EAChB,IAAI,EAAE,kBAAkB,GACvB,OAAO,CAAC,qBAAqB,CAAC,CA+EhC;AAED;;GAEG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,kBAAkB,EAC3B,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,YAAY,GAAG,IAAI,GAC5B,OAAO,CAAC,qBAAqB,CAAC,CA8DhC;AAED;;GAEG;AACH,wBAAsB,cAAc,CAClC,OAAO,EAAE,kBAAkB,EAC3B,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,qBAAqB,CAAC,CAEhC;AAED;;;GAGG;AACH,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,kBAAkB,EAC3B,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,qBAAqB,CAAC,CAiChC"}
@@ -8,6 +8,48 @@ import { sanitize_error_for_user } from "../utils/error_sanitizer.js";
8
8
  function is_valid_hex_color(color) {
9
9
  return /^#[0-9A-Fa-f]{6}$/.test(color);
10
10
  }
11
+ /**
12
+ * Validates an admin-supplied href against a scheme allowlist. Blocks
13
+ * `javascript:`/`data:`/`vbscript:` URIs that would become stored XSS when the
14
+ * value is later rendered into an `<a href>` (defense-in-depth alongside the
15
+ * render-time `safeHref` filter in hazo_ui's chrome components). Allows
16
+ * http(s), mailto, and same-origin absolute paths (`/…` but not `//…`).
17
+ */
18
+ function is_safe_href(url) {
19
+ const t = url.trim().toLowerCase();
20
+ if (t.startsWith("http://") || t.startsWith("https://") || t.startsWith("mailto:")) {
21
+ return true;
22
+ }
23
+ return t.startsWith("/") && !t.startsWith("//");
24
+ }
25
+ /**
26
+ * Walks every admin-configurable href in a footer config (column links + social
27
+ * links across both public/app variants) and returns the first unsafe one, or
28
+ * null when all are safe.
29
+ */
30
+ function first_unsafe_footer_href(footer) {
31
+ var _a, _b;
32
+ if (!footer)
33
+ return null;
34
+ for (const variant of ["public", "app"]) {
35
+ const cfg = footer[variant];
36
+ if (!cfg)
37
+ continue;
38
+ for (const column of (_a = cfg.columns) !== null && _a !== void 0 ? _a : []) {
39
+ for (const link of (_b = column.links) !== null && _b !== void 0 ? _b : []) {
40
+ if ((link === null || link === void 0 ? void 0 : link.href) && !is_safe_href(link.href))
41
+ return link.href;
42
+ }
43
+ }
44
+ if (cfg.social) {
45
+ for (const handle of Object.values(cfg.social)) {
46
+ if (typeof handle === "string" && handle && !is_safe_href(handle))
47
+ return handle;
48
+ }
49
+ }
50
+ }
51
+ return null;
52
+ }
11
53
  /**
12
54
  * Validates branding data
13
55
  */
@@ -28,6 +70,18 @@ export function validate_branding(data) {
28
70
  if (data.logo_url && typeof data.logo_url === "string" && data.logo_url.length > 500) {
29
71
  return { valid: false, error: "Logo URL must be 500 characters or less" };
30
72
  }
73
+ if (data.app_title && typeof data.app_title === "string" && data.app_title.length > 200) {
74
+ return { valid: false, error: "App title must be 200 characters or less" };
75
+ }
76
+ // Reject unsafe footer link/social hrefs (javascript:/data: etc.) before they
77
+ // are persisted to footer_config and later rendered into <a href>.
78
+ const bad_href = first_unsafe_footer_href(data.footer);
79
+ if (bad_href) {
80
+ return {
81
+ valid: false,
82
+ error: "Footer links must use http(s), mailto, or same-origin (/…) URLs",
83
+ };
84
+ }
31
85
  return { valid: true };
32
86
  }
33
87
  // section: core operations
@@ -177,12 +231,16 @@ export async function update_branding(adapter, scope_id, data) {
177
231
  const updated_primary = data.primary_color !== undefined ? data.primary_color : current.primary_color;
178
232
  const updated_secondary = data.secondary_color !== undefined ? data.secondary_color : current.secondary_color;
179
233
  const updated_tagline = data.tagline !== undefined ? data.tagline : current.tagline;
234
+ const updated_app_title = data.app_title !== undefined ? data.app_title : current.app_title;
235
+ const updated_footer = data.footer !== undefined ? data.footer : current.footer;
180
236
  // Update the scope with individual fields
181
237
  const update_result = await update_scope(adapter, scope_id, {
182
238
  logo_url: updated_logo || null,
183
239
  primary_color: updated_primary || null,
184
240
  secondary_color: updated_secondary || null,
185
241
  tagline: updated_tagline || null,
242
+ app_title: updated_app_title || null,
243
+ footer: updated_footer || null,
186
244
  });
187
245
  if (!update_result.success) {
188
246
  return {
@@ -244,6 +302,8 @@ export async function replace_branding(adapter, scope_id, branding) {
244
302
  primary_color: (branding === null || branding === void 0 ? void 0 : branding.primary_color) || null,
245
303
  secondary_color: (branding === null || branding === void 0 ? void 0 : branding.secondary_color) || null,
246
304
  tagline: (branding === null || branding === void 0 ? void 0 : branding.tagline) || null,
305
+ app_title: (branding === null || branding === void 0 ? void 0 : branding.app_title) || null,
306
+ footer: (branding === null || branding === void 0 ? void 0 : branding.footer) || null,
247
307
  });
248
308
  if (!update_result.success) {
249
309
  return {
@@ -3,7 +3,10 @@ export type InvitationStatus = "PENDING" | "ACCEPTED" | "EXPIRED" | "REVOKED";
3
3
  export type InvitationRecord = {
4
4
  id: string;
5
5
  email_address: string;
6
+ /** Opaque unique token identifying this invitation (e.g. for an accept link). */
7
+ token: string;
6
8
  scope_id: string;
9
+ root_scope_id: string;
7
10
  role_id: string;
8
11
  status: InvitationStatus;
9
12
  invited_by: string | null;
@@ -1 +1 @@
1
- {"version":3,"file":"invitation_service.d.ts","sourceRoot":"","sources":["../../../src/lib/services/invitation_service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AASvD,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,GAAG,SAAS,CAAC;AAE9E,MAAM,MAAM,gBAAgB,GAAG;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,gBAAgB,CAAC;IACzB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,WAAW,CAAC,EAAE,gBAAgB,EAAE,CAAC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gEAAgE;IAChE,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AASF;;GAEG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,kBAAkB,EAC3B,IAAI,EAAE,oBAAoB,GACzB,OAAO,CAAC,uBAAuB,CAAC,CAyElC;AAED;;;GAGG;AACH,wBAAsB,+BAA+B,CACnD,OAAO,EAAE,kBAAkB,EAC3B,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,uBAAuB,CAAC,CAkFlC;AAED;;GAEG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,kBAAkB,EAC3B,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,uBAAuB,CAAC,CAmClC;AAED;;GAEG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,kBAAkB,EAC3B,aAAa,EAAE,MAAM,EACrB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,uBAAuB,CAAC,CA8FlC;AAED;;GAEG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,kBAAkB,EAC3B,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,uBAAuB,CAAC,CA0DlC;AAED;;GAEG;AACH,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,kBAAkB,EAC3B,QAAQ,EAAE,MAAM,EAChB,MAAM,CAAC,EAAE,gBAAgB,GACxB,OAAO,CAAC,uBAAuB,CAAC,CAoClC;AAED;;GAEG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,kBAAkB,EAC3B,MAAM,CAAC,EAAE,gBAAgB,GACxB,OAAO,CAAC,uBAAuB,CAAC,CAmClC;AAED;;;GAGG;AACH,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,kBAAkB,GAC1B,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CA0CvE"}
1
+ {"version":3,"file":"invitation_service.d.ts","sourceRoot":"","sources":["../../../src/lib/services/invitation_service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAUvD,MAAM,MAAM,gBAAgB,GAAG,SAAS,GAAG,UAAU,GAAG,SAAS,GAAG,SAAS,CAAC;AAE9E,MAAM,MAAM,gBAAgB,GAAG;IAC7B,EAAE,EAAE,MAAM,CAAC;IACX,aAAa,EAAE,MAAM,CAAC;IACtB,iFAAiF;IACjF,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,aAAa,EAAE,MAAM,CAAC;IACtB,OAAO,EAAE,MAAM,CAAC;IAChB,MAAM,EAAE,gBAAgB,CAAC;IACzB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,UAAU,EAAE,MAAM,CAAC;IACnB,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,UAAU,EAAE,MAAM,CAAC;IACnB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,OAAO,EAAE,OAAO,CAAC;IACjB,UAAU,CAAC,EAAE,gBAAgB,CAAC;IAC9B,WAAW,CAAC,EAAE,gBAAgB,EAAE,CAAC;IACjC,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gEAAgE;IAChE,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB,CAAC;AAEF,MAAM,MAAM,oBAAoB,GAAG;IACjC,aAAa,EAAE,MAAM,CAAC;IACtB,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAkBF;;GAEG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,kBAAkB,EAC3B,IAAI,EAAE,oBAAoB,GACzB,OAAO,CAAC,uBAAuB,CAAC,CAmFlC;AAED;;;GAGG;AACH,wBAAsB,+BAA+B,CACnD,OAAO,EAAE,kBAAkB,EAC3B,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,uBAAuB,CAAC,CAkFlC;AAED;;GAEG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,kBAAkB,EAC3B,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,uBAAuB,CAAC,CAmClC;AAED;;GAEG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,kBAAkB,EAC3B,aAAa,EAAE,MAAM,EACrB,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,uBAAuB,CAAC,CA8FlC;AAED;;GAEG;AACH,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,kBAAkB,EAC3B,aAAa,EAAE,MAAM,GACpB,OAAO,CAAC,uBAAuB,CAAC,CA0DlC;AAED;;GAEG;AACH,wBAAsB,yBAAyB,CAC7C,OAAO,EAAE,kBAAkB,EAC3B,QAAQ,EAAE,MAAM,EAChB,MAAM,CAAC,EAAE,gBAAgB,GACxB,OAAO,CAAC,uBAAuB,CAAC,CAoClC;AAED;;GAEG;AACH,wBAAsB,oBAAoB,CACxC,OAAO,EAAE,kBAAkB,EAC3B,MAAM,CAAC,EAAE,gBAAgB,GACxB,OAAO,CAAC,uBAAuB,CAAC,CAmClC;AAED;;;GAGG;AACH,wBAAsB,sBAAsB,CAC1C,OAAO,EAAE,kBAAkB,GAC1B,OAAO,CAAC;IAAE,OAAO,EAAE,OAAO,CAAC;IAAC,aAAa,CAAC,EAAE,MAAM,CAAC;IAAC,KAAK,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC,CA0CvE"}
@@ -1,4 +1,5 @@
1
1
  import { createCrudService } from "hazo_connect/server";
2
+ import { randomBytes } from "crypto";
2
3
  import { create_app_logger } from "../app_logger.js";
3
4
  import { sanitize_error_for_user } from "../utils/error_sanitizer.js";
4
5
  import { assign_user_scope } from "./user_scope_service.js";
@@ -7,6 +8,14 @@ import { get_scope_by_id, get_root_scope_id } from "./scope_service.js";
7
8
  const DEFAULT_EXPIRY_HOURS = 48;
8
9
  const MAX_PENDING_PER_EMAIL = 5;
9
10
  // section: helpers
11
+ /**
12
+ * Generates an opaque, unique invitation token.
13
+ * `hazo_invitations.token` is `NOT NULL UNIQUE` in the schema (migration 011 /
14
+ * db_setup_sqlite.sql / db_setup_postgres.sql) — every insert must supply one.
15
+ */
16
+ function generate_invitation_token() {
17
+ return randomBytes(32).toString("hex");
18
+ }
10
19
  /**
11
20
  * Creates a new invitation
12
21
  */
@@ -36,9 +45,18 @@ export async function create_invitation(adapter, data) {
36
45
  // Calculate expiration
37
46
  const expires_hours = data.expires_in_hours || DEFAULT_EXPIRY_HOURS;
38
47
  const expires_at = new Date(now.getTime() + expires_hours * 60 * 60 * 1000);
48
+ // `hazo_invitations.root_scope_id` is NOT NULL — same computation
49
+ // `assign_user_scope` (user_scope_service.ts) uses for the analogous
50
+ // `hazo_user_scopes.root_scope_id` column: resolve the scope's root
51
+ // ancestor, falling back to the scope itself if it IS the root (or the
52
+ // computation fails).
53
+ const computed_root_scope_id = await get_root_scope_id(adapter, data.scope_id);
54
+ const root_scope_id = computed_root_scope_id || data.scope_id;
39
55
  const inserted = await invitation_service.insert({
40
56
  email_address: data.email_address.toLowerCase(),
57
+ token: generate_invitation_token(),
41
58
  scope_id: data.scope_id,
59
+ root_scope_id,
42
60
  role_id: data.role_id,
43
61
  status: "PENDING",
44
62
  invited_by: data.invited_by || null,
@@ -1,4 +1,5 @@
1
1
  import type { HazoConnectAdapter } from "hazo_connect";
2
+ import type { FooterConfig } from "hazo_theme";
2
3
  /**
3
4
  * Default system scope ID - for non-multi-tenancy mode
4
5
  * All users are assigned to this scope when multi-tenancy is disabled
@@ -13,6 +14,11 @@ export type FirmBranding = {
13
14
  primary_color?: string | null;
14
15
  secondary_color?: string | null;
15
16
  tagline?: string | null;
17
+ app_title?: string | null;
18
+ footer?: {
19
+ public?: FooterConfig;
20
+ app?: FooterConfig;
21
+ } | null;
16
22
  };
17
23
  export type ScopeRecord = {
18
24
  id: string;
@@ -23,6 +29,11 @@ export type ScopeRecord = {
23
29
  primary_color: string | null;
24
30
  secondary_color: string | null;
25
31
  tagline: string | null;
32
+ app_title: string | null;
33
+ footer: {
34
+ public?: FooterConfig;
35
+ app?: FooterConfig;
36
+ } | null;
26
37
  created_at: string;
27
38
  changed_at: string;
28
39
  };
@@ -40,6 +51,11 @@ export type CreateScopeData = {
40
51
  primary_color?: string | null;
41
52
  secondary_color?: string | null;
42
53
  tagline?: string | null;
54
+ app_title?: string | null;
55
+ footer?: {
56
+ public?: FooterConfig;
57
+ app?: FooterConfig;
58
+ } | null;
43
59
  };
44
60
  export type UpdateScopeData = {
45
61
  name?: string;
@@ -49,6 +65,11 @@ export type UpdateScopeData = {
49
65
  primary_color?: string | null;
50
66
  secondary_color?: string | null;
51
67
  tagline?: string | null;
68
+ app_title?: string | null;
69
+ footer?: {
70
+ public?: FooterConfig;
71
+ app?: FooterConfig;
72
+ } | null;
52
73
  };
53
74
  export type ScopeTreeNode = ScopeRecord & {
54
75
  children?: ScopeTreeNode[];
@@ -1 +1 @@
1
- {"version":3,"file":"scope_service.d.ts","sourceRoot":"","sources":["../../../src/lib/services/scope_service.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"scope_service.d.ts","sourceRoot":"","sources":["../../../src/lib/services/scope_service.ts"],"names":[],"mappings":"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"}
@@ -21,6 +21,18 @@ function normalize_scope_record(raw) {
21
21
  primary_color: raw.primary_color || null,
22
22
  secondary_color: raw.secondary_color || null,
23
23
  tagline: raw.tagline || null,
24
+ app_title: raw.app_title || null,
25
+ footer: (() => {
26
+ const raw_fc = raw.footer_config;
27
+ if (typeof raw_fc !== "string" || raw_fc.length === 0)
28
+ return null;
29
+ try {
30
+ return JSON.parse(raw_fc);
31
+ }
32
+ catch (_a) {
33
+ return null;
34
+ }
35
+ })(),
24
36
  created_at: raw.created_at,
25
37
  changed_at: raw.changed_at,
26
38
  };
@@ -39,13 +51,17 @@ export function extract_branding(scope) {
39
51
  branding.secondary_color = scope.secondary_color;
40
52
  if (scope.tagline)
41
53
  branding.tagline = scope.tagline;
54
+ if (scope.app_title)
55
+ branding.app_title = scope.app_title;
56
+ if (scope.footer)
57
+ branding.footer = scope.footer;
42
58
  return Object.keys(branding).length > 0 ? branding : null;
43
59
  }
44
60
  /**
45
61
  * Checks if a scope has any branding set
46
62
  */
47
63
  export function has_branding(scope) {
48
- return !!(scope.logo_url || scope.primary_color || scope.secondary_color || scope.tagline);
64
+ return !!(scope.logo_url || scope.primary_color || scope.secondary_color || scope.tagline || scope.app_title || scope.footer);
49
65
  }
50
66
  /**
51
67
  * Checks if the given scope_id is the default system scope
@@ -208,6 +224,8 @@ export async function create_scope(adapter, data) {
208
224
  primary_color: data.primary_color || null,
209
225
  secondary_color: data.secondary_color || null,
210
226
  tagline: data.tagline || null,
227
+ app_title: data.app_title || null,
228
+ footer_config: data.footer ? JSON.stringify(data.footer) : null,
211
229
  created_at: now,
212
230
  changed_at: now,
213
231
  };
@@ -313,6 +331,12 @@ export async function update_scope(adapter, scope_id, data) {
313
331
  if (data.tagline !== undefined) {
314
332
  update_data.tagline = data.tagline;
315
333
  }
334
+ if (data.app_title !== undefined) {
335
+ update_data.app_title = data.app_title;
336
+ }
337
+ if (data.footer !== undefined) {
338
+ update_data.footer_config = data.footer ? JSON.stringify(data.footer) : null;
339
+ }
316
340
  const updated = await scope_service.updateById(scope_id, update_data);
317
341
  if (!Array.isArray(updated) || updated.length === 0) {
318
342
  return {
@@ -616,6 +640,8 @@ export async function ensure_default_system_scope(adapter) {
616
640
  primary_color: null,
617
641
  secondary_color: null,
618
642
  tagline: null,
643
+ app_title: null,
644
+ footer_config: null,
619
645
  created_at: now,
620
646
  changed_at: now,
621
647
  });
@@ -1 +1 @@
1
- {"version":3,"file":"create_firm.d.ts","sourceRoot":"","sources":["../../src/page_components/create_firm.tsx"],"names":[],"mappings":"AAEA,OAAO,aAAa,CAAC;AAQrB,MAAM,MAAM,mBAAmB,GAAG;IAChC,iCAAiC;IACjC,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB,CAAC;AAyCF,MAAM,CAAC,OAAO,UAAU,cAAc,CAAC,EAAE,aAAa,EAAE,GAAE,mBAAwB,+BAyBjF;AAGD,OAAO,EAAE,cAAc,EAAE,CAAC"}
1
+ {"version":3,"file":"create_firm.d.ts","sourceRoot":"","sources":["../../src/page_components/create_firm.tsx"],"names":[],"mappings":"AAEA,OAAO,aAAa,CAAC;AAWrB,MAAM,MAAM,mBAAmB,GAAG;IAChC,iCAAiC;IACjC,aAAa,CAAC,EAAE,OAAO,CAAC;CACzB,CAAC;AA8CF,MAAM,CAAC,OAAO,UAAU,cAAc,CAAC,EAAE,aAAa,EAAE,GAAE,mBAAwB,+BA0BjF;AAGD,OAAO,EAAE,cAAc,EAAE,CAAC"}
@@ -5,7 +5,7 @@ import "server-only";
5
5
  // section: imports
6
6
  import CreateFirmLayout from "../components/layouts/create_firm/index.js";
7
7
  import { AuthPageShell } from "../components/layouts/shared/components/auth_page_shell.js";
8
- import { get_config_value } from "../lib/config/config_loader.server.js";
8
+ import { get_config_value, get_config_boolean, } from "../lib/config/config_loader.server.js";
9
9
  // section: config_loader
10
10
  const CREATE_FIRM_SECTION = "hazo_auth__create_firm";
11
11
  // Reads an optional label override: returns the configured string, or undefined
@@ -21,13 +21,14 @@ function get_create_firm_config() {
21
21
  keyword: get_config_value(CREATE_FIRM_SECTION, "keyword", "Firm"),
22
22
  keyword_lower: get_config_value(CREATE_FIRM_SECTION, "keyword_lower", "firm"),
23
23
  verb: get_config_value(CREATE_FIRM_SECTION, "verb", "Create"),
24
- image_src: get_config_value(CREATE_FIRM_SECTION, "image_src", "/hazo_auth/images/new_firm_default.jpg"),
24
+ image_src: get_config_value(CREATE_FIRM_SECTION, "image_src", "/api/hazo_auth/assets/register_default.jpg"),
25
25
  // Explicit per-label overrides (undefined → keyword-derived default).
26
26
  heading: get_optional_config_value("heading"),
27
27
  sub_heading: get_optional_config_value("sub_heading"),
28
28
  firm_name_label: get_optional_config_value("firm_name_label"),
29
29
  org_structure_label: get_optional_config_value("org_structure_label"),
30
30
  org_structure_default: get_config_value(CREATE_FIRM_SECTION, "org_structure_default", "Headquarters"),
31
+ show_org_structure: get_config_boolean(CREATE_FIRM_SECTION, "show_org_structure", true),
31
32
  submit_button_label: get_optional_config_value("submit_button_label"),
32
33
  success_message: get_optional_config_value("success_message"),
33
34
  redirect_route: get_config_value(CREATE_FIRM_SECTION, "redirect_route", "/"),
@@ -36,7 +37,7 @@ function get_create_firm_config() {
36
37
  // section: component
37
38
  export default function CreateFirmPage({ disableNavbar } = {}) {
38
39
  const config = get_create_firm_config();
39
- const layoutContent = (_jsx(CreateFirmLayout, { image_src: config.image_src, keyword: config.keyword, keyword_lower: config.keyword_lower, verb: config.verb, heading: config.heading, sub_heading: config.sub_heading, firm_name_label: config.firm_name_label, org_structure_label: config.org_structure_label, default_org_structure: config.org_structure_default, submit_button_label: config.submit_button_label, success_message: config.success_message, redirect_route: config.redirect_route }));
40
+ const layoutContent = (_jsx(CreateFirmLayout, { image_src: config.image_src, keyword: config.keyword, keyword_lower: config.keyword_lower, verb: config.verb, heading: config.heading, sub_heading: config.sub_heading, firm_name_label: config.firm_name_label, org_structure_label: config.org_structure_label, default_org_structure: config.org_structure_default, show_org_structure: config.show_org_structure, submit_button_label: config.submit_button_label, success_message: config.success_message, redirect_route: config.redirect_route }));
40
41
  if (disableNavbar) {
41
42
  return layoutContent;
42
43
  }
@@ -4,12 +4,14 @@ export { default as ForgotPasswordPage } from "./forgot_password.js";
4
4
  export { default as ResetPasswordPage } from "./reset_password.js";
5
5
  export { default as VerifyEmailPage } from "./verify_email.js";
6
6
  export { default as MySettingsPage } from "./my_settings.js";
7
+ export { CreateFirmPage } from "../page_components/create_firm.js";
7
8
  export type { LoginPageProps } from "./login";
8
9
  export type { RegisterPageProps } from "./register";
9
10
  export type { ForgotPasswordPageProps } from "./forgot_password";
10
11
  export type { ResetPasswordPageProps } from "./reset_password";
11
12
  export type { VerifyEmailPageProps } from "./verify_email";
12
13
  export type { MySettingsPageProps } from "./my_settings";
14
+ export type { CreateFirmPageProps } from "../page_components/create_firm";
13
15
  export { default as OTPPage } from "./otp.js";
14
16
  export type { OTPPageProps } from "./otp";
15
17
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server_pages/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,YAAY,CAAC;AACrD,OAAO,EAAE,OAAO,IAAI,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAClE,OAAO,EAAE,OAAO,IAAI,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAChE,OAAO,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAC5D,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,eAAe,CAAC;AAG1D,YAAY,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAC9C,YAAY,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACpD,YAAY,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AACjE,YAAY,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAC/D,YAAY,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAC3D,YAAY,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACzD,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,OAAO,CAAC;AAC3C,YAAY,EAAE,YAAY,EAAE,MAAM,OAAO,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/server_pages/index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,OAAO,IAAI,SAAS,EAAE,MAAM,SAAS,CAAC;AAC/C,OAAO,EAAE,OAAO,IAAI,YAAY,EAAE,MAAM,YAAY,CAAC;AACrD,OAAO,EAAE,OAAO,IAAI,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAClE,OAAO,EAAE,OAAO,IAAI,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAChE,OAAO,EAAE,OAAO,IAAI,eAAe,EAAE,MAAM,gBAAgB,CAAC;AAC5D,OAAO,EAAE,OAAO,IAAI,cAAc,EAAE,MAAM,eAAe,CAAC;AAC1D,OAAO,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AAGhE,YAAY,EAAE,cAAc,EAAE,MAAM,SAAS,CAAC;AAC9C,YAAY,EAAE,iBAAiB,EAAE,MAAM,YAAY,CAAC;AACpD,YAAY,EAAE,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AACjE,YAAY,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAC/D,YAAY,EAAE,oBAAoB,EAAE,MAAM,gBAAgB,CAAC;AAC3D,YAAY,EAAE,mBAAmB,EAAE,MAAM,eAAe,CAAC;AACzD,YAAY,EAAE,mBAAmB,EAAE,MAAM,gCAAgC,CAAC;AAC1E,OAAO,EAAE,OAAO,IAAI,OAAO,EAAE,MAAM,OAAO,CAAC;AAC3C,YAAY,EAAE,YAAY,EAAE,MAAM,OAAO,CAAC"}
@@ -5,4 +5,5 @@ export { default as ForgotPasswordPage } from "./forgot_password.js";
5
5
  export { default as ResetPasswordPage } from "./reset_password.js";
6
6
  export { default as VerifyEmailPage } from "./verify_email.js";
7
7
  export { default as MySettingsPage } from "./my_settings.js";
8
+ export { CreateFirmPage } from "../page_components/create_firm.js";
8
9
  export { default as OTPPage } from "./otp.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hazo_auth",
3
- "version": "10.7.1",
3
+ "version": "10.8.1",
4
4
  "description": "Zero-config authentication UI components for Next.js with RBAC, OAuth, scope-based multi-tenancy, and invitations",
5
5
  "keywords": [
6
6
  "authentication",
@@ -264,12 +264,13 @@
264
264
  "@radix-ui/react-tooltip": "^1.2.0",
265
265
  "hazo_api": "^2.5.1",
266
266
  "hazo_config": "^2.4.1",
267
- "hazo_connect": "^3.9.0",
267
+ "hazo_connect": "^3.9.2",
268
268
  "hazo_core": "^1.2.1",
269
269
  "hazo_logs": "^2.1.1",
270
270
  "hazo_notify": "^6.1.4",
271
271
  "hazo_secure": "^1.3.0",
272
- "hazo_ui": "^4.9.0",
272
+ "hazo_theme": "^1.0.0",
273
+ "hazo_ui": "^6.0.0",
273
274
  "input-otp": "^1.4.0",
274
275
  "lucide-react": "^0.553.0",
275
276
  "next": "^14.0.0",
@@ -301,6 +302,9 @@
301
302
  "hazo_secure": {
302
303
  "optional": true
303
304
  },
305
+ "hazo_theme": {
306
+ "optional": true
307
+ },
304
308
  "hazo_ui": {
305
309
  "optional": true
306
310
  },
@@ -404,11 +408,12 @@
404
408
  "eslint-plugin-storybook": "^10.0.6",
405
409
  "hazo_api": "^2.5.1",
406
410
  "hazo_config": "^2.4.1",
407
- "hazo_connect": "^3.9.0",
411
+ "hazo_connect": "^3.9.2",
408
412
  "hazo_core": "^1.2.1",
409
413
  "hazo_logs": "^2.1.1",
410
414
  "hazo_notify": "^6.1.4",
411
- "hazo_ui": "^4.9.0",
415
+ "hazo_theme": "^1.0.0",
416
+ "hazo_ui": "^6.0.0",
412
417
  "input-otp": "^1.4.0",
413
418
  "jest": "^30.2.0",
414
419
  "jest-environment-jsdom": "^30.0.0",