hazo_auth 5.1.16 → 5.1.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -921,6 +921,19 @@ auto_link_unverified_accounts = true
921
921
  # Customize button text (optional)
922
922
  google_button_text = Continue with Google
923
923
  oauth_divider_text = or
924
+
925
+ # Post-Login Redirect Configuration (v5.1.16+)
926
+ # URL for users who need to create a firm (default: /hazo_auth/create_firm)
927
+ # create_firm_url = /hazo_auth/create_firm
928
+
929
+ # Default redirect after OAuth login for users with scopes (default: /)
930
+ # default_redirect = /
931
+
932
+ # Skip invitation table check (set true if not using invitations)
933
+ # skip_invitation_check = false
934
+
935
+ # Redirect when skip_invitation_check=true and user has no scope (default: /)
936
+ # no_scope_redirect = /
924
937
  ```
925
938
 
926
939
  ### Step 5: Create NextAuth API Routes
@@ -1034,6 +1047,13 @@ Google OAuth adds one new dependency:
1034
1047
  - Verify `/api/hazo_auth/oauth/google/callback` route exists
1035
1048
  - Check server logs for errors during session creation
1036
1049
 
1050
+ **404 after Google OAuth login (v5.1.16+ fix):**
1051
+ - If users get 404 after Google OAuth, the `hazo_invitations` table may be missing
1052
+ - **Option 1:** Run migration `009_scope_consolidation.sql` to create the table
1053
+ - **Option 2:** Set `skip_invitation_check = true` in `[hazo_auth__oauth]` if not using invitations
1054
+ - Check logs for `invitation_table_missing` warnings
1055
+ - If using custom paths, set `create_firm_url` to your app's create firm page URL
1056
+
1037
1057
  ---
1038
1058
 
1039
1059
  ## Using Components
@@ -723,6 +723,19 @@ auto_link_unverified_accounts = true
723
723
  # Customize button text (optional)
724
724
  google_button_text = Continue with Google
725
725
  oauth_divider_text = or
726
+
727
+ # Post-Login Redirect Configuration (v5.1.16+)
728
+ # URL for users who need to create a firm (default: /hazo_auth/create_firm)
729
+ # create_firm_url = /hazo_auth/create_firm
730
+
731
+ # Default redirect after OAuth login for users with scopes (default: /)
732
+ # default_redirect = /
733
+
734
+ # Skip invitation table check (set true if not using invitations)
735
+ # skip_invitation_check = false
736
+
737
+ # Redirect when skip_invitation_check=true and user has no scope (default: /)
738
+ # no_scope_redirect = /
726
739
  ```
727
740
 
728
741
  **Configuration Options:**
@@ -785,6 +798,7 @@ ls app/api/hazo_auth/set_password/route.ts
785
798
  - [ ] OAuth migration applied (google_id and auth_providers columns added)
786
799
  - [ ] `[hazo_auth__oauth]` section configured in `hazo_auth_config.ini`
787
800
  - [ ] OAuth API routes created (`[...nextauth]`, `oauth/google/callback`, `set_password`)
801
+ - [ ] Post-login redirect configured (if not using invitations, set `skip_invitation_check = true`)
788
802
 
789
803
  ---
790
804
 
@@ -1957,6 +1971,31 @@ curl -H "Cookie: hazo_auth_session=YOUR_TOKEN; hazo_auth_scope_id=SCOPE_UUID" \
1957
1971
  2. Check cookies are being set (inspect browser devtools > Application > Cookies)
1958
1972
  3. Ensure API routes are on same domain (no CORS issues)
1959
1973
 
1974
+ ### Issue: 404 after Google OAuth login
1975
+
1976
+ **Symptoms:** User completes Google sign-in but gets redirected to 404 page (typically `/hazo_auth/create_firm`).
1977
+
1978
+ **Solutions:**
1979
+ 1. **If not using invitations system:** Set `skip_invitation_check = true` in `[hazo_auth__oauth]`:
1980
+ ```ini
1981
+ [hazo_auth__oauth]
1982
+ skip_invitation_check = true
1983
+ no_scope_redirect = /
1984
+ ```
1985
+
1986
+ 2. **If using invitations system:** Run the migration to create `hazo_invitations` table:
1987
+ ```bash
1988
+ npm run migrate migrations/009_scope_consolidation.sql
1989
+ ```
1990
+
1991
+ 3. **If using custom paths:** Set `create_firm_url` to your app's create firm page:
1992
+ ```ini
1993
+ [hazo_auth__oauth]
1994
+ create_firm_url = /my-app/create-organization
1995
+ ```
1996
+
1997
+ 4. **Check logs** for `invitation_table_missing` warnings - this indicates the table doesn't exist
1998
+
1960
1999
  ### Issue: Navbar logo or company name not showing
1961
2000
 
1962
2001
  **Symptoms:** Navbar appears but logo and/or company name are missing.
@@ -165,6 +165,14 @@ export const DEFAULT_OAUTH = {
165
165
  google_button_text: "Continue with Google",
166
166
  /** Text displayed on the divider between OAuth and email/password form */
167
167
  oauth_divider_text: "or continue with email",
168
+ /** URL for users who need to create a firm (default: /hazo_auth/create_firm) */
169
+ create_firm_url: "/hazo_auth/create_firm",
170
+ /** Default redirect after OAuth login for users with scopes */
171
+ default_redirect: "/",
172
+ /** Skip invitation table check (set true if not using invitations) */
173
+ skip_invitation_check: false,
174
+ /** Redirect when skip_invitation_check=true and user has no scope */
175
+ no_scope_redirect: "/",
168
176
  } as const;
169
177
 
170
178
  // section: navbar
@@ -15,6 +15,14 @@ export type OAuthConfig = {
15
15
  google_button_text: string;
16
16
  /** Text displayed on the divider between OAuth and email/password form */
17
17
  oauth_divider_text: string;
18
+ /** URL for users who need to create a firm (default: /hazo_auth/create_firm) */
19
+ create_firm_url: string;
20
+ /** Default redirect after OAuth login for users with scopes */
21
+ default_redirect: string;
22
+ /** Skip invitation table check (set true if not using invitations) */
23
+ skip_invitation_check: boolean;
24
+ /** Redirect when skip_invitation_check=true and user has no scope */
25
+ no_scope_redirect: string;
18
26
  };
19
27
 
20
28
  // section: constants
@@ -57,12 +65,40 @@ export function get_oauth_config(): OAuthConfig {
57
65
  DEFAULT_OAUTH.oauth_divider_text
58
66
  );
59
67
 
68
+ const create_firm_url = get_config_value(
69
+ SECTION_NAME,
70
+ "create_firm_url",
71
+ DEFAULT_OAUTH.create_firm_url
72
+ );
73
+
74
+ const default_redirect = get_config_value(
75
+ SECTION_NAME,
76
+ "default_redirect",
77
+ DEFAULT_OAUTH.default_redirect
78
+ );
79
+
80
+ const skip_invitation_check = get_config_boolean(
81
+ SECTION_NAME,
82
+ "skip_invitation_check",
83
+ DEFAULT_OAUTH.skip_invitation_check
84
+ );
85
+
86
+ const no_scope_redirect = get_config_value(
87
+ SECTION_NAME,
88
+ "no_scope_redirect",
89
+ DEFAULT_OAUTH.no_scope_redirect
90
+ );
91
+
60
92
  return {
61
93
  enable_google,
62
94
  enable_email_password,
63
95
  auto_link_unverified_accounts,
64
96
  google_button_text,
65
97
  oauth_divider_text,
98
+ create_firm_url,
99
+ default_redirect,
100
+ skip_invitation_check,
101
+ no_scope_redirect,
66
102
  };
67
103
  }
68
104
 
@@ -29,6 +29,8 @@ export type InvitationServiceResult = {
29
29
  invitation?: InvitationRecord;
30
30
  invitations?: InvitationRecord[];
31
31
  error?: string;
32
+ /** Set to true when the hazo_invitations table doesn't exist */
33
+ table_missing?: boolean;
32
34
  };
33
35
 
34
36
  export type CreateInvitationData = {
@@ -172,6 +174,33 @@ export async function get_pending_invitation_by_email(
172
174
  };
173
175
  } catch (error) {
174
176
  const logger = create_app_logger();
177
+
178
+ // Check if the error indicates the table doesn't exist
179
+ const error_string = error instanceof Error ? error.message : String(error);
180
+ const is_table_missing =
181
+ // SQLite: "no such table: hazo_invitations"
182
+ error_string.toLowerCase().includes("no such table") ||
183
+ // PostgreSQL: 'relation "hazo_invitations" does not exist'
184
+ (error_string.toLowerCase().includes("relation") && error_string.toLowerCase().includes("does not exist")) ||
185
+ // PostgREST: 404 response for missing table
186
+ (error_string.includes("404") && error_string.includes("hazo_invitations"));
187
+
188
+ if (is_table_missing) {
189
+ logger.warn("invitation_table_missing", {
190
+ filename: "invitation_service.ts",
191
+ line_number: 0,
192
+ operation: "get_pending_invitation_by_email",
193
+ email_address,
194
+ note: "hazo_invitations table does not exist - run migration or set skip_invitation_check=true",
195
+ });
196
+
197
+ return {
198
+ success: false,
199
+ error: "Invitation table not found",
200
+ table_missing: true,
201
+ };
202
+ }
203
+
175
204
  const error_message = sanitize_error_for_user(error, {
176
205
  logToConsole: true,
177
206
  logToLogger: true,
@@ -27,6 +27,28 @@ export type PostVerificationOptions = {
27
27
  create_firm_url?: string; // Default: "/hazo_auth/create_firm"
28
28
  };
29
29
 
30
+ export type PostLoginRedirectOptions = {
31
+ /** Default redirect for users with scopes (default: "/") */
32
+ default_redirect?: string;
33
+ /** URL for users who need to create a firm (default: "/hazo_auth/create_firm") */
34
+ create_firm_url?: string;
35
+ /** Skip invitation table check (set true if not using invitations) */
36
+ skip_invitation_check?: boolean;
37
+ /** Redirect when skip_invitation_check=true and user has no scope */
38
+ no_scope_redirect?: string;
39
+ };
40
+
41
+ export type PostLoginRedirectResult = {
42
+ /** The URL to redirect the user to */
43
+ redirect_url: string;
44
+ /** True if user needs onboarding (no scope, create firm, etc.) */
45
+ needs_onboarding: boolean;
46
+ /** True if invitation check was skipped due to config */
47
+ invitation_check_skipped?: boolean;
48
+ /** True if invitation table query failed (table missing) */
49
+ invitation_table_error?: boolean;
50
+ };
51
+
30
52
  // section: constants
31
53
 
32
54
  const DEFAULT_REDIRECT_URL = "/";
@@ -181,36 +203,71 @@ export async function needs_onboarding(
181
203
 
182
204
  /**
183
205
  * Gets the appropriate redirect URL for a user after login
184
- * Takes into account scope status and url_on_logon
206
+ * Takes into account scope status, invitations, and url_on_logon
207
+ *
208
+ * @param adapter - HazoConnect adapter
209
+ * @param user_id - User ID
210
+ * @param user_email - User email (for invitation lookup)
211
+ * @param options - Configuration options (or string for backwards compatibility)
185
212
  */
186
213
  export async function get_post_login_redirect(
187
214
  adapter: HazoConnectAdapter,
188
215
  user_id: string,
189
216
  user_email: string,
190
- default_redirect: string = DEFAULT_REDIRECT_URL,
191
- ): Promise<{ redirect_url: string; needs_onboarding: boolean }> {
217
+ options?: PostLoginRedirectOptions | string,
218
+ ): Promise<PostLoginRedirectResult> {
219
+ // Handle backwards compatibility: if options is a string, treat it as default_redirect
220
+ const opts: PostLoginRedirectOptions = typeof options === "string"
221
+ ? { default_redirect: options }
222
+ : options || {};
223
+
224
+ const default_redirect = opts.default_redirect || DEFAULT_REDIRECT_URL;
225
+ const create_firm_url = opts.create_firm_url || CREATE_FIRM_URL;
226
+ const skip_invitation_check = opts.skip_invitation_check || false;
227
+ const no_scope_redirect = opts.no_scope_redirect || DEFAULT_REDIRECT_URL;
228
+
192
229
  try {
193
230
  // Check if user has scope
194
231
  const has_scope = await user_has_any_scope(adapter, user_id);
195
232
 
196
233
  if (!has_scope) {
234
+ // User has no scope - check invitations (unless skipped)
235
+ if (skip_invitation_check) {
236
+ // Invitation check skipped via config - redirect to no_scope_redirect
237
+ return {
238
+ redirect_url: no_scope_redirect,
239
+ needs_onboarding: true,
240
+ invitation_check_skipped: true,
241
+ };
242
+ }
243
+
197
244
  // Check for invitation
198
245
  const invitation_result = await get_pending_invitation_by_email(
199
246
  adapter,
200
247
  user_email,
201
248
  );
202
249
 
250
+ // Check if invitation table is missing
251
+ if (invitation_result.table_missing) {
252
+ // Table doesn't exist - redirect to create_firm_url but flag the error
253
+ return {
254
+ redirect_url: create_firm_url,
255
+ needs_onboarding: true,
256
+ invitation_table_error: true,
257
+ };
258
+ }
259
+
203
260
  if (invitation_result.success && invitation_result.invitation) {
204
261
  // Has invitation - they need to complete the flow
205
262
  return {
206
- redirect_url: CREATE_FIRM_URL,
263
+ redirect_url: create_firm_url,
207
264
  needs_onboarding: true,
208
265
  };
209
266
  }
210
267
 
211
268
  // No scope, no invitation - needs to create firm
212
269
  return {
213
- redirect_url: CREATE_FIRM_URL,
270
+ redirect_url: create_firm_url,
214
271
  needs_onboarding: true,
215
272
  };
216
273
  }
@@ -158,6 +158,20 @@ auto_link_unverified_accounts = true
158
158
  # Text displayed on the divider between OAuth and email/password form
159
159
  # oauth_divider_text = or continue with email
160
160
 
161
+ # Post-Login Redirect Configuration
162
+ # URL for users who need to create a firm/organization (default: /hazo_auth/create_firm)
163
+ # create_firm_url = /hazo_auth/create_firm
164
+
165
+ # Default redirect after OAuth login for users with scopes (default: /)
166
+ # default_redirect = /
167
+
168
+ # Skip invitation table check (set true if not using invitations)
169
+ # When enabled, users without scopes will redirect to no_scope_redirect instead of checking invitations
170
+ # skip_invitation_check = false
171
+
172
+ # Redirect when skip_invitation_check=true and user has no scope (default: /)
173
+ # no_scope_redirect = /
174
+
161
175
  [hazo_auth__login_layout]
162
176
  # Image configuration
163
177
  # If image_src is commented out or empty, the default image from package assets will be used
@@ -124,6 +124,14 @@ export declare const DEFAULT_OAUTH: {
124
124
  readonly google_button_text: "Continue with Google";
125
125
  /** Text displayed on the divider between OAuth and email/password form */
126
126
  readonly oauth_divider_text: "or continue with email";
127
+ /** URL for users who need to create a firm (default: /hazo_auth/create_firm) */
128
+ readonly create_firm_url: "/hazo_auth/create_firm";
129
+ /** Default redirect after OAuth login for users with scopes */
130
+ readonly default_redirect: "/";
131
+ /** Skip invitation table check (set true if not using invitations) */
132
+ readonly skip_invitation_check: false;
133
+ /** Redirect when skip_invitation_check=true and user has no scope */
134
+ readonly no_scope_redirect: "/";
127
135
  };
128
136
  export declare const DEFAULT_NAVBAR: {
129
137
  /** Enable navbar on auth pages */
@@ -314,6 +322,14 @@ export declare const HAZO_AUTH_DEFAULTS: {
314
322
  readonly google_button_text: "Continue with Google";
315
323
  /** Text displayed on the divider between OAuth and email/password form */
316
324
  readonly oauth_divider_text: "or continue with email";
325
+ /** URL for users who need to create a firm (default: /hazo_auth/create_firm) */
326
+ readonly create_firm_url: "/hazo_auth/create_firm";
327
+ /** Default redirect after OAuth login for users with scopes */
328
+ readonly default_redirect: "/";
329
+ /** Skip invitation table check (set true if not using invitations) */
330
+ readonly skip_invitation_check: false;
331
+ /** Redirect when skip_invitation_check=true and user has no scope */
332
+ readonly no_scope_redirect: "/";
317
333
  };
318
334
  readonly devLock: {
319
335
  /** Enable the development lock screen (also requires HAZO_AUTH_DEV_LOCK_ENABLED env var) */
@@ -1 +1 @@
1
- {"version":3,"file":"default_config.d.ts","sourceRoot":"","sources":["../../../src/lib/config/default_config.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"default_config.d.ts","sourceRoot":"","sources":["../../../src/lib/config/default_config.ts"],"names":[],"mappings":"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"}
@@ -146,6 +146,14 @@ export const DEFAULT_OAUTH = {
146
146
  google_button_text: "Continue with Google",
147
147
  /** Text displayed on the divider between OAuth and email/password form */
148
148
  oauth_divider_text: "or continue with email",
149
+ /** URL for users who need to create a firm (default: /hazo_auth/create_firm) */
150
+ create_firm_url: "/hazo_auth/create_firm",
151
+ /** Default redirect after OAuth login for users with scopes */
152
+ default_redirect: "/",
153
+ /** Skip invitation table check (set true if not using invitations) */
154
+ skip_invitation_check: false,
155
+ /** Redirect when skip_invitation_check=true and user has no scope */
156
+ no_scope_redirect: "/",
149
157
  };
150
158
  // section: navbar
151
159
  export const DEFAULT_NAVBAR = {
@@ -9,6 +9,14 @@ export type OAuthConfig = {
9
9
  google_button_text: string;
10
10
  /** Text displayed on the divider between OAuth and email/password form */
11
11
  oauth_divider_text: string;
12
+ /** URL for users who need to create a firm (default: /hazo_auth/create_firm) */
13
+ create_firm_url: string;
14
+ /** Default redirect after OAuth login for users with scopes */
15
+ default_redirect: string;
16
+ /** Skip invitation table check (set true if not using invitations) */
17
+ skip_invitation_check: boolean;
18
+ /** Redirect when skip_invitation_check=true and user has no scope */
19
+ no_scope_redirect: string;
12
20
  };
13
21
  /**
14
22
  * Reads OAuth configuration from hazo_auth_config.ini file
@@ -1 +1 @@
1
- {"version":3,"file":"oauth_config.server.d.ts","sourceRoot":"","sources":["../../src/lib/oauth_config.server.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,WAAW,GAAG;IACxB,gCAAgC;IAChC,aAAa,EAAE,OAAO,CAAC;IACvB,8CAA8C;IAC9C,qBAAqB,EAAE,OAAO,CAAC;IAC/B,4EAA4E;IAC5E,6BAA6B,EAAE,OAAO,CAAC;IACvC,kDAAkD;IAClD,kBAAkB,EAAE,MAAM,CAAC;IAC3B,0EAA0E;IAC1E,kBAAkB,EAAE,MAAM,CAAC;CAC5B,CAAC;AAMF;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,WAAW,CAsC9C;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,IAAI,OAAO,CAEjD;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,IAAI,OAAO,CAMnD"}
1
+ {"version":3,"file":"oauth_config.server.d.ts","sourceRoot":"","sources":["../../src/lib/oauth_config.server.ts"],"names":[],"mappings":"AAMA,MAAM,MAAM,WAAW,GAAG;IACxB,gCAAgC;IAChC,aAAa,EAAE,OAAO,CAAC;IACvB,8CAA8C;IAC9C,qBAAqB,EAAE,OAAO,CAAC;IAC/B,4EAA4E;IAC5E,6BAA6B,EAAE,OAAO,CAAC;IACvC,kDAAkD;IAClD,kBAAkB,EAAE,MAAM,CAAC;IAC3B,0EAA0E;IAC1E,kBAAkB,EAAE,MAAM,CAAC;IAC3B,gFAAgF;IAChF,eAAe,EAAE,MAAM,CAAC;IACxB,+DAA+D;IAC/D,gBAAgB,EAAE,MAAM,CAAC;IACzB,sEAAsE;IACtE,qBAAqB,EAAE,OAAO,CAAC;IAC/B,qEAAqE;IACrE,iBAAiB,EAAE,MAAM,CAAC;CAC3B,CAAC;AAMF;;;;GAIG;AACH,wBAAgB,gBAAgB,IAAI,WAAW,CAkE9C;AAED;;;GAGG;AACH,wBAAgB,uBAAuB,IAAI,OAAO,CAEjD;AAED;;;GAGG;AACH,wBAAgB,yBAAyB,IAAI,OAAO,CAMnD"}
@@ -16,12 +16,20 @@ export function get_oauth_config() {
16
16
  const auto_link_unverified_accounts = get_config_boolean(SECTION_NAME, "auto_link_unverified_accounts", DEFAULT_OAUTH.auto_link_unverified_accounts);
17
17
  const google_button_text = get_config_value(SECTION_NAME, "google_button_text", DEFAULT_OAUTH.google_button_text);
18
18
  const oauth_divider_text = get_config_value(SECTION_NAME, "oauth_divider_text", DEFAULT_OAUTH.oauth_divider_text);
19
+ const create_firm_url = get_config_value(SECTION_NAME, "create_firm_url", DEFAULT_OAUTH.create_firm_url);
20
+ const default_redirect = get_config_value(SECTION_NAME, "default_redirect", DEFAULT_OAUTH.default_redirect);
21
+ const skip_invitation_check = get_config_boolean(SECTION_NAME, "skip_invitation_check", DEFAULT_OAUTH.skip_invitation_check);
22
+ const no_scope_redirect = get_config_value(SECTION_NAME, "no_scope_redirect", DEFAULT_OAUTH.no_scope_redirect);
19
23
  return {
20
24
  enable_google,
21
25
  enable_email_password,
22
26
  auto_link_unverified_accounts,
23
27
  google_button_text,
24
28
  oauth_divider_text,
29
+ create_firm_url,
30
+ default_redirect,
31
+ skip_invitation_check,
32
+ no_scope_redirect,
25
33
  };
26
34
  }
27
35
  /**
@@ -17,6 +17,8 @@ export type InvitationServiceResult = {
17
17
  invitation?: InvitationRecord;
18
18
  invitations?: InvitationRecord[];
19
19
  error?: string;
20
+ /** Set to true when the hazo_invitations table doesn't exist */
21
+ table_missing?: boolean;
20
22
  };
21
23
  export type CreateInvitationData = {
22
24
  email_address: string;
@@ -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;CAChB,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,CAuDlC;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;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"}
@@ -112,6 +112,29 @@ export async function get_pending_invitation_by_email(adapter, email_address) {
112
112
  }
113
113
  catch (error) {
114
114
  const logger = create_app_logger();
115
+ // Check if the error indicates the table doesn't exist
116
+ const error_string = error instanceof Error ? error.message : String(error);
117
+ const is_table_missing =
118
+ // SQLite: "no such table: hazo_invitations"
119
+ error_string.toLowerCase().includes("no such table") ||
120
+ // PostgreSQL: 'relation "hazo_invitations" does not exist'
121
+ (error_string.toLowerCase().includes("relation") && error_string.toLowerCase().includes("does not exist")) ||
122
+ // PostgREST: 404 response for missing table
123
+ (error_string.includes("404") && error_string.includes("hazo_invitations"));
124
+ if (is_table_missing) {
125
+ logger.warn("invitation_table_missing", {
126
+ filename: "invitation_service.ts",
127
+ line_number: 0,
128
+ operation: "get_pending_invitation_by_email",
129
+ email_address,
130
+ note: "hazo_invitations table does not exist - run migration or set skip_invitation_check=true",
131
+ });
132
+ return {
133
+ success: false,
134
+ error: "Invitation table not found",
135
+ table_missing: true,
136
+ };
137
+ }
115
138
  const error_message = sanitize_error_for_user(error, {
116
139
  logToConsole: true,
117
140
  logToLogger: true,
@@ -11,6 +11,26 @@ export type PostVerificationOptions = {
11
11
  default_redirect_url?: string;
12
12
  create_firm_url?: string;
13
13
  };
14
+ export type PostLoginRedirectOptions = {
15
+ /** Default redirect for users with scopes (default: "/") */
16
+ default_redirect?: string;
17
+ /** URL for users who need to create a firm (default: "/hazo_auth/create_firm") */
18
+ create_firm_url?: string;
19
+ /** Skip invitation table check (set true if not using invitations) */
20
+ skip_invitation_check?: boolean;
21
+ /** Redirect when skip_invitation_check=true and user has no scope */
22
+ no_scope_redirect?: string;
23
+ };
24
+ export type PostLoginRedirectResult = {
25
+ /** The URL to redirect the user to */
26
+ redirect_url: string;
27
+ /** True if user needs onboarding (no scope, create firm, etc.) */
28
+ needs_onboarding: boolean;
29
+ /** True if invitation check was skipped due to config */
30
+ invitation_check_skipped?: boolean;
31
+ /** True if invitation table query failed (table missing) */
32
+ invitation_table_error?: boolean;
33
+ };
14
34
  /**
15
35
  * Handles the post-email-verification flow
16
36
  *
@@ -36,10 +56,12 @@ export declare function handle_post_verification(adapter: HazoConnectAdapter, us
36
56
  export declare function needs_onboarding(adapter: HazoConnectAdapter, user_id: string): Promise<boolean>;
37
57
  /**
38
58
  * Gets the appropriate redirect URL for a user after login
39
- * Takes into account scope status and url_on_logon
59
+ * Takes into account scope status, invitations, and url_on_logon
60
+ *
61
+ * @param adapter - HazoConnect adapter
62
+ * @param user_id - User ID
63
+ * @param user_email - User email (for invitation lookup)
64
+ * @param options - Configuration options (or string for backwards compatibility)
40
65
  */
41
- export declare function get_post_login_redirect(adapter: HazoConnectAdapter, user_id: string, user_email: string, default_redirect?: string): Promise<{
42
- redirect_url: string;
43
- needs_onboarding: boolean;
44
- }>;
66
+ export declare function get_post_login_redirect(adapter: HazoConnectAdapter, user_id: string, user_email: string, options?: PostLoginRedirectOptions | string): Promise<PostLoginRedirectResult>;
45
67
  //# sourceMappingURL=post_verification_service.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"post_verification_service.d.ts","sourceRoot":"","sources":["../../../src/lib/services/post_verification_service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAYvD,MAAM,MAAM,sBAAsB,GAAG,UAAU,GAAG,aAAa,CAAC;AAEhE,MAAM,MAAM,sBAAsB,GAAG;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,sBAAsB,CAAC;IAC/B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B,CAAC;AA8BF;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,kBAAkB,EAC3B,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,uBAAuB,GAChC,OAAO,CAAC,sBAAsB,CAAC,CAqFjC;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,kBAAkB,EAC3B,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,OAAO,CAAC,CAMlB;AAED;;;GAGG;AACH,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,kBAAkB,EAC3B,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,gBAAgB,GAAE,MAA6B,GAC9C,OAAO,CAAC;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,gBAAgB,EAAE,OAAO,CAAA;CAAE,CAAC,CAwC9D"}
1
+ {"version":3,"file":"post_verification_service.d.ts","sourceRoot":"","sources":["../../../src/lib/services/post_verification_service.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,cAAc,CAAC;AAYvD,MAAM,MAAM,sBAAsB,GAAG,UAAU,GAAG,aAAa,CAAC;AAEhE,MAAM,MAAM,sBAAsB,GAAG;IACnC,OAAO,EAAE,OAAO,CAAC;IACjB,MAAM,EAAE,sBAAsB,CAAC;IAC/B,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,oBAAoB,CAAC,EAAE,MAAM,CAAC;IAC9B,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B,CAAC;AAEF,MAAM,MAAM,wBAAwB,GAAG;IACrC,4DAA4D;IAC5D,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,kFAAkF;IAClF,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,sEAAsE;IACtE,qBAAqB,CAAC,EAAE,OAAO,CAAC;IAChC,qEAAqE;IACrE,iBAAiB,CAAC,EAAE,MAAM,CAAC;CAC5B,CAAC;AAEF,MAAM,MAAM,uBAAuB,GAAG;IACpC,sCAAsC;IACtC,YAAY,EAAE,MAAM,CAAC;IACrB,kEAAkE;IAClE,gBAAgB,EAAE,OAAO,CAAC;IAC1B,yDAAyD;IACzD,wBAAwB,CAAC,EAAE,OAAO,CAAC;IACnC,4DAA4D;IAC5D,sBAAsB,CAAC,EAAE,OAAO,CAAC;CAClC,CAAC;AA8BF;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,wBAAwB,CAC5C,OAAO,EAAE,kBAAkB,EAC3B,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,uBAAuB,GAChC,OAAO,CAAC,sBAAsB,CAAC,CAqFjC;AAED;;;GAGG;AACH,wBAAsB,gBAAgB,CACpC,OAAO,EAAE,kBAAkB,EAC3B,OAAO,EAAE,MAAM,GACd,OAAO,CAAC,OAAO,CAAC,CAMlB;AAED;;;;;;;;GAQG;AACH,wBAAsB,uBAAuB,CAC3C,OAAO,EAAE,kBAAkB,EAC3B,OAAO,EAAE,MAAM,EACf,UAAU,EAAE,MAAM,EAClB,OAAO,CAAC,EAAE,wBAAwB,GAAG,MAAM,GAC1C,OAAO,CAAC,uBAAuB,CAAC,CAsElC"}
@@ -126,25 +126,56 @@ export async function needs_onboarding(adapter, user_id) {
126
126
  }
127
127
  /**
128
128
  * Gets the appropriate redirect URL for a user after login
129
- * Takes into account scope status and url_on_logon
129
+ * Takes into account scope status, invitations, and url_on_logon
130
+ *
131
+ * @param adapter - HazoConnect adapter
132
+ * @param user_id - User ID
133
+ * @param user_email - User email (for invitation lookup)
134
+ * @param options - Configuration options (or string for backwards compatibility)
130
135
  */
131
- export async function get_post_login_redirect(adapter, user_id, user_email, default_redirect = DEFAULT_REDIRECT_URL) {
136
+ export async function get_post_login_redirect(adapter, user_id, user_email, options) {
137
+ // Handle backwards compatibility: if options is a string, treat it as default_redirect
138
+ const opts = typeof options === "string"
139
+ ? { default_redirect: options }
140
+ : options || {};
141
+ const default_redirect = opts.default_redirect || DEFAULT_REDIRECT_URL;
142
+ const create_firm_url = opts.create_firm_url || CREATE_FIRM_URL;
143
+ const skip_invitation_check = opts.skip_invitation_check || false;
144
+ const no_scope_redirect = opts.no_scope_redirect || DEFAULT_REDIRECT_URL;
132
145
  try {
133
146
  // Check if user has scope
134
147
  const has_scope = await user_has_any_scope(adapter, user_id);
135
148
  if (!has_scope) {
149
+ // User has no scope - check invitations (unless skipped)
150
+ if (skip_invitation_check) {
151
+ // Invitation check skipped via config - redirect to no_scope_redirect
152
+ return {
153
+ redirect_url: no_scope_redirect,
154
+ needs_onboarding: true,
155
+ invitation_check_skipped: true,
156
+ };
157
+ }
136
158
  // Check for invitation
137
159
  const invitation_result = await get_pending_invitation_by_email(adapter, user_email);
160
+ // Check if invitation table is missing
161
+ if (invitation_result.table_missing) {
162
+ // Table doesn't exist - redirect to create_firm_url but flag the error
163
+ return {
164
+ redirect_url: create_firm_url,
165
+ needs_onboarding: true,
166
+ invitation_table_error: true,
167
+ };
168
+ }
138
169
  if (invitation_result.success && invitation_result.invitation) {
139
170
  // Has invitation - they need to complete the flow
140
171
  return {
141
- redirect_url: CREATE_FIRM_URL,
172
+ redirect_url: create_firm_url,
142
173
  needs_onboarding: true,
143
174
  };
144
175
  }
145
176
  // No scope, no invitation - needs to create firm
146
177
  return {
147
- redirect_url: CREATE_FIRM_URL,
178
+ redirect_url: create_firm_url,
148
179
  needs_onboarding: true,
149
180
  };
150
181
  }
@@ -1 +1 @@
1
- {"version":3,"file":"oauth_google_callback.d.ts","sourceRoot":"","sources":["../../../src/server/routes/oauth_google_callback.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAsBxD;;;;GAIG;AACH,wBAAsB,GAAG,CAAC,OAAO,EAAE,WAAW,kCA8H7C"}
1
+ {"version":3,"file":"oauth_google_callback.d.ts","sourceRoot":"","sources":["../../../src/server/routes/oauth_google_callback.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAuBxD;;;;GAIG;AACH,wBAAsB,GAAG,CAAC,OAAO,EAAE,WAAW,kCAgJ7C"}
@@ -9,6 +9,7 @@ import { get_login_config } from "../../lib/login_config.server.js";
9
9
  import { get_cookie_name, get_cookie_options, BASE_COOKIE_NAMES } from "../../lib/cookies_config.server.js";
10
10
  import { get_hazo_connect_instance } from "../../lib/hazo_connect_instance.server.js";
11
11
  import { get_post_login_redirect } from "../../lib/services/post_verification_service.js";
12
+ import { get_oauth_config } from "../../lib/oauth_config.server.js";
12
13
  // section: api_handler
13
14
  /**
14
15
  * Handles the OAuth callback after Google sign-in
@@ -62,10 +63,25 @@ export async function GET(request) {
62
63
  });
63
64
  // Get redirect URL based on user's scope/invitation status
64
65
  const loginConfig = get_login_config();
65
- const default_redirect = loginConfig.redirectRoute || "/";
66
+ const oauthConfig = get_oauth_config();
66
67
  // Check if user needs onboarding (no scope, no invitation = create firm)
67
68
  const hazoConnect = get_hazo_connect_instance();
68
- const { redirect_url: determined_redirect, needs_onboarding } = await get_post_login_redirect(hazoConnect, user_id, email, default_redirect);
69
+ const { redirect_url: determined_redirect, needs_onboarding, invitation_check_skipped, invitation_table_error, } = await get_post_login_redirect(hazoConnect, user_id, email, {
70
+ default_redirect: oauthConfig.default_redirect || loginConfig.redirectRoute || "/",
71
+ create_firm_url: oauthConfig.create_firm_url,
72
+ skip_invitation_check: oauthConfig.skip_invitation_check,
73
+ no_scope_redirect: oauthConfig.no_scope_redirect,
74
+ });
75
+ // Log warning if invitation table is missing
76
+ if (invitation_table_error) {
77
+ logger.warn("invitation_table_missing", {
78
+ filename: get_filename(),
79
+ line_number: get_line_number(),
80
+ user_id,
81
+ email,
82
+ note: "hazo_invitations table does not exist - run migration or set skip_invitation_check=true in [hazo_auth__oauth]",
83
+ });
84
+ }
69
85
  logger.info("google_callback_post_login_redirect", {
70
86
  filename: get_filename(),
71
87
  line_number: get_line_number(),
@@ -73,6 +89,8 @@ export async function GET(request) {
73
89
  email,
74
90
  redirect_url: determined_redirect,
75
91
  needs_onboarding,
92
+ invitation_check_skipped,
93
+ invitation_table_error,
76
94
  });
77
95
  // Create redirect response
78
96
  const redirect_url = new URL(determined_redirect, request.url);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "hazo_auth",
3
- "version": "5.1.16",
3
+ "version": "5.1.17",
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",