hazo_auth 4.1.0 → 4.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (78) hide show
  1. package/README.md +230 -0
  2. package/SETUP_CHECKLIST.md +202 -0
  3. package/dist/app/api/hazo_auth/forgot_password/route.d.ts.map +1 -1
  4. package/dist/app/api/hazo_auth/forgot_password/route.js +15 -0
  5. package/dist/app/api/hazo_auth/logout/route.d.ts.map +1 -1
  6. package/dist/app/api/hazo_auth/logout/route.js +31 -0
  7. package/dist/app/api/hazo_auth/me/route.d.ts.map +1 -1
  8. package/dist/app/api/hazo_auth/me/route.js +10 -0
  9. package/dist/components/layouts/forgot_password/hooks/use_forgot_password_form.d.ts +2 -0
  10. package/dist/components/layouts/forgot_password/hooks/use_forgot_password_form.d.ts.map +1 -1
  11. package/dist/components/layouts/forgot_password/hooks/use_forgot_password_form.js +8 -0
  12. package/dist/components/layouts/forgot_password/index.d.ts +7 -1
  13. package/dist/components/layouts/forgot_password/index.d.ts.map +1 -1
  14. package/dist/components/layouts/forgot_password/index.js +7 -2
  15. package/dist/components/layouts/login/index.d.ts +13 -1
  16. package/dist/components/layouts/login/index.d.ts.map +1 -1
  17. package/dist/components/layouts/login/index.js +11 -2
  18. package/dist/components/layouts/my_settings/components/connected_accounts_section.d.ts +17 -0
  19. package/dist/components/layouts/my_settings/components/connected_accounts_section.d.ts.map +1 -0
  20. package/dist/components/layouts/my_settings/components/connected_accounts_section.js +17 -0
  21. package/dist/components/layouts/my_settings/components/set_password_section.d.ts +26 -0
  22. package/dist/components/layouts/my_settings/components/set_password_section.d.ts.map +1 -0
  23. package/dist/components/layouts/my_settings/components/set_password_section.js +127 -0
  24. package/dist/components/layouts/my_settings/hooks/use_my_settings.d.ts +3 -0
  25. package/dist/components/layouts/my_settings/hooks/use_my_settings.d.ts.map +1 -1
  26. package/dist/components/layouts/my_settings/hooks/use_my_settings.js +9 -0
  27. package/dist/components/layouts/my_settings/index.d.ts.map +1 -1
  28. package/dist/components/layouts/my_settings/index.js +4 -2
  29. package/dist/components/layouts/shared/components/google_icon.d.ts +12 -0
  30. package/dist/components/layouts/shared/components/google_icon.d.ts.map +1 -0
  31. package/dist/components/layouts/shared/components/google_icon.js +9 -0
  32. package/dist/components/layouts/shared/components/google_sign_in_button.d.ts +21 -0
  33. package/dist/components/layouts/shared/components/google_sign_in_button.d.ts.map +1 -0
  34. package/dist/components/layouts/shared/components/google_sign_in_button.js +50 -0
  35. package/dist/components/layouts/shared/components/oauth_divider.d.ts +13 -0
  36. package/dist/components/layouts/shared/components/oauth_divider.d.ts.map +1 -0
  37. package/dist/components/layouts/shared/components/oauth_divider.js +13 -0
  38. package/dist/components/layouts/shared/hooks/use_auth_status.d.ts +3 -0
  39. package/dist/components/layouts/shared/hooks/use_auth_status.d.ts.map +1 -1
  40. package/dist/components/layouts/shared/hooks/use_auth_status.js +4 -0
  41. package/dist/components/layouts/shared/index.d.ts +5 -0
  42. package/dist/components/layouts/shared/index.d.ts.map +1 -1
  43. package/dist/components/layouts/shared/index.js +3 -0
  44. package/dist/components/ui/button.d.ts +1 -1
  45. package/dist/lib/auth/nextauth_config.d.ts +34 -0
  46. package/dist/lib/auth/nextauth_config.d.ts.map +1 -0
  47. package/dist/lib/auth/nextauth_config.js +171 -0
  48. package/dist/lib/config/default_config.d.ts +24 -0
  49. package/dist/lib/config/default_config.d.ts.map +1 -1
  50. package/dist/lib/config/default_config.js +14 -0
  51. package/dist/lib/index.d.ts +2 -0
  52. package/dist/lib/index.d.ts.map +1 -1
  53. package/dist/lib/index.js +1 -0
  54. package/dist/lib/login_config.server.d.ts +3 -0
  55. package/dist/lib/login_config.server.d.ts.map +1 -1
  56. package/dist/lib/login_config.server.js +4 -0
  57. package/dist/lib/oauth_config.server.d.ts +29 -0
  58. package/dist/lib/oauth_config.server.d.ts.map +1 -0
  59. package/dist/lib/oauth_config.server.js +40 -0
  60. package/dist/lib/services/login_service.d.ts.map +1 -1
  61. package/dist/lib/services/login_service.js +16 -1
  62. package/dist/lib/services/oauth_service.d.ts +88 -0
  63. package/dist/lib/services/oauth_service.d.ts.map +1 -0
  64. package/dist/lib/services/oauth_service.js +376 -0
  65. package/dist/lib/services/password_reset_service.d.ts +2 -0
  66. package/dist/lib/services/password_reset_service.d.ts.map +1 -1
  67. package/dist/lib/services/password_reset_service.js +10 -0
  68. package/dist/lib/services/registration_service.d.ts.map +1 -1
  69. package/dist/lib/services/registration_service.js +1 -0
  70. package/dist/lib/utils/password_validator.d.ts +13 -0
  71. package/dist/lib/utils/password_validator.d.ts.map +1 -0
  72. package/dist/lib/utils/password_validator.js +36 -0
  73. package/dist/server_pages/login.d.ts.map +1 -1
  74. package/dist/server_pages/login.js +6 -1
  75. package/dist/server_pages/login_client_wrapper.d.ts +5 -2
  76. package/dist/server_pages/login_client_wrapper.d.ts.map +1 -1
  77. package/dist/server_pages/login_client_wrapper.js +2 -2
  78. package/package.json +2 -1
package/README.md CHANGED
@@ -24,6 +24,7 @@ A reusable authentication UI component package powered by Next.js, TailwindCSS,
24
24
  - [Quick Start](#quick-start)
25
25
  - [Configuration Setup](#configuration-setup)
26
26
  - [Database Setup](#database-setup)
27
+ - [Google OAuth Setup](#google-oauth-setup)
27
28
  - [Using Components](#using-components)
28
29
  - [Authentication Service](#authentication-service)
29
30
  - [Proxy/Middleware Authentication](#proxymiddleware-authentication)
@@ -366,6 +367,19 @@ cp node_modules/hazo_auth/hazo_notify_config.example.ini ./hazo_notify_config.in
366
367
 
367
368
  **Note:** `JWT_SECRET` is required for JWT session token functionality (used for Edge-compatible proxy/middleware authentication). Generate a secure random string at least 32 characters long.
368
369
 
370
+ **For Google OAuth (optional):**
371
+ ```env
372
+ # NextAuth.js configuration (required for OAuth)
373
+ NEXTAUTH_SECRET=your_secure_random_string_at_least_32_characters
374
+ NEXTAUTH_URL=http://localhost:3000 # Change to production URL in production
375
+
376
+ # Google OAuth credentials (from Google Cloud Console)
377
+ HAZO_AUTH_GOOGLE_CLIENT_ID=your_google_client_id
378
+ HAZO_AUTH_GOOGLE_CLIENT_SECRET=your_google_client_secret
379
+ ```
380
+
381
+ See [Google OAuth Setup](#google-oauth-setup) for detailed instructions.
382
+
369
383
  **Important:** The configuration files must be located in your project root directory (where `process.cwd()` points to), not inside `node_modules`. The package reads configuration from `process.cwd()` at runtime, so storing them elsewhere (including `node_modules/hazo_auth`) will break runtime access.
370
384
 
371
385
  ---
@@ -672,6 +686,222 @@ npx tsx scripts/apply_migration.ts
672
686
 
673
687
  ---
674
688
 
689
+ ## Google OAuth Setup
690
+
691
+ hazo_auth supports Google Sign-In via NextAuth.js v4, allowing users to authenticate with their Google accounts.
692
+
693
+ ### Features
694
+
695
+ - **Dual authentication**: Users can have BOTH Google OAuth and email/password login
696
+ - **Auto-linking**: Automatically links Google login to existing unverified email/password accounts
697
+ - **Graceful degradation**: Login page adapts based on enabled authentication methods
698
+ - **Set password feature**: Google-only users can add a password later via My Settings
699
+ - **Profile data**: Full name and profile picture automatically populated from Google
700
+
701
+ ### Step 1: Get Google OAuth Credentials
702
+
703
+ 1. Go to [Google Cloud Console](https://console.cloud.google.com/)
704
+ 2. Create a project or select an existing project
705
+ 3. Enable Google+ API (or Google Identity Services)
706
+ 4. Navigate to **Credentials** → **Create Credentials** → **OAuth 2.0 Client ID**
707
+ 5. Configure OAuth consent screen if prompted
708
+ 6. Set **Application type** to "Web application"
709
+ 7. Add **Authorized JavaScript origins**:
710
+ - Development: `http://localhost:3000`
711
+ - Production: `https://yourdomain.com`
712
+ 8. Add **Authorized redirect URIs**:
713
+ - Development: `http://localhost:3000/api/auth/callback/google`
714
+ - Production: `https://yourdomain.com/api/auth/callback/google`
715
+ 9. Copy the **Client ID** and **Client Secret**
716
+
717
+ ### Step 2: Add Environment Variables
718
+
719
+ Add to your `.env.local`:
720
+
721
+ ```env
722
+ # NextAuth.js configuration (REQUIRED for OAuth)
723
+ NEXTAUTH_SECRET=your_secure_random_string_at_least_32_characters
724
+ NEXTAUTH_URL=http://localhost:3000 # Change to production URL in production
725
+
726
+ # Google OAuth credentials (from Google Cloud Console)
727
+ HAZO_AUTH_GOOGLE_CLIENT_ID=your_google_client_id_from_step_1
728
+ HAZO_AUTH_GOOGLE_CLIENT_SECRET=your_google_client_secret_from_step_1
729
+ ```
730
+
731
+ **Generate NEXTAUTH_SECRET:**
732
+ ```bash
733
+ openssl rand -base64 32
734
+ ```
735
+
736
+ ### Step 3: Run Database Migration
737
+
738
+ Add OAuth fields to the `hazo_users` table:
739
+
740
+ ```bash
741
+ npm run migrate migrations/005_add_oauth_fields_to_hazo_users.sql
742
+ ```
743
+
744
+ This migration adds:
745
+ - `google_id` - Google's unique user ID (TEXT, UNIQUE)
746
+ - `auth_providers` - Tracks authentication methods: 'email', 'google', or 'email,google'
747
+ - Index on `google_id` for fast OAuth lookups
748
+
749
+ **Manual migration (if needed):**
750
+
751
+ **PostgreSQL:**
752
+ ```sql
753
+ ALTER TABLE hazo_users
754
+ ADD COLUMN google_id TEXT UNIQUE;
755
+
756
+ ALTER TABLE hazo_users
757
+ ADD COLUMN auth_providers TEXT DEFAULT 'email';
758
+
759
+ CREATE INDEX IF NOT EXISTS idx_hazo_users_google_id ON hazo_users(google_id);
760
+ ```
761
+
762
+ **SQLite:**
763
+ ```sql
764
+ ALTER TABLE hazo_users
765
+ ADD COLUMN google_id TEXT;
766
+
767
+ ALTER TABLE hazo_users
768
+ ADD COLUMN auth_providers TEXT DEFAULT 'email';
769
+
770
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_hazo_users_google_id_unique ON hazo_users(google_id);
771
+ CREATE INDEX IF NOT EXISTS idx_hazo_users_google_id ON hazo_users(google_id);
772
+ ```
773
+
774
+ ### Step 4: Configure OAuth in hazo_auth_config.ini
775
+
776
+ ```ini
777
+ [hazo_auth__oauth]
778
+ # Enable Google OAuth login (default: true)
779
+ enable_google = true
780
+
781
+ # Enable traditional email/password login (default: true)
782
+ enable_email_password = true
783
+
784
+ # Auto-link Google login to existing unverified email/password accounts (default: true)
785
+ auto_link_unverified_accounts = true
786
+
787
+ # Customize button text (optional)
788
+ google_button_text = Continue with Google
789
+ oauth_divider_text = or
790
+ ```
791
+
792
+ ### Step 5: Create NextAuth API Routes
793
+
794
+ Create `app/api/auth/[...nextauth]/route.ts`:
795
+
796
+ ```typescript
797
+ export { GET, POST } from "hazo_auth/server/routes/nextauth";
798
+ ```
799
+
800
+ Create `app/api/hazo_auth/oauth/google/callback/route.ts`:
801
+
802
+ ```typescript
803
+ export { GET } from "hazo_auth/server/routes/oauth_google_callback";
804
+ ```
805
+
806
+ Create `app/api/hazo_auth/set_password/route.ts`:
807
+
808
+ ```typescript
809
+ export { POST } from "hazo_auth/server/routes/set_password";
810
+ ```
811
+
812
+ **Or use the CLI generator:**
813
+ ```bash
814
+ npx hazo_auth generate-routes --oauth
815
+ ```
816
+
817
+ ### Step 6: Test Google OAuth
818
+
819
+ 1. Start your dev server: `npm run dev`
820
+ 2. Visit `http://localhost:3000/hazo_auth/login`
821
+ 3. You should see the "Sign in with Google" button
822
+ 4. Click it and authenticate with your Google account
823
+ 5. You'll be redirected back and logged in
824
+
825
+ ### User Flows
826
+
827
+ **New User - Google Sign-In:**
828
+ - User clicks "Sign in with Google"
829
+ - Authenticates with Google
830
+ - Account created with Google profile data (email, name, profile picture)
831
+ - Email is automatically verified
832
+ - User can log in with Google anytime
833
+
834
+ **Existing Unverified User - Google Sign-In:**
835
+ - User has email/password account but hasn't verified email
836
+ - Clicks "Sign in with Google" with same email
837
+ - System auto-links Google account (if `auto_link_unverified_accounts = true`)
838
+ - Email becomes verified
839
+ - User can now log in with EITHER Google OR email/password
840
+
841
+ **Google-Only User Adds Password:**
842
+ - Google-only user visits My Settings
843
+ - "Set Password" section appears
844
+ - User sets a password
845
+ - User can now log in with EITHER method
846
+
847
+ **Google-Only User Tries Forgot Password:**
848
+ - User registered with Google tries "Forgot Password"
849
+ - System shows: "You registered with Google. Please sign in with Google instead."
850
+
851
+ ### Configuration Options
852
+
853
+ **Disable email/password login (Google-only):**
854
+ ```ini
855
+ [hazo_auth__oauth]
856
+ enable_google = true
857
+ enable_email_password = false
858
+ ```
859
+
860
+ **Disable Google OAuth (email/password only):**
861
+ ```ini
862
+ [hazo_auth__oauth]
863
+ enable_google = false
864
+ enable_email_password = true
865
+ ```
866
+
867
+ ### API Response Changes
868
+
869
+ The `/api/hazo_auth/me` endpoint now includes OAuth status:
870
+
871
+ ```typescript
872
+ {
873
+ authenticated: true,
874
+ // ... existing fields
875
+ auth_providers: "email,google", // NEW: Tracks authentication methods
876
+ has_password: true, // NEW: Whether user has password set
877
+ google_connected: true, // NEW: Whether Google account is linked
878
+ }
879
+ ```
880
+
881
+ ### Dependencies
882
+
883
+ Google OAuth adds one new dependency:
884
+ - `next-auth@^4.24.11` - NextAuth.js for OAuth handling (automatically installed with hazo_auth)
885
+
886
+ ### Troubleshooting
887
+
888
+ **"Sign in with Google" button not showing:**
889
+ - Verify `enable_google = true` in `[hazo_auth__oauth]` section
890
+ - Check `HAZO_AUTH_GOOGLE_CLIENT_ID` and `HAZO_AUTH_GOOGLE_CLIENT_SECRET` are set
891
+ - Check `NEXTAUTH_URL` matches your current URL
892
+
893
+ **OAuth callback error:**
894
+ - Verify redirect URI in Google Cloud Console matches exactly: `http://localhost:3000/api/auth/callback/google`
895
+ - Check `NEXTAUTH_SECRET` is set and at least 32 characters
896
+ - Verify API routes are created: `/api/auth/[...nextauth]/route.ts` and `/api/hazo_auth/oauth/google/callback/route.ts`
897
+
898
+ **User created but not logged in:**
899
+ - Check browser console for errors
900
+ - Verify `/api/hazo_auth/oauth/google/callback` route exists
901
+ - Check server logs for errors during session creation
902
+
903
+ ---
904
+
675
905
  ## Using Components
676
906
 
677
907
  ### Package Exports
@@ -492,6 +492,169 @@ This script will:
492
492
 
493
493
  ---
494
494
 
495
+ ## Phase 3.2: Google OAuth Setup (Optional)
496
+
497
+ Google OAuth Sign-In allows users to authenticate with their Google accounts. This section is optional - skip if you don't need OAuth.
498
+
499
+ ### Step 3.2.1: Get Google OAuth Credentials
500
+
501
+ 1. Go to [Google Cloud Console](https://console.cloud.google.com/)
502
+ 2. Create a project or select an existing project
503
+ 3. Enable **Google+ API** (or Google Identity Services)
504
+ 4. Navigate to **Credentials** → **Create Credentials** → **OAuth 2.0 Client ID**
505
+ 5. Configure OAuth consent screen if prompted
506
+ 6. Set **Application type** to "Web application"
507
+ 7. Add **Authorized JavaScript origins**:
508
+ - Development: `http://localhost:3000`
509
+ - Production: `https://yourdomain.com`
510
+ 8. Add **Authorized redirect URIs**:
511
+ - Development: `http://localhost:3000/api/auth/callback/google`
512
+ - Production: `https://yourdomain.com/api/auth/callback/google`
513
+ 9. Copy the **Client ID** and **Client Secret**
514
+
515
+ ### Step 3.2.2: Add OAuth Environment Variables
516
+
517
+ Add to your `.env.local`:
518
+
519
+ ```env
520
+ # NextAuth.js configuration (REQUIRED for OAuth)
521
+ NEXTAUTH_SECRET=your_secure_random_string_at_least_32_characters
522
+ NEXTAUTH_URL=http://localhost:3000 # Change to production URL in production
523
+
524
+ # Google OAuth credentials (from Google Cloud Console)
525
+ HAZO_AUTH_GOOGLE_CLIENT_ID=your_google_client_id_from_step_1
526
+ HAZO_AUTH_GOOGLE_CLIENT_SECRET=your_google_client_secret_from_step_1
527
+ ```
528
+
529
+ **Generate NEXTAUTH_SECRET:**
530
+ ```bash
531
+ openssl rand -base64 32
532
+ ```
533
+
534
+ ### Step 3.2.3: Run OAuth Database Migration
535
+
536
+ Add OAuth fields to the `hazo_users` table:
537
+
538
+ ```bash
539
+ npm run migrate migrations/005_add_oauth_fields_to_hazo_users.sql
540
+ ```
541
+
542
+ **Verify migration applied:**
543
+
544
+ **PostgreSQL:**
545
+ ```sql
546
+ SELECT column_name FROM information_schema.columns
547
+ WHERE table_name = 'hazo_users'
548
+ AND column_name IN ('google_id', 'auth_providers');
549
+ -- Expected: 2 rows returned
550
+ ```
551
+
552
+ **SQLite:**
553
+ ```bash
554
+ sqlite3 data/hazo_auth.sqlite ".schema hazo_users" | grep -E "google_id|auth_providers"
555
+ # Expected: google_id TEXT UNIQUE, auth_providers TEXT DEFAULT 'email'
556
+ ```
557
+
558
+ **Manual migration (if needed):**
559
+
560
+ **PostgreSQL:**
561
+ ```sql
562
+ ALTER TABLE hazo_users ADD COLUMN google_id TEXT UNIQUE;
563
+ ALTER TABLE hazo_users ADD COLUMN auth_providers TEXT DEFAULT 'email';
564
+ CREATE INDEX IF NOT EXISTS idx_hazo_users_google_id ON hazo_users(google_id);
565
+ ```
566
+
567
+ **SQLite:**
568
+ ```sql
569
+ ALTER TABLE hazo_users ADD COLUMN google_id TEXT;
570
+ ALTER TABLE hazo_users ADD COLUMN auth_providers TEXT DEFAULT 'email';
571
+ CREATE UNIQUE INDEX IF NOT EXISTS idx_hazo_users_google_id_unique ON hazo_users(google_id);
572
+ CREATE INDEX IF NOT EXISTS idx_hazo_users_google_id ON hazo_users(google_id);
573
+ ```
574
+
575
+ ### Step 3.2.4: Configure OAuth in hazo_auth_config.ini
576
+
577
+ Add (or modify) the OAuth section:
578
+
579
+ ```ini
580
+ [hazo_auth__oauth]
581
+ # Enable Google OAuth login (default: true)
582
+ enable_google = true
583
+
584
+ # Enable traditional email/password login (default: true)
585
+ enable_email_password = true
586
+
587
+ # Auto-link Google login to existing unverified email/password accounts (default: true)
588
+ auto_link_unverified_accounts = true
589
+
590
+ # Customize button text (optional)
591
+ google_button_text = Continue with Google
592
+ oauth_divider_text = or
593
+ ```
594
+
595
+ **Configuration Options:**
596
+
597
+ - **Google-only authentication** (no email/password):
598
+ ```ini
599
+ enable_google = true
600
+ enable_email_password = false
601
+ ```
602
+
603
+ - **Email/password only** (no OAuth):
604
+ ```ini
605
+ enable_google = false
606
+ enable_email_password = true
607
+ ```
608
+
609
+ - **Both methods** (recommended):
610
+ ```ini
611
+ enable_google = true
612
+ enable_email_password = true
613
+ ```
614
+
615
+ ### Step 3.2.5: Create OAuth API Routes
616
+
617
+ **Option A: Use CLI generator (Recommended):**
618
+ ```bash
619
+ npx hazo_auth generate-routes --oauth
620
+ ```
621
+
622
+ **Option B: Create manually:**
623
+
624
+ Create `app/api/auth/[...nextauth]/route.ts`:
625
+ ```typescript
626
+ export { GET, POST } from "hazo_auth/server/routes/nextauth";
627
+ ```
628
+
629
+ Create `app/api/hazo_auth/oauth/google/callback/route.ts`:
630
+ ```typescript
631
+ export { GET } from "hazo_auth/server/routes/oauth_google_callback";
632
+ ```
633
+
634
+ Create `app/api/hazo_auth/set_password/route.ts`:
635
+ ```typescript
636
+ export { POST } from "hazo_auth/server/routes/set_password";
637
+ ```
638
+
639
+ ### Step 3.2.6: Verify OAuth API Routes
640
+
641
+ ```bash
642
+ ls app/api/auth/\[...nextauth\]/route.ts
643
+ ls app/api/hazo_auth/oauth/google/callback/route.ts
644
+ ls app/api/hazo_auth/set_password/route.ts
645
+ # All files should exist
646
+ ```
647
+
648
+ **OAuth Setup Checklist:**
649
+ - [ ] Google OAuth credentials obtained
650
+ - [ ] `NEXTAUTH_SECRET` and `NEXTAUTH_URL` set in `.env.local`
651
+ - [ ] `HAZO_AUTH_GOOGLE_CLIENT_ID` and `HAZO_AUTH_GOOGLE_CLIENT_SECRET` set
652
+ - [ ] OAuth migration applied (google_id and auth_providers columns added)
653
+ - [ ] `[hazo_auth__oauth]` section configured in `hazo_auth_config.ini`
654
+ - [ ] OAuth API routes created (`[...nextauth]`, `oauth/google/callback`, `set_password`)
655
+
656
+ ---
657
+
495
658
  ## Phase 4: API Routes
496
659
 
497
660
  Create API route files in your project. Each file re-exports handlers from hazo_auth.
@@ -956,6 +1119,45 @@ Visit each page and verify it loads:
956
1119
  - [ ] `http://localhost:3000/hazo_auth/my_settings` - Settings page displays (after login)
957
1120
  - [ ] `http://localhost:3000/hazo_auth/profile_stamp_test` - ProfileStamp component examples display
958
1121
 
1122
+ ### Test 7: Google OAuth (if configured)
1123
+
1124
+ If you completed Phase 3.2 (Google OAuth Setup):
1125
+
1126
+ **Check login page:**
1127
+ 1. Visit `http://localhost:3000/hazo_auth/login`
1128
+ 2. Verify "Sign in with Google" button appears
1129
+ 3. Verify divider with "or" text (if email/password is also enabled)
1130
+
1131
+ **Test Google sign-in:**
1132
+ 1. Click "Sign in with Google" button
1133
+ 2. You should be redirected to Google's login page
1134
+ 3. Sign in with your Google account
1135
+ 4. You should be redirected back to your app and logged in
1136
+ 5. Visit `/hazo_auth/my_settings`
1137
+ 6. Verify "Connected Accounts" section shows Google as connected
1138
+
1139
+ **Test Google-only user features:**
1140
+ 1. If you signed in with Google (and didn't have a password account first):
1141
+ 2. Visit `/hazo_auth/my_settings`
1142
+ 3. Verify "Set Password" section appears
1143
+ 4. Set a password
1144
+ 5. Log out and try logging in with email/password (should work)
1145
+
1146
+ **Test forgot password with Google-only user:**
1147
+ 1. Create a new user with Google OAuth only
1148
+ 2. Try visiting `/hazo_auth/forgot_password`
1149
+ 3. Enter the Google user's email
1150
+ 4. Should show message: "You registered with Google. Please sign in with Google instead."
1151
+
1152
+ **OAuth Test Checklist:**
1153
+ - [ ] "Sign in with Google" button appears on login page
1154
+ - [ ] OAuth divider appears (if both auth methods enabled)
1155
+ - [ ] Google OAuth flow completes successfully
1156
+ - [ ] User is logged in after OAuth callback
1157
+ - [ ] Connected Accounts section shows Google in My Settings
1158
+ - [ ] Set Password feature works for Google-only users
1159
+ - [ ] Forgot password shows appropriate message for Google-only users
1160
+
959
1161
  ---
960
1162
 
961
1163
  ## Phase 7: HRBAC Setup (Optional)
@@ -1 +1 @@
1
- {"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../../../../src/app/api/hazo_auth/forgot_password/route.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAOxD,wBAAsB,IAAI,CAAC,OAAO,EAAE,WAAW;;;;;IAgG9C"}
1
+ {"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../../../../src/app/api/hazo_auth/forgot_password/route.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAOxD,wBAAsB,IAAI,CAAC,OAAO,EAAE,WAAW;;;;;IAoH9C"}
@@ -49,6 +49,21 @@ export async function POST(request) {
49
49
  message: "If an account with that email exists, a password reset link has been sent.",
50
50
  }, { status: 200 });
51
51
  }
52
+ // Check if this is a Google-only user (no password set)
53
+ if (result.no_password_set) {
54
+ logger.info("password_reset_no_password_set", {
55
+ filename: get_filename(),
56
+ line_number: get_line_number(),
57
+ email,
58
+ note: "User does not have a password set (OAuth-only account)",
59
+ });
60
+ // Return success to prevent email enumeration, but include flag for UI handling
61
+ return NextResponse.json({
62
+ success: true,
63
+ no_password_set: true,
64
+ message: "If an account with that email exists, a password reset link has been sent.",
65
+ }, { status: 200 });
66
+ }
52
67
  logger.info("password_reset_requested", {
53
68
  filename: get_filename(),
54
69
  line_number: get_line_number(),
@@ -1 +1 @@
1
- {"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../../../../src/app/api/hazo_auth/logout/route.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAOxD,wBAAsB,IAAI,CAAC,OAAO,EAAE,WAAW;;;;;IAmF9C"}
1
+ {"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../../../../src/app/api/hazo_auth/logout/route.ts"],"names":[],"mappings":"AAEA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAOxD,wBAAsB,IAAI,CAAC,OAAO,EAAE,WAAW;;;;;IAkH9C"}
@@ -32,6 +32,37 @@ export async function POST(request) {
32
32
  expires: new Date(0),
33
33
  path: "/",
34
34
  });
35
+ // Clear NextAuth session cookies (for OAuth users)
36
+ response.cookies.set("next-auth.session-token", "", {
37
+ expires: new Date(0),
38
+ path: "/",
39
+ });
40
+ response.cookies.set("next-auth.csrf-token", "", {
41
+ expires: new Date(0),
42
+ path: "/",
43
+ });
44
+ response.cookies.set("next-auth.callback-url", "", {
45
+ expires: new Date(0),
46
+ path: "/",
47
+ });
48
+ // Also clear secure cookie variants (used in production with HTTPS)
49
+ response.cookies.set("__Secure-next-auth.session-token", "", {
50
+ expires: new Date(0),
51
+ path: "/",
52
+ });
53
+ response.cookies.set("__Secure-next-auth.csrf-token", "", {
54
+ expires: new Date(0),
55
+ path: "/",
56
+ });
57
+ response.cookies.set("__Secure-next-auth.callback-url", "", {
58
+ expires: new Date(0),
59
+ path: "/",
60
+ });
61
+ // Host-prefixed variants (for some NextAuth configurations)
62
+ response.cookies.set("__Host-next-auth.csrf-token", "", {
63
+ expires: new Date(0),
64
+ path: "/",
65
+ });
35
66
  // Invalidate user cache
36
67
  if (user_id) {
37
68
  try {
@@ -1 +1 @@
1
- {"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../../../../src/app/api/hazo_auth/me/route.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AASxD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAsB,GAAG,CAAC,OAAO,EAAE,WAAW;;IA+E7C"}
1
+ {"version":3,"file":"route.d.ts","sourceRoot":"","sources":["../../../../../src/app/api/hazo_auth/me/route.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AASxD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,wBAAsB,GAAG,CAAC,OAAO,EAAE,WAAW;;IA2F7C"}
@@ -65,6 +65,12 @@ export async function GET(request) {
65
65
  // Map database profile_source to UI representation
66
66
  const profile_source_db = user_db.profile_source;
67
67
  const profile_source_ui = profile_source_db ? map_db_source_to_ui(profile_source_db) : undefined;
68
+ // Parse auth_providers (comma-separated string to array)
69
+ const auth_providers_str = user_db.auth_providers || "email";
70
+ const auth_providers = auth_providers_str.split(",").map((p) => p.trim());
71
+ // Check if user has a password set
72
+ const password_hash = user_db.password_hash;
73
+ const has_password = password_hash !== null && password_hash !== undefined && password_hash !== "";
68
74
  // Return unified format with all fields
69
75
  const profile_pic = auth_result.user.profile_picture_url;
70
76
  return NextResponse.json({
@@ -81,6 +87,10 @@ export async function GET(request) {
81
87
  avatar_url: profile_pic,
82
88
  image: profile_pic,
83
89
  profile_source: profile_source_ui,
90
+ // OAuth-related fields
91
+ auth_providers,
92
+ has_password,
93
+ google_connected: auth_providers.includes("google"),
84
94
  // Permissions and user object (always included)
85
95
  user: auth_result.user,
86
96
  permissions: auth_result.permissions,
@@ -13,6 +13,8 @@ export type UseForgotPasswordFormResult = {
13
13
  isSubmitDisabled: boolean;
14
14
  isSubmitting: boolean;
15
15
  emailTouched: boolean;
16
+ /** True if the submitted email is for a Google-only account (no password set) */
17
+ isGoogleOnlyAccount: boolean;
16
18
  handleFieldChange: (fieldId: ForgotPasswordFieldId, value: string) => void;
17
19
  handleEmailBlur: () => void;
18
20
  handleSubmit: (event: React.FormEvent<HTMLFormElement>) => void;
@@ -1 +1 @@
1
- {"version":3,"file":"use_forgot_password_form.d.ts","sourceRoot":"","sources":["../../../../../src/components/layouts/forgot_password/hooks/use_forgot_password_form.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sCAAsC,CAAC;AAC7E,OAAO,EAA6B,KAAK,qBAAqB,EAAE,MAAM,wCAAwC,CAAC;AAK/G,MAAM,MAAM,wBAAwB,GAAG,MAAM,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAC;AAC7E,MAAM,MAAM,wBAAwB,GAAG,OAAO,CAAC,MAAM,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAC,GAAG;IACtF,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,2BAA2B,CAAC,OAAO,GAAG,OAAO,IAAI;IAC3D,UAAU,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAC;CACvC,CAAC;AAEF,MAAM,MAAM,2BAA2B,GAAG;IACxC,MAAM,EAAE,wBAAwB,CAAC;IACjC,MAAM,EAAE,wBAAwB,CAAC;IACjC,gBAAgB,EAAE,OAAO,CAAC;IAC1B,YAAY,EAAE,OAAO,CAAC;IACtB,YAAY,EAAE,OAAO,CAAC;IACtB,iBAAiB,EAAE,CAAC,OAAO,EAAE,qBAAqB,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3E,eAAe,EAAE,MAAM,IAAI,CAAC;IAC5B,YAAY,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,SAAS,CAAC,eAAe,CAAC,KAAK,IAAI,CAAC;IAChE,YAAY,EAAE,MAAM,IAAI,CAAC;CAC1B,CAAC;AAQF,eAAO,MAAM,wBAAwB,GAAI,OAAO,EAAG,iBAEhD,2BAA2B,CAAC,OAAO,CAAC,KAAG,2BA4IzC,CAAC"}
1
+ {"version":3,"file":"use_forgot_password_form.d.ts","sourceRoot":"","sources":["../../../../../src/components/layouts/forgot_password/hooks/use_forgot_password_form.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,sCAAsC,CAAC;AAC7E,OAAO,EAA6B,KAAK,qBAAqB,EAAE,MAAM,wCAAwC,CAAC;AAK/G,MAAM,MAAM,wBAAwB,GAAG,MAAM,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAC;AAC7E,MAAM,MAAM,wBAAwB,GAAG,OAAO,CAAC,MAAM,CAAC,qBAAqB,EAAE,MAAM,CAAC,CAAC,GAAG;IACtF,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,MAAM,MAAM,2BAA2B,CAAC,OAAO,GAAG,OAAO,IAAI;IAC3D,UAAU,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAC;CACvC,CAAC;AAEF,MAAM,MAAM,2BAA2B,GAAG;IACxC,MAAM,EAAE,wBAAwB,CAAC;IACjC,MAAM,EAAE,wBAAwB,CAAC;IACjC,gBAAgB,EAAE,OAAO,CAAC;IAC1B,YAAY,EAAE,OAAO,CAAC;IACtB,YAAY,EAAE,OAAO,CAAC;IACtB,iFAAiF;IACjF,mBAAmB,EAAE,OAAO,CAAC;IAC7B,iBAAiB,EAAE,CAAC,OAAO,EAAE,qBAAqB,EAAE,KAAK,EAAE,MAAM,KAAK,IAAI,CAAC;IAC3E,eAAe,EAAE,MAAM,IAAI,CAAC;IAC5B,YAAY,EAAE,CAAC,KAAK,EAAE,KAAK,CAAC,SAAS,CAAC,eAAe,CAAC,KAAK,IAAI,CAAC;IAChE,YAAY,EAAE,MAAM,IAAI,CAAC;CAC1B,CAAC;AAQF,eAAO,MAAM,wBAAwB,GAAI,OAAO,EAAG,iBAEhD,2BAA2B,CAAC,OAAO,CAAC,KAAG,2BAqJzC,CAAC"}
@@ -16,6 +16,7 @@ export const use_forgot_password_form = ({ dataClient, }) => {
16
16
  const [errors, setErrors] = useState({});
17
17
  const [emailTouched, setEmailTouched] = useState(false);
18
18
  const [isSubmitting, setIsSubmitting] = useState(false);
19
+ const [isGoogleOnlyAccount, setIsGoogleOnlyAccount] = useState(false);
19
20
  const isSubmitDisabled = useMemo(() => {
20
21
  if (isSubmitting) {
21
22
  return true;
@@ -72,6 +73,7 @@ export const use_forgot_password_form = ({ dataClient, }) => {
72
73
  }
73
74
  setIsSubmitting(true);
74
75
  setErrors({});
76
+ setIsGoogleOnlyAccount(false);
75
77
  try {
76
78
  const response = await fetch(`${apiBasePath}/forgot_password`, {
77
79
  method: "POST",
@@ -86,6 +88,11 @@ export const use_forgot_password_form = ({ dataClient, }) => {
86
88
  if (!response.ok) {
87
89
  throw new Error(data.error || "Password reset request failed");
88
90
  }
91
+ // Check if user is a Google-only account (no password set)
92
+ if (data.no_password_set) {
93
+ setIsGoogleOnlyAccount(true);
94
+ return;
95
+ }
89
96
  // Show success notification
90
97
  toast.success("Password reset link sent", {
91
98
  description: "If an account with that email exists, a password reset link has been sent.",
@@ -121,6 +128,7 @@ export const use_forgot_password_form = ({ dataClient, }) => {
121
128
  isSubmitDisabled,
122
129
  isSubmitting,
123
130
  emailTouched,
131
+ isGoogleOnlyAccount,
124
132
  handleFieldChange,
125
133
  handleEmailBlur,
126
134
  handleSubmit,
@@ -16,6 +16,12 @@ export type ForgotPasswordLayoutProps<TClient = unknown> = {
16
16
  showReturnHomeButton?: boolean;
17
17
  returnHomeButtonLabel?: string;
18
18
  returnHomePath?: string;
19
+ /** Message shown when user's account is Google-only (no password set) */
20
+ googleOnlyAccountHeading?: string;
21
+ googleOnlyAccountMessage?: string;
22
+ googleOnlyAccountHelpText?: string;
23
+ mySettingsPath?: string;
24
+ mySettingsLabel?: string;
19
25
  };
20
- export default function forgot_password_layout<TClient>({ image_src, image_alt, image_background_color, field_overrides, labels, button_colors, data_client, sign_in_path, sign_in_label, alreadyLoggedInMessage, showLogoutButton, showReturnHomeButton, returnHomeButtonLabel, returnHomePath, }: ForgotPasswordLayoutProps<TClient>): import("react/jsx-runtime").JSX.Element;
26
+ export default function forgot_password_layout<TClient>({ image_src, image_alt, image_background_color, field_overrides, labels, button_colors, data_client, sign_in_path, sign_in_label, alreadyLoggedInMessage, showLogoutButton, showReturnHomeButton, returnHomeButtonLabel, returnHomePath, googleOnlyAccountHeading, googleOnlyAccountMessage, googleOnlyAccountHelpText, mySettingsPath, mySettingsLabel, }: ForgotPasswordLayoutProps<TClient>): import("react/jsx-runtime").JSX.Element;
21
27
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/layouts/forgot_password/index.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAMlD,OAAO,EACL,KAAK,sBAAsB,EAC3B,KAAK,uBAAuB,EAC5B,KAAK,oBAAoB,EAC1B,MAAM,uCAAuC,CAAC;AAW/C,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AAI1E,MAAM,MAAM,yBAAyB,CAAC,OAAO,GAAG,OAAO,IAAI;IACzD,SAAS,EAAE,MAAM,GAAG,eAAe,CAAC;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,eAAe,CAAC,EAAE,uBAAuB,CAAC;IAC1C,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAC9B,aAAa,CAAC,EAAE,sBAAsB,CAAC;IACvC,WAAW,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAC;IACvC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB,CAAC;AASF,MAAM,CAAC,OAAO,UAAU,sBAAsB,CAAC,OAAO,EAAE,EACtD,SAAS,EACT,SAAS,EACT,sBAAkC,EAClC,eAAe,EACf,MAAM,EACN,aAAa,EACb,WAAW,EACX,YAAiC,EACjC,aAAyB,EACzB,sBAAoD,EACpD,gBAAuB,EACvB,oBAA4B,EAC5B,qBAAqC,EACrC,cAAoB,GACrB,EAAE,yBAAyB,CAAC,OAAO,CAAC,2CA+GpC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../../src/components/layouts/forgot_password/index.tsx"],"names":[],"mappings":"AAMA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,YAAY,CAAC;AAOlD,OAAO,EACL,KAAK,sBAAsB,EAC3B,KAAK,uBAAuB,EAC5B,KAAK,oBAAoB,EAC1B,MAAM,uCAAuC,CAAC;AAW/C,OAAO,EAAE,KAAK,gBAAgB,EAAE,MAAM,mCAAmC,CAAC;AAI1E,MAAM,MAAM,yBAAyB,CAAC,OAAO,GAAG,OAAO,IAAI;IACzD,SAAS,EAAE,MAAM,GAAG,eAAe,CAAC;IACpC,SAAS,EAAE,MAAM,CAAC;IAClB,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,eAAe,CAAC,EAAE,uBAAuB,CAAC;IAC1C,MAAM,CAAC,EAAE,oBAAoB,CAAC;IAC9B,aAAa,CAAC,EAAE,sBAAsB,CAAC;IACvC,WAAW,EAAE,gBAAgB,CAAC,OAAO,CAAC,CAAC;IACvC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,sBAAsB,CAAC,EAAE,MAAM,CAAC;IAChC,gBAAgB,CAAC,EAAE,OAAO,CAAC;IAC3B,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,qBAAqB,CAAC,EAAE,MAAM,CAAC;IAC/B,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,yEAAyE;IACzE,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,wBAAwB,CAAC,EAAE,MAAM,CAAC;IAClC,yBAAyB,CAAC,EAAE,MAAM,CAAC;IACnC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B,CAAC;AASF,MAAM,CAAC,OAAO,UAAU,sBAAsB,CAAC,OAAO,EAAE,EACtD,SAAS,EACT,SAAS,EACT,sBAAkC,EAClC,eAAe,EACf,MAAM,EACN,aAAa,EACb,WAAW,EACX,YAAiC,EACjC,aAAyB,EACzB,sBAAoD,EACpD,gBAAuB,EACvB,oBAA4B,EAC5B,qBAAqC,EACrC,cAAoB,EACpB,wBAAoD,EACpD,wBAA2G,EAC3G,yBAA0I,EAC1I,cAAyC,EACzC,eAAkC,GACnC,EAAE,yBAAyB,CAAC,OAAO,CAAC,2CAgJpC"}