@progalaxyelabs/ngx-stonescriptphp-client 1.3.1 → 1.5.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.
@@ -1,6 +1,7 @@
1
1
  import * as i0 from '@angular/core';
2
- import { Injectable, Inject, NgModule, Input, Component, EventEmitter, Output, Optional } from '@angular/core';
2
+ import { Injectable, Inject, NgModule, EventEmitter, Output, Input, Component, Optional } from '@angular/core';
3
3
  import { BehaviorSubject } from 'rxjs';
4
+ import * as i2$1 from '@angular/common';
4
5
  import { CommonModule } from '@angular/common';
5
6
  import * as i2 from '@angular/forms';
6
7
  import { FormsModule } from '@angular/forms';
@@ -165,10 +166,23 @@ class MyEnvironmentModel {
165
166
  */
166
167
  platformCode = '';
167
168
  /**
168
- * Accounts platform URL for centralized authentication
169
+ * Accounts platform URL for centralized authentication (single-server mode)
169
170
  * @example 'https://accounts.progalaxyelabs.com'
171
+ * @deprecated Use authServers for multi-server support
170
172
  */
171
173
  accountsUrl = '';
174
+ /**
175
+ * Multiple authentication servers configuration
176
+ * Enables platforms to authenticate against different identity providers
177
+ * @example
178
+ * ```typescript
179
+ * authServers: {
180
+ * customer: { url: 'https://auth.progalaxyelabs.com', default: true },
181
+ * employee: { url: 'https://admin-auth.progalaxyelabs.com' }
182
+ * }
183
+ * ```
184
+ */
185
+ authServers;
172
186
  firebase = {
173
187
  projectId: '',
174
188
  appId: '',
@@ -194,6 +208,11 @@ class MyEnvironmentModel {
194
208
  csrfTokenCookieName: 'csrf_token',
195
209
  csrfHeaderName: 'X-CSRF-Token'
196
210
  };
211
+ /**
212
+ * Branding configuration for auth components
213
+ * Allows platforms to customize login/register pages without creating wrappers
214
+ */
215
+ branding;
197
216
  }
198
217
 
199
218
  /**
@@ -577,15 +596,157 @@ class AuthService {
577
596
  signinStatus;
578
597
  environment;
579
598
  USER_STORAGE_KEY = 'progalaxyapi_user';
599
+ ACTIVE_AUTH_SERVER_KEY = 'progalaxyapi_active_auth_server';
580
600
  // Observable user state
581
601
  userSubject = new BehaviorSubject(null);
582
602
  user$ = this.userSubject.asObservable();
603
+ // Current active auth server name (for multi-server mode)
604
+ activeAuthServer = null;
583
605
  constructor(tokens, signinStatus, environment) {
584
606
  this.tokens = tokens;
585
607
  this.signinStatus = signinStatus;
586
608
  this.environment = environment;
587
609
  // Restore user from localStorage on initialization
588
610
  this.restoreUser();
611
+ // Restore active auth server
612
+ this.restoreActiveAuthServer();
613
+ }
614
+ // ===== Multi-Server Support Methods =====
615
+ /**
616
+ * Get the current accounts URL based on configuration
617
+ * Supports both single-server (accountsUrl) and multi-server (authServers) modes
618
+ * @param serverName - Optional server name for multi-server mode
619
+ */
620
+ getAccountsUrl(serverName) {
621
+ // Multi-server mode
622
+ if (this.environment.authServers && Object.keys(this.environment.authServers).length > 0) {
623
+ const targetServer = serverName || this.activeAuthServer || this.getDefaultAuthServer();
624
+ if (!targetServer) {
625
+ throw new Error('No auth server specified and no default server configured');
626
+ }
627
+ const serverConfig = this.environment.authServers[targetServer];
628
+ if (!serverConfig) {
629
+ throw new Error(`Auth server '${targetServer}' not found in configuration`);
630
+ }
631
+ return serverConfig.url;
632
+ }
633
+ // Single-server mode (backward compatibility)
634
+ if (this.environment.accountsUrl) {
635
+ return this.environment.accountsUrl;
636
+ }
637
+ throw new Error('No authentication server configured. Set either accountsUrl or authServers in environment config.');
638
+ }
639
+ /**
640
+ * Get the default auth server name
641
+ */
642
+ getDefaultAuthServer() {
643
+ if (!this.environment.authServers) {
644
+ return null;
645
+ }
646
+ // Find server marked as default
647
+ for (const [name, config] of Object.entries(this.environment.authServers)) {
648
+ if (config.default) {
649
+ return name;
650
+ }
651
+ }
652
+ // If no default is marked, use the first server
653
+ const firstServer = Object.keys(this.environment.authServers)[0];
654
+ return firstServer || null;
655
+ }
656
+ /**
657
+ * Restore active auth server from localStorage
658
+ */
659
+ restoreActiveAuthServer() {
660
+ try {
661
+ const savedServer = localStorage.getItem(this.ACTIVE_AUTH_SERVER_KEY);
662
+ if (savedServer && this.environment.authServers?.[savedServer]) {
663
+ this.activeAuthServer = savedServer;
664
+ }
665
+ else {
666
+ // Set to default if saved server is invalid
667
+ this.activeAuthServer = this.getDefaultAuthServer();
668
+ }
669
+ }
670
+ catch (error) {
671
+ console.error('Failed to restore active auth server:', error);
672
+ this.activeAuthServer = this.getDefaultAuthServer();
673
+ }
674
+ }
675
+ /**
676
+ * Save active auth server to localStorage
677
+ */
678
+ saveActiveAuthServer(serverName) {
679
+ try {
680
+ localStorage.setItem(this.ACTIVE_AUTH_SERVER_KEY, serverName);
681
+ this.activeAuthServer = serverName;
682
+ }
683
+ catch (error) {
684
+ console.error('Failed to save active auth server:', error);
685
+ }
686
+ }
687
+ /**
688
+ * Get available auth servers
689
+ * @returns Array of server names or empty array if using single-server mode
690
+ */
691
+ getAvailableAuthServers() {
692
+ if (!this.environment.authServers) {
693
+ return [];
694
+ }
695
+ return Object.keys(this.environment.authServers);
696
+ }
697
+ /**
698
+ * Get current active auth server name
699
+ * @returns Server name or null if using single-server mode
700
+ */
701
+ getActiveAuthServer() {
702
+ return this.activeAuthServer;
703
+ }
704
+ /**
705
+ * Switch to a different auth server
706
+ * @param serverName - Name of the server to switch to
707
+ * @throws Error if server not found in configuration
708
+ */
709
+ switchAuthServer(serverName) {
710
+ if (!this.environment.authServers) {
711
+ throw new Error('Multi-server mode not configured. Use authServers in environment config.');
712
+ }
713
+ if (!this.environment.authServers[serverName]) {
714
+ throw new Error(`Auth server '${serverName}' not found in configuration`);
715
+ }
716
+ this.saveActiveAuthServer(serverName);
717
+ }
718
+ /**
719
+ * Get auth server configuration
720
+ * @param serverName - Optional server name (uses active server if not specified)
721
+ */
722
+ getAuthServerConfig(serverName) {
723
+ if (!this.environment.authServers) {
724
+ return null;
725
+ }
726
+ const targetServer = serverName || this.activeAuthServer || this.getDefaultAuthServer();
727
+ if (!targetServer) {
728
+ return null;
729
+ }
730
+ return this.environment.authServers[targetServer] || null;
731
+ }
732
+ /**
733
+ * Check if multi-server mode is enabled
734
+ */
735
+ isMultiServerMode() {
736
+ return !!(this.environment.authServers && Object.keys(this.environment.authServers).length > 0);
737
+ }
738
+ /**
739
+ * Hash UUID to numeric ID for backward compatibility
740
+ * Converts UUID string to a consistent numeric ID for legacy code
741
+ */
742
+ hashUUID(uuid) {
743
+ let hash = 0;
744
+ for (let i = 0; i < uuid.length; i++) {
745
+ const char = uuid.charCodeAt(i);
746
+ hash = ((hash << 5) - hash) + char;
747
+ hash = hash & hash; // Convert to 32bit integer
748
+ }
749
+ return Math.abs(hash);
589
750
  }
590
751
  /**
591
752
  * Restore user from localStorage
@@ -622,15 +783,19 @@ class AuthService {
622
783
  * Update user subject and persist to localStorage
623
784
  */
624
785
  updateUser(user) {
625
- this.updateUser(user);
786
+ this.userSubject.next(user);
626
787
  this.saveUser(user);
627
788
  }
628
789
  /**
629
790
  * Login with email and password
791
+ * @param email - User email
792
+ * @param password - User password
793
+ * @param serverName - Optional: Specify which auth server to use (for multi-server mode)
630
794
  */
631
- async loginWithEmail(email, password) {
795
+ async loginWithEmail(email, password, serverName) {
632
796
  try {
633
- const response = await fetch(`${this.environment.accountsUrl}/api/auth/login`, {
797
+ const accountsUrl = this.getAccountsUrl(serverName);
798
+ const response = await fetch(`${accountsUrl}/api/auth/login`, {
634
799
  method: 'POST',
635
800
  headers: { 'Content-Type': 'application/json' },
636
801
  credentials: 'include', // Include cookies for refresh token
@@ -644,8 +809,17 @@ class AuthService {
644
809
  if (data.success && data.access_token) {
645
810
  this.tokens.setAccessToken(data.access_token);
646
811
  this.signinStatus.setSigninStatus(true);
647
- this.updateUser(data.user);
648
- return { success: true, user: data.user };
812
+ // Normalize user object to handle both response formats
813
+ const normalizedUser = {
814
+ user_id: data.user.user_id ?? (data.user.id ? this.hashUUID(data.user.id) : 0),
815
+ id: data.user.id ?? String(data.user.user_id),
816
+ email: data.user.email,
817
+ display_name: data.user.display_name ?? data.user.email.split('@')[0],
818
+ photo_url: data.user.photo_url,
819
+ is_email_verified: data.user.is_email_verified ?? false
820
+ };
821
+ this.updateUser(normalizedUser);
822
+ return { success: true, user: normalizedUser };
649
823
  }
650
824
  return {
651
825
  success: false,
@@ -661,55 +835,71 @@ class AuthService {
661
835
  }
662
836
  /**
663
837
  * Login with Google OAuth (popup window)
838
+ * @param serverName - Optional: Specify which auth server to use (for multi-server mode)
664
839
  */
665
- async loginWithGoogle() {
666
- return this.loginWithOAuth('google');
840
+ async loginWithGoogle(serverName) {
841
+ return this.loginWithOAuth('google', serverName);
667
842
  }
668
843
  /**
669
844
  * Login with GitHub OAuth (popup window)
845
+ * @param serverName - Optional: Specify which auth server to use (for multi-server mode)
670
846
  */
671
- async loginWithGitHub() {
672
- return this.loginWithOAuth('github');
847
+ async loginWithGitHub(serverName) {
848
+ return this.loginWithOAuth('github', serverName);
673
849
  }
674
850
  /**
675
851
  * Login with LinkedIn OAuth (popup window)
852
+ * @param serverName - Optional: Specify which auth server to use (for multi-server mode)
676
853
  */
677
- async loginWithLinkedIn() {
678
- return this.loginWithOAuth('linkedin');
854
+ async loginWithLinkedIn(serverName) {
855
+ return this.loginWithOAuth('linkedin', serverName);
679
856
  }
680
857
  /**
681
858
  * Login with Apple OAuth (popup window)
859
+ * @param serverName - Optional: Specify which auth server to use (for multi-server mode)
682
860
  */
683
- async loginWithApple() {
684
- return this.loginWithOAuth('apple');
861
+ async loginWithApple(serverName) {
862
+ return this.loginWithOAuth('apple', serverName);
685
863
  }
686
864
  /**
687
865
  * Login with Microsoft OAuth (popup window)
866
+ * @param serverName - Optional: Specify which auth server to use (for multi-server mode)
867
+ */
868
+ async loginWithMicrosoft(serverName) {
869
+ return this.loginWithOAuth('microsoft', serverName);
870
+ }
871
+ /**
872
+ * Login with Zoho OAuth (popup window)
873
+ * @param serverName - Optional: Specify which auth server to use (for multi-server mode)
688
874
  */
689
- async loginWithMicrosoft() {
690
- return this.loginWithOAuth('microsoft');
875
+ async loginWithZoho(serverName) {
876
+ return this.loginWithOAuth('zoho', serverName);
691
877
  }
692
878
  /**
693
879
  * Generic provider-based login (supports all OAuth providers)
694
880
  * @param provider - The provider identifier
881
+ * @param serverName - Optional: Specify which auth server to use (for multi-server mode)
695
882
  */
696
- async loginWithProvider(provider) {
883
+ async loginWithProvider(provider, serverName) {
697
884
  if (provider === 'emailPassword') {
698
885
  throw new Error('Use loginWithEmail() for email/password authentication');
699
886
  }
700
- return this.loginWithOAuth(provider);
887
+ return this.loginWithOAuth(provider, serverName);
701
888
  }
702
889
  /**
703
890
  * Generic OAuth login handler
704
891
  * Opens popup window and listens for postMessage
892
+ * @param provider - OAuth provider name
893
+ * @param serverName - Optional: Specify which auth server to use (for multi-server mode)
705
894
  */
706
- async loginWithOAuth(provider) {
895
+ async loginWithOAuth(provider, serverName) {
707
896
  return new Promise((resolve) => {
708
897
  const width = 500;
709
898
  const height = 600;
710
899
  const left = (window.screen.width - width) / 2;
711
900
  const top = (window.screen.height - height) / 2;
712
- const oauthUrl = `${this.environment.accountsUrl}/oauth/${provider}?` +
901
+ const accountsUrl = this.getAccountsUrl(serverName);
902
+ const oauthUrl = `${accountsUrl}/oauth/${provider}?` +
713
903
  `platform=${this.environment.platformCode}&` +
714
904
  `mode=popup`;
715
905
  const popup = window.open(oauthUrl, `${provider}_login`, `width=${width},height=${height},left=${left},top=${top}`);
@@ -723,18 +913,27 @@ class AuthService {
723
913
  // Listen for message from popup
724
914
  const messageHandler = (event) => {
725
915
  // Verify origin
726
- if (event.origin !== new URL(this.environment.accountsUrl).origin) {
916
+ if (event.origin !== new URL(accountsUrl).origin) {
727
917
  return;
728
918
  }
729
919
  if (event.data.type === 'oauth_success') {
730
920
  this.tokens.setAccessToken(event.data.access_token);
731
921
  this.signinStatus.setSigninStatus(true);
732
- this.updateUser(event.data.user);
922
+ // Normalize user object to handle both response formats
923
+ const normalizedUser = {
924
+ user_id: event.data.user.user_id ?? (event.data.user.id ? this.hashUUID(event.data.user.id) : 0),
925
+ id: event.data.user.id ?? String(event.data.user.user_id),
926
+ email: event.data.user.email,
927
+ display_name: event.data.user.display_name ?? event.data.user.email.split('@')[0],
928
+ photo_url: event.data.user.photo_url,
929
+ is_email_verified: event.data.user.is_email_verified ?? false
930
+ };
931
+ this.updateUser(normalizedUser);
733
932
  window.removeEventListener('message', messageHandler);
734
933
  popup.close();
735
934
  resolve({
736
935
  success: true,
737
- user: event.data.user
936
+ user: normalizedUser
738
937
  });
739
938
  }
740
939
  else if (event.data.type === 'oauth_error') {
@@ -762,10 +961,15 @@ class AuthService {
762
961
  }
763
962
  /**
764
963
  * Register new user
964
+ * @param email - User email
965
+ * @param password - User password
966
+ * @param displayName - Display name
967
+ * @param serverName - Optional: Specify which auth server to use (for multi-server mode)
765
968
  */
766
- async register(email, password, displayName) {
969
+ async register(email, password, displayName, serverName) {
767
970
  try {
768
- const response = await fetch(`${this.environment.accountsUrl}/api/auth/register`, {
971
+ const accountsUrl = this.getAccountsUrl(serverName);
972
+ const response = await fetch(`${accountsUrl}/api/auth/register`, {
769
973
  method: 'POST',
770
974
  headers: { 'Content-Type': 'application/json' },
771
975
  credentials: 'include',
@@ -780,10 +984,19 @@ class AuthService {
780
984
  if (data.success && data.access_token) {
781
985
  this.tokens.setAccessToken(data.access_token);
782
986
  this.signinStatus.setSigninStatus(true);
783
- this.updateUser(data.user);
987
+ // Normalize user object to handle both response formats
988
+ const normalizedUser = {
989
+ user_id: data.user.user_id ?? (data.user.id ? this.hashUUID(data.user.id) : 0),
990
+ id: data.user.id ?? String(data.user.user_id),
991
+ email: data.user.email,
992
+ display_name: data.user.display_name ?? data.user.email.split('@')[0],
993
+ photo_url: data.user.photo_url,
994
+ is_email_verified: data.user.is_email_verified ?? false
995
+ };
996
+ this.updateUser(normalizedUser);
784
997
  return {
785
998
  success: true,
786
- user: data.user,
999
+ user: normalizedUser,
787
1000
  message: data.needs_verification ? 'Please verify your email' : undefined
788
1001
  };
789
1002
  }
@@ -801,12 +1014,14 @@ class AuthService {
801
1014
  }
802
1015
  /**
803
1016
  * Sign out user
1017
+ * @param serverName - Optional: Specify which auth server to logout from (for multi-server mode)
804
1018
  */
805
- async signout() {
1019
+ async signout(serverName) {
806
1020
  try {
807
1021
  const refreshToken = this.tokens.getRefreshToken();
808
1022
  if (refreshToken) {
809
- await fetch(`${this.environment.accountsUrl}/api/auth/logout`, {
1023
+ const accountsUrl = this.getAccountsUrl(serverName);
1024
+ await fetch(`${accountsUrl}/api/auth/logout`, {
810
1025
  method: 'POST',
811
1026
  headers: {
812
1027
  'Content-Type': 'application/json'
@@ -829,15 +1044,17 @@ class AuthService {
829
1044
  }
830
1045
  /**
831
1046
  * Check for active session (call on app init)
1047
+ * @param serverName - Optional: Specify which auth server to check (for multi-server mode)
832
1048
  */
833
- async checkSession() {
1049
+ async checkSession(serverName) {
834
1050
  if (this.tokens.hasValidAccessToken()) {
835
1051
  this.signinStatus.setSigninStatus(true);
836
1052
  return true;
837
1053
  }
838
1054
  // Try to refresh using httpOnly cookie
839
1055
  try {
840
- const response = await fetch(`${this.environment.accountsUrl}/api/auth/refresh`, {
1056
+ const accountsUrl = this.getAccountsUrl(serverName);
1057
+ const response = await fetch(`${accountsUrl}/api/auth/refresh`, {
841
1058
  method: 'POST',
842
1059
  credentials: 'include'
843
1060
  });
@@ -848,7 +1065,18 @@ class AuthService {
848
1065
  const data = await response.json();
849
1066
  if (data.access_token) {
850
1067
  this.tokens.setAccessToken(data.access_token);
851
- this.updateUser(data.user);
1068
+ // Normalize user object to handle both response formats
1069
+ if (data.user) {
1070
+ const normalizedUser = {
1071
+ user_id: data.user.user_id ?? (data.user.id ? this.hashUUID(data.user.id) : 0),
1072
+ id: data.user.id ?? String(data.user.user_id),
1073
+ email: data.user.email,
1074
+ display_name: data.user.display_name ?? data.user.email.split('@')[0],
1075
+ photo_url: data.user.photo_url,
1076
+ is_email_verified: data.user.is_email_verified ?? false
1077
+ };
1078
+ this.updateUser(normalizedUser);
1079
+ }
852
1080
  this.signinStatus.setSigninStatus(true);
853
1081
  return true;
854
1082
  }
@@ -883,7 +1111,8 @@ class AuthService {
883
1111
  return await this.registerTenantWithOAuth(data.tenantName, data.tenantSlug, data.provider);
884
1112
  }
885
1113
  // Email/password registration
886
- const response = await fetch(`${this.environment.accountsUrl}/api/auth/register-tenant`, {
1114
+ const accountsUrl = this.getAccountsUrl();
1115
+ const response = await fetch(`${accountsUrl}/api/auth/register-tenant`, {
887
1116
  method: 'POST',
888
1117
  headers: { 'Content-Type': 'application/json' },
889
1118
  credentials: 'include',
@@ -902,7 +1131,16 @@ class AuthService {
902
1131
  this.tokens.setAccessToken(result.access_token);
903
1132
  this.signinStatus.setSigninStatus(true);
904
1133
  if (result.user) {
905
- this.updateUser(result.user);
1134
+ // Normalize user object to handle both response formats
1135
+ const normalizedUser = {
1136
+ user_id: result.user.user_id ?? (result.user.id ? this.hashUUID(result.user.id) : 0),
1137
+ id: result.user.id ?? String(result.user.user_id),
1138
+ email: result.user.email,
1139
+ display_name: result.user.display_name ?? result.user.email.split('@')[0],
1140
+ photo_url: result.user.photo_url,
1141
+ is_email_verified: result.user.is_email_verified ?? false
1142
+ };
1143
+ this.updateUser(normalizedUser);
906
1144
  }
907
1145
  }
908
1146
  return result;
@@ -925,7 +1163,8 @@ class AuthService {
925
1163
  const left = (window.screen.width - width) / 2;
926
1164
  const top = (window.screen.height - height) / 2;
927
1165
  // Build OAuth URL with tenant registration params
928
- const oauthUrl = `${this.environment.accountsUrl}/oauth/${provider}?` +
1166
+ const accountsUrl = this.getAccountsUrl();
1167
+ const oauthUrl = `${accountsUrl}/oauth/${provider}?` +
929
1168
  `platform=${this.environment.platformCode}&` +
930
1169
  `mode=popup&` +
931
1170
  `action=register_tenant&` +
@@ -942,7 +1181,7 @@ class AuthService {
942
1181
  // Listen for message from popup
943
1182
  const messageHandler = (event) => {
944
1183
  // Verify origin
945
- if (event.origin !== new URL(this.environment.accountsUrl).origin) {
1184
+ if (event.origin !== new URL(accountsUrl).origin) {
946
1185
  return;
947
1186
  }
948
1187
  if (event.data.type === 'tenant_register_success') {
@@ -952,7 +1191,16 @@ class AuthService {
952
1191
  this.signinStatus.setSigninStatus(true);
953
1192
  }
954
1193
  if (event.data.user) {
955
- this.updateUser(event.data.user);
1194
+ // Normalize user object to handle both response formats
1195
+ const normalizedUser = {
1196
+ user_id: event.data.user.user_id ?? (event.data.user.id ? this.hashUUID(event.data.user.id) : 0),
1197
+ id: event.data.user.id ?? String(event.data.user.user_id),
1198
+ email: event.data.user.email,
1199
+ display_name: event.data.user.display_name ?? event.data.user.email.split('@')[0],
1200
+ photo_url: event.data.user.photo_url,
1201
+ is_email_verified: event.data.user.is_email_verified ?? false
1202
+ };
1203
+ this.updateUser(normalizedUser);
956
1204
  }
957
1205
  window.removeEventListener('message', messageHandler);
958
1206
  popup.close();
@@ -987,10 +1235,12 @@ class AuthService {
987
1235
  }
988
1236
  /**
989
1237
  * Get all tenant memberships for the authenticated user
1238
+ * @param serverName - Optional: Specify which auth server to query (for multi-server mode)
990
1239
  */
991
- async getTenantMemberships() {
1240
+ async getTenantMemberships(serverName) {
992
1241
  try {
993
- const response = await fetch(`${this.environment.accountsUrl}/api/auth/memberships`, {
1242
+ const accountsUrl = this.getAccountsUrl(serverName);
1243
+ const response = await fetch(`${accountsUrl}/api/auth/memberships`, {
994
1244
  method: 'GET',
995
1245
  headers: {
996
1246
  'Authorization': `Bearer ${this.tokens.getAccessToken()}`,
@@ -1010,10 +1260,13 @@ class AuthService {
1010
1260
  /**
1011
1261
  * Select a tenant for the current session
1012
1262
  * Updates the JWT token with tenant context
1263
+ * @param tenantId - Tenant ID to select
1264
+ * @param serverName - Optional: Specify which auth server to use (for multi-server mode)
1013
1265
  */
1014
- async selectTenant(tenantId) {
1266
+ async selectTenant(tenantId, serverName) {
1015
1267
  try {
1016
- const response = await fetch(`${this.environment.accountsUrl}/api/auth/select-tenant`, {
1268
+ const accountsUrl = this.getAccountsUrl(serverName);
1269
+ const response = await fetch(`${accountsUrl}/api/auth/select-tenant`, {
1017
1270
  method: 'POST',
1018
1271
  headers: {
1019
1272
  'Authorization': `Bearer ${this.tokens.getAccessToken()}`,
@@ -1044,10 +1297,13 @@ class AuthService {
1044
1297
  }
1045
1298
  /**
1046
1299
  * Check if a tenant slug is available
1300
+ * @param slug - Tenant slug to check
1301
+ * @param serverName - Optional: Specify which auth server to query (for multi-server mode)
1047
1302
  */
1048
- async checkTenantSlugAvailable(slug) {
1303
+ async checkTenantSlugAvailable(slug, serverName) {
1049
1304
  try {
1050
- const response = await fetch(`${this.environment.accountsUrl}/api/auth/check-tenant-slug/${slug}`, {
1305
+ const accountsUrl = this.getAccountsUrl(serverName);
1306
+ const response = await fetch(`${accountsUrl}/api/auth/check-tenant-slug/${slug}`, {
1051
1307
  method: 'GET',
1052
1308
  headers: { 'Content-Type': 'application/json' }
1053
1309
  });
@@ -1130,9 +1386,10 @@ class AuthService {
1130
1386
  /**
1131
1387
  * @deprecated Check if user exists by calling /api/auth/check-email endpoint
1132
1388
  */
1133
- async getUserProfile(email) {
1389
+ async getUserProfile(email, serverName) {
1134
1390
  try {
1135
- const response = await fetch(`${this.environment.accountsUrl}/api/auth/check-email`, {
1391
+ const accountsUrl = this.getAccountsUrl(serverName);
1392
+ const response = await fetch(`${accountsUrl}/api/auth/check-email`, {
1136
1393
  method: 'POST',
1137
1394
  headers: { 'Content-Type': 'application/json' },
1138
1395
  body: JSON.stringify({ email })
@@ -1146,10 +1403,13 @@ class AuthService {
1146
1403
  }
1147
1404
  /**
1148
1405
  * Check if user has completed onboarding (has a tenant)
1406
+ * @param identityId - User identity ID
1407
+ * @param serverName - Optional: Specify which auth server to query (for multi-server mode)
1149
1408
  */
1150
- async checkOnboardingStatus(identityId) {
1409
+ async checkOnboardingStatus(identityId, serverName) {
1151
1410
  try {
1152
- const response = await fetch(`${this.environment.accountsUrl}/api/auth/onboarding/status?platform_code=${this.environment.platformCode}&identity_id=${identityId}`, {
1411
+ const accountsUrl = this.getAccountsUrl(serverName);
1412
+ const response = await fetch(`${accountsUrl}/api/auth/onboarding/status?platform_code=${this.environment.platformCode}&identity_id=${identityId}`, {
1153
1413
  method: 'GET',
1154
1414
  headers: { 'Content-Type': 'application/json' },
1155
1415
  credentials: 'include'
@@ -1165,14 +1425,18 @@ class AuthService {
1165
1425
  }
1166
1426
  /**
1167
1427
  * Complete tenant onboarding (create tenant with country + org name)
1428
+ * @param countryCode - Country code
1429
+ * @param tenantName - Tenant organization name
1430
+ * @param serverName - Optional: Specify which auth server to use (for multi-server mode)
1168
1431
  */
1169
- async completeTenantOnboarding(countryCode, tenantName) {
1432
+ async completeTenantOnboarding(countryCode, tenantName, serverName) {
1170
1433
  try {
1171
1434
  const accessToken = this.tokens.getAccessToken();
1172
1435
  if (!accessToken) {
1173
1436
  throw new Error('Not authenticated');
1174
1437
  }
1175
- const response = await fetch(`${this.environment.accountsUrl}/api/auth/register-tenant`, {
1438
+ const accountsUrl = this.getAccountsUrl(serverName);
1439
+ const response = await fetch(`${accountsUrl}/api/auth/register-tenant`, {
1176
1440
  method: 'POST',
1177
1441
  headers: {
1178
1442
  'Content-Type': 'application/json',
@@ -1251,29 +1515,59 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
1251
1515
  }]
1252
1516
  }] });
1253
1517
 
1254
- class LoginDialogComponent {
1518
+ class TenantLoginComponent {
1255
1519
  auth;
1256
- /**
1257
- * REQUIRED: Which authentication providers to show in this dialog
1258
- * @example ['google', 'linkedin', 'emailPassword']
1259
- */
1260
- providers = [];
1520
+ // Component Configuration
1521
+ title = 'Sign In';
1522
+ providers = ['google'];
1523
+ showTenantSelector = true;
1524
+ autoSelectSingleTenant = true;
1525
+ prefillEmail; // Email to prefill (for account linking flow)
1526
+ allowTenantCreation = true;
1527
+ // Tenant Selector Labels
1528
+ tenantSelectorTitle = 'Select Organization';
1529
+ tenantSelectorDescription = 'Choose which organization you want to access:';
1530
+ continueButtonText = 'Continue';
1531
+ // Link Labels
1532
+ registerLinkText = "Don't have an account?";
1533
+ registerLinkAction = 'Sign up';
1534
+ createTenantLinkText = "Don't see your organization?";
1535
+ createTenantLinkAction = 'Create New Organization';
1536
+ // Outputs
1537
+ tenantSelected = new EventEmitter();
1538
+ createTenant = new EventEmitter();
1539
+ // Form Fields
1261
1540
  email = '';
1262
1541
  password = '';
1542
+ // State
1263
1543
  error = '';
1264
1544
  loading = false;
1545
+ showPassword = false;
1546
+ useOAuth = true;
1265
1547
  oauthProviders = [];
1548
+ // Tenant Selection State
1549
+ showingTenantSelector = false;
1550
+ memberships = [];
1551
+ selectedTenantId = null;
1552
+ userName = '';
1266
1553
  constructor(auth) {
1267
1554
  this.auth = auth;
1268
1555
  }
1269
1556
  ngOnInit() {
1270
1557
  if (!this.providers || this.providers.length === 0) {
1271
- this.error = 'Configuration Error: No authentication providers specified. Please pass providers to LoginDialogComponent.';
1272
- throw new Error('LoginDialogComponent requires providers input. Example: dialogRef.componentInstance.providers = [\'google\', \'emailPassword\']');
1558
+ this.error = 'Configuration Error: No authentication providers specified.';
1559
+ throw new Error('TenantLoginComponent requires providers input.');
1560
+ }
1561
+ this.oauthProviders = this.providers.filter(p => p !== 'emailPassword');
1562
+ // If only emailPassword is available, use it by default
1563
+ if (this.oauthProviders.length === 0 && this.isProviderEnabled('emailPassword')) {
1564
+ this.useOAuth = false;
1565
+ }
1566
+ // Prefill email if provided (for account linking flow)
1567
+ if (this.prefillEmail) {
1568
+ this.email = this.prefillEmail;
1569
+ this.useOAuth = false; // Switch to email/password form
1273
1570
  }
1274
- // Get OAuth providers (excluding emailPassword)
1275
- this.oauthProviders = this.providers
1276
- .filter(p => p !== 'emailPassword');
1277
1571
  }
1278
1572
  isProviderEnabled(provider) {
1279
1573
  return this.providers.includes(provider);
@@ -1285,13 +1579,21 @@ class LoginDialogComponent {
1285
1579
  apple: 'Sign in with Apple',
1286
1580
  microsoft: 'Sign in with Microsoft',
1287
1581
  github: 'Sign in with GitHub',
1582
+ zoho: 'Sign in with Zoho',
1288
1583
  emailPassword: 'Sign in with Email'
1289
1584
  };
1290
1585
  return labels[provider];
1291
1586
  }
1292
1587
  getProviderIcon(provider) {
1293
- // Platforms can customize icons via CSS classes: .btn-google, .btn-linkedin, etc.
1294
- return undefined;
1588
+ const icons = {
1589
+ zoho: '🔶'
1590
+ };
1591
+ return icons[provider];
1592
+ }
1593
+ toggleAuthMethod(event) {
1594
+ event.preventDefault();
1595
+ this.useOAuth = !this.useOAuth;
1596
+ this.error = '';
1295
1597
  }
1296
1598
  async onEmailLogin() {
1297
1599
  if (!this.email || !this.password) {
@@ -1304,11 +1606,13 @@ class LoginDialogComponent {
1304
1606
  const result = await this.auth.loginWithEmail(this.email, this.password);
1305
1607
  if (!result.success) {
1306
1608
  this.error = result.message || 'Login failed';
1609
+ return;
1307
1610
  }
1308
- // On success, parent component/dialog should close automatically via user$ subscription
1611
+ // Authentication successful, now handle tenant selection
1612
+ await this.handlePostAuthFlow();
1309
1613
  }
1310
1614
  catch (err) {
1311
- this.error = 'An unexpected error occurred';
1615
+ this.error = err.message || 'An unexpected error occurred';
1312
1616
  }
1313
1617
  finally {
1314
1618
  this.loading = false;
@@ -1321,194 +1625,499 @@ class LoginDialogComponent {
1321
1625
  const result = await this.auth.loginWithProvider(provider);
1322
1626
  if (!result.success) {
1323
1627
  this.error = result.message || 'OAuth login failed';
1628
+ return;
1324
1629
  }
1325
- // On success, parent component/dialog should close automatically via user$ subscription
1630
+ // Authentication successful, now handle tenant selection
1631
+ await this.handlePostAuthFlow();
1326
1632
  }
1327
1633
  catch (err) {
1328
- this.error = 'An unexpected error occurred';
1634
+ this.error = err.message || 'An unexpected error occurred';
1329
1635
  }
1330
1636
  finally {
1331
1637
  this.loading = false;
1332
1638
  }
1333
1639
  }
1334
- onRegisterClick(event) {
1335
- event.preventDefault();
1336
- // Platforms can override this or listen for a custom event
1337
- // For now, just emit a console message
1338
- console.log('Register clicked - platform should handle navigation');
1339
- }
1340
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: LoginDialogComponent, deps: [{ token: AuthService }], target: i0.ɵɵFactoryTarget.Component });
1341
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.15", type: LoginDialogComponent, isStandalone: true, selector: "lib-login-dialog", inputs: { providers: "providers" }, ngImport: i0, template: `
1342
- <div class="login-dialog">
1343
- <h2 class="login-title">Sign In</h2>
1344
-
1345
- <!-- Email/Password Form (if enabled) -->
1346
- @if (isProviderEnabled('emailPassword')) {
1347
- <form (ngSubmit)="onEmailLogin()" class="email-form">
1348
- <div class="form-group">
1349
- <input
1350
- [(ngModel)]="email"
1351
- name="email"
1352
- placeholder="Email"
1353
- type="email"
1354
- required
1355
- class="form-control">
1356
- </div>
1357
- <div class="form-group">
1358
- <input
1359
- [(ngModel)]="password"
1360
- name="password"
1361
- placeholder="Password"
1362
- type="password"
1363
- required
1364
- class="form-control">
1365
- </div>
1366
- <button
1367
- type="submit"
1368
- [disabled]="loading"
1369
- class="btn btn-primary btn-block">
1370
- {{ loading ? 'Signing in...' : getProviderLabel('emailPassword') }}
1371
- </button>
1372
- </form>
1373
- }
1374
-
1375
- <!-- Divider if both email and OAuth are present -->
1376
- @if (isProviderEnabled('emailPassword') && oauthProviders.length > 0) {
1377
- <div class="divider">
1378
- <span>OR</span>
1379
- </div>
1640
+ async handlePostAuthFlow() {
1641
+ if (!this.showTenantSelector) {
1642
+ // Tenant selection is disabled, emit event immediately
1643
+ this.tenantSelected.emit({
1644
+ tenantId: '',
1645
+ tenantSlug: '',
1646
+ role: ''
1647
+ });
1648
+ return;
1649
+ }
1650
+ // Fetch user's tenant memberships
1651
+ this.loading = true;
1652
+ try {
1653
+ const result = await this.auth.getTenantMemberships();
1654
+ if (!result.memberships || result.memberships.length === 0) {
1655
+ // User has no tenants, prompt to create one
1656
+ this.error = 'You are not a member of any organization. Please create one.';
1657
+ if (this.allowTenantCreation) {
1658
+ setTimeout(() => this.createTenant.emit(), 2000);
1659
+ }
1660
+ return;
1380
1661
  }
1381
-
1382
- <!-- OAuth Providers -->
1383
- @if (oauthProviders.length > 0) {
1384
- <div class="oauth-buttons">
1385
- @for (provider of oauthProviders; track provider) {
1386
- <button
1387
- (click)="onOAuthLogin(provider)"
1388
- [disabled]="loading"
1389
- class="btn btn-oauth btn-{{ provider }}">
1390
- @if (getProviderIcon(provider)) {
1391
- <span class="oauth-icon">
1392
- {{ getProviderIcon(provider) }}
1393
- </span>
1394
- }
1395
- {{ getProviderLabel(provider) }}
1396
- </button>
1397
- }
1398
- </div>
1662
+ this.memberships = result.memberships;
1663
+ // Get user name if available
1664
+ const currentUser = this.auth.getCurrentUser();
1665
+ if (currentUser) {
1666
+ this.userName = currentUser.display_name || currentUser.email;
1399
1667
  }
1400
-
1401
- <!-- Error Message -->
1402
- @if (error) {
1403
- <div class="error-message">
1404
- {{ error }}
1405
- </div>
1668
+ // Auto-select if user has only one tenant
1669
+ if (this.memberships.length === 1 && this.autoSelectSingleTenant) {
1670
+ await this.selectAndContinue(this.memberships[0]);
1406
1671
  }
1407
-
1408
- <!-- Loading State -->
1409
- @if (loading) {
1410
- <div class="loading-overlay">
1411
- <div class="spinner"></div>
1412
- </div>
1672
+ else {
1673
+ // Show tenant selector
1674
+ this.showingTenantSelector = true;
1413
1675
  }
1414
-
1415
- <!-- Register Link -->
1416
- <div class="register-link">
1417
- Don't have an account?
1418
- <a href="#" (click)="onRegisterClick($event)">Sign up</a>
1419
- </div>
1420
- </div>
1421
- `, isInline: true, styles: [".login-dialog{padding:24px;max-width:400px;position:relative}.login-title{margin:0 0 24px;font-size:24px;font-weight:500;text-align:center}.email-form,.form-group{margin-bottom:16px}.form-control{width:100%;padding:12px;border:1px solid #ddd;border-radius:4px;font-size:14px;box-sizing:border-box}.form-control:focus{outline:none;border-color:#4285f4}.btn{padding:12px 24px;border:none;border-radius:4px;font-size:14px;font-weight:500;cursor:pointer;transition:background-color .2s}.btn:disabled{opacity:.6;cursor:not-allowed}.btn-block{width:100%}.btn-primary{background-color:#4285f4;color:#fff}.btn-primary:hover:not(:disabled){background-color:#357ae8}.divider{margin:16px 0;text-align:center;position:relative}.divider:before{content:\"\";position:absolute;top:50%;left:0;right:0;height:1px;background:#ddd}.divider span{background:#fff;padding:0 12px;position:relative;color:#666;font-size:12px}.oauth-buttons{display:flex;flex-direction:column;gap:12px}.btn-oauth{width:100%;background:#fff;color:#333;border:1px solid #ddd;display:flex;align-items:center;justify-content:center;gap:8px}.btn-oauth:hover:not(:disabled){background:#f8f8f8}.btn-google{border-color:#4285f4}.btn-linkedin{border-color:#0077b5}.btn-apple{border-color:#000}.btn-microsoft{border-color:#00a4ef}.btn-github{border-color:#333}.oauth-icon{font-size:18px}.error-message{margin-top:16px;padding:12px;background:#fee;color:#c33;border-radius:4px;font-size:14px}.loading-overlay{position:absolute;inset:0;background:#fffc;display:flex;align-items:center;justify-content:center}.spinner{width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #4285f4;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.register-link{margin-top:16px;text-align:center;font-size:14px;color:#666}.register-link a{color:#4285f4;text-decoration:none}.register-link a:hover{text-decoration:underline}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i2.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }] });
1422
- }
1423
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: LoginDialogComponent, decorators: [{
1424
- type: Component,
1425
- args: [{ selector: 'lib-login-dialog', standalone: true, imports: [CommonModule, FormsModule], template: `
1426
- <div class="login-dialog">
1427
- <h2 class="login-title">Sign In</h2>
1428
-
1429
- <!-- Email/Password Form (if enabled) -->
1430
- @if (isProviderEnabled('emailPassword')) {
1431
- <form (ngSubmit)="onEmailLogin()" class="email-form">
1432
- <div class="form-group">
1433
- <input
1434
- [(ngModel)]="email"
1435
- name="email"
1436
- placeholder="Email"
1437
- type="email"
1438
- required
1439
- class="form-control">
1676
+ }
1677
+ catch (err) {
1678
+ this.error = err.message || 'Failed to load organizations';
1679
+ }
1680
+ finally {
1681
+ this.loading = false;
1682
+ }
1683
+ }
1684
+ selectTenantItem(tenantId) {
1685
+ this.selectedTenantId = tenantId;
1686
+ }
1687
+ async onContinueWithTenant() {
1688
+ if (!this.selectedTenantId) {
1689
+ this.error = 'Please select an organization';
1690
+ return;
1691
+ }
1692
+ const membership = this.memberships.find(m => m.tenant_id === this.selectedTenantId);
1693
+ if (!membership) {
1694
+ this.error = 'Selected organization not found';
1695
+ return;
1696
+ }
1697
+ await this.selectAndContinue(membership);
1698
+ }
1699
+ async selectAndContinue(membership) {
1700
+ this.loading = true;
1701
+ this.error = '';
1702
+ try {
1703
+ const result = await this.auth.selectTenant(membership.tenant_id);
1704
+ if (!result.success) {
1705
+ this.error = result.message || 'Failed to select organization';
1706
+ return;
1707
+ }
1708
+ // Emit tenant selected event
1709
+ this.tenantSelected.emit({
1710
+ tenantId: membership.tenant_id,
1711
+ tenantSlug: membership.slug,
1712
+ role: membership.role
1713
+ });
1714
+ }
1715
+ catch (err) {
1716
+ this.error = err.message || 'An unexpected error occurred';
1717
+ }
1718
+ finally {
1719
+ this.loading = false;
1720
+ }
1721
+ }
1722
+ formatRole(role) {
1723
+ return role.charAt(0).toUpperCase() + role.slice(1);
1724
+ }
1725
+ formatLastAccessed(dateStr) {
1726
+ try {
1727
+ const date = new Date(dateStr);
1728
+ const now = new Date();
1729
+ const diffMs = now.getTime() - date.getTime();
1730
+ const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
1731
+ if (diffDays === 0)
1732
+ return 'today';
1733
+ if (diffDays === 1)
1734
+ return 'yesterday';
1735
+ if (diffDays < 7)
1736
+ return `${diffDays} days ago`;
1737
+ if (diffDays < 30)
1738
+ return `${Math.floor(diffDays / 7)} weeks ago`;
1739
+ return `${Math.floor(diffDays / 30)} months ago`;
1740
+ }
1741
+ catch {
1742
+ return dateStr;
1743
+ }
1744
+ }
1745
+ onCreateTenantClick(event) {
1746
+ event.preventDefault();
1747
+ this.createTenant.emit();
1748
+ }
1749
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TenantLoginComponent, deps: [{ token: AuthService }], target: i0.ɵɵFactoryTarget.Component });
1750
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.15", type: TenantLoginComponent, isStandalone: true, selector: "lib-tenant-login", inputs: { title: "title", providers: "providers", showTenantSelector: "showTenantSelector", autoSelectSingleTenant: "autoSelectSingleTenant", prefillEmail: "prefillEmail", allowTenantCreation: "allowTenantCreation", tenantSelectorTitle: "tenantSelectorTitle", tenantSelectorDescription: "tenantSelectorDescription", continueButtonText: "continueButtonText", registerLinkText: "registerLinkText", registerLinkAction: "registerLinkAction", createTenantLinkText: "createTenantLinkText", createTenantLinkAction: "createTenantLinkAction" }, outputs: { tenantSelected: "tenantSelected", createTenant: "createTenant" }, ngImport: i0, template: `
1751
+ <div class="tenant-login-dialog">
1752
+ @if (!showingTenantSelector) {
1753
+ <!-- Step 1: Authentication -->
1754
+ <h2 class="login-title">{{ title }}</h2>
1755
+
1756
+ <!-- Email/Password Form (if enabled) -->
1757
+ @if (isProviderEnabled('emailPassword') && !useOAuth) {
1758
+ <form (ngSubmit)="onEmailLogin()" class="email-form">
1759
+ <div class="form-group">
1760
+ <input
1761
+ [(ngModel)]="email"
1762
+ name="email"
1763
+ placeholder="Email"
1764
+ type="email"
1765
+ required
1766
+ class="form-control">
1767
+ </div>
1768
+ <div class="form-group password-group">
1769
+ <input
1770
+ [(ngModel)]="password"
1771
+ name="password"
1772
+ placeholder="Password"
1773
+ [type]="showPassword ? 'text' : 'password'"
1774
+ required
1775
+ class="form-control password-input">
1776
+ <button
1777
+ type="button"
1778
+ class="password-toggle"
1779
+ (click)="showPassword = !showPassword"
1780
+ [attr.aria-label]="showPassword ? 'Hide password' : 'Show password'">
1781
+ {{ showPassword ? '👁️' : '👁️‍🗨️' }}
1782
+ </button>
1783
+ </div>
1784
+ <button
1785
+ type="submit"
1786
+ [disabled]="loading"
1787
+ class="btn btn-primary btn-block">
1788
+ {{ loading ? 'Signing in...' : 'Sign in with Email' }}
1789
+ </button>
1790
+ </form>
1791
+
1792
+ <!-- Divider -->
1793
+ @if (oauthProviders.length > 0) {
1794
+ <div class="divider">
1795
+ <span>OR</span>
1796
+ </div>
1797
+ }
1798
+ }
1799
+
1800
+ <!-- OAuth Providers -->
1801
+ @if (oauthProviders.length > 0 && (useOAuth || !isProviderEnabled('emailPassword'))) {
1802
+ <div class="oauth-buttons">
1803
+ @for (provider of oauthProviders; track provider) {
1804
+ <button
1805
+ type="button"
1806
+ (click)="onOAuthLogin(provider)"
1807
+ [disabled]="loading"
1808
+ class="btn btn-oauth btn-{{ provider }}">
1809
+ @if (getProviderIcon(provider)) {
1810
+ <span class="oauth-icon">
1811
+ {{ getProviderIcon(provider) }}
1812
+ </span>
1813
+ }
1814
+ {{ getProviderLabel(provider) }}
1815
+ </button>
1816
+ }
1440
1817
  </div>
1441
- <div class="form-group">
1442
- <input
1443
- [(ngModel)]="password"
1444
- name="password"
1445
- placeholder="Password"
1446
- type="password"
1447
- required
1448
- class="form-control">
1818
+
1819
+ <!-- Switch to Email/Password -->
1820
+ @if (isProviderEnabled('emailPassword') && oauthProviders.length > 0) {
1821
+ <div class="switch-method">
1822
+ <a href="#" (click)="toggleAuthMethod($event)">
1823
+ {{ useOAuth ? 'Use email/password instead' : 'Use OAuth instead' }}
1824
+ </a>
1825
+ </div>
1826
+ }
1827
+ }
1828
+
1829
+ <!-- Error Message -->
1830
+ @if (error) {
1831
+ <div class="error-message">
1832
+ {{ error }}
1449
1833
  </div>
1450
- <button
1451
- type="submit"
1452
- [disabled]="loading"
1453
- class="btn btn-primary btn-block">
1454
- {{ loading ? 'Signing in...' : getProviderLabel('emailPassword') }}
1455
- </button>
1456
- </form>
1834
+ }
1835
+
1836
+ <!-- Register Link -->
1837
+ @if (allowTenantCreation) {
1838
+ <div class="register-link">
1839
+ {{ registerLinkText }}
1840
+ <a href="#" (click)="onCreateTenantClick($event)">{{ registerLinkAction }}</a>
1841
+ </div>
1842
+ }
1843
+ } @else {
1844
+ <!-- Step 2: Tenant Selection -->
1845
+ <h2 class="login-title">{{ tenantSelectorTitle }}</h2>
1846
+
1847
+ @if (userName) {
1848
+ <div class="welcome-message">
1849
+ Welcome back, <strong>{{ userName }}</strong>!
1850
+ </div>
1851
+ }
1852
+
1853
+ <p class="selector-description">{{ tenantSelectorDescription }}</p>
1854
+
1855
+ <div class="tenant-list">
1856
+ @for (membership of memberships; track membership.tenant_id) {
1857
+ <div
1858
+ class="tenant-item"
1859
+ [class.selected]="selectedTenantId === membership.tenant_id"
1860
+ (click)="selectTenantItem(membership.tenant_id)">
1861
+ <div class="tenant-radio">
1862
+ <input
1863
+ type="radio"
1864
+ [checked]="selectedTenantId === membership.tenant_id"
1865
+ [name]="'tenant-' + membership.tenant_id"
1866
+ [id]="'tenant-' + membership.tenant_id">
1867
+ </div>
1868
+ <div class="tenant-info">
1869
+ <div class="tenant-name">{{ membership.name }}</div>
1870
+ <div class="tenant-meta">
1871
+ <span class="tenant-role">{{ formatRole(membership.role) }}</span>
1872
+ @if (membership.last_accessed) {
1873
+ <span class="tenant-separator">·</span>
1874
+ <span class="tenant-last-accessed">
1875
+ Last accessed {{ formatLastAccessed(membership.last_accessed) }}
1876
+ </span>
1877
+ }
1878
+ </div>
1879
+ </div>
1880
+ </div>
1881
+ }
1882
+ </div>
1883
+
1884
+ <button
1885
+ type="button"
1886
+ (click)="onContinueWithTenant()"
1887
+ [disabled]="!selectedTenantId || loading"
1888
+ class="btn btn-primary btn-block">
1889
+ {{ loading ? 'Loading...' : continueButtonText }}
1890
+ </button>
1891
+
1892
+ <!-- Error Message -->
1893
+ @if (error) {
1894
+ <div class="error-message">
1895
+ {{ error }}
1896
+ </div>
1897
+ }
1898
+
1899
+ <!-- Create New Tenant Link -->
1900
+ @if (allowTenantCreation) {
1901
+ <div class="create-tenant-link">
1902
+ {{ createTenantLinkText }}
1903
+ <a href="#" (click)="onCreateTenantClick($event)">{{ createTenantLinkAction }}</a>
1904
+ </div>
1905
+ }
1457
1906
  }
1458
1907
 
1459
- <!-- Divider if both email and OAuth are present -->
1460
- @if (isProviderEnabled('emailPassword') && oauthProviders.length > 0) {
1461
- <div class="divider">
1462
- <span>OR</span>
1908
+ <!-- Loading Overlay -->
1909
+ @if (loading) {
1910
+ <div class="loading-overlay">
1911
+ <div class="spinner"></div>
1463
1912
  </div>
1464
1913
  }
1914
+ </div>
1915
+ `, isInline: true, styles: [".tenant-login-dialog{padding:24px;max-width:450px;position:relative}.login-title{margin:0 0 24px;font-size:24px;font-weight:500;text-align:center}.welcome-message{margin-bottom:16px;padding:12px;background:#e8f5e9;border-radius:4px;text-align:center;font-size:14px;color:#2e7d32}.selector-description{margin-bottom:20px;font-size:14px;color:#666;text-align:center}.email-form,.form-group{margin-bottom:16px}.password-group{position:relative}.form-control{width:100%;padding:12px;border:1px solid #ddd;border-radius:4px;font-size:14px;box-sizing:border-box}.password-input{padding-right:45px}.password-toggle{position:absolute;right:8px;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;font-size:18px;padding:8px;line-height:1;opacity:.6;transition:opacity .2s}.password-toggle:hover{opacity:1}.password-toggle:focus{outline:2px solid #4285f4;outline-offset:2px;border-radius:4px}.form-control:focus{outline:none;border-color:#4285f4}.btn{padding:12px 24px;border:none;border-radius:4px;font-size:14px;font-weight:500;cursor:pointer;transition:background-color .2s}.btn:disabled{opacity:.6;cursor:not-allowed}.btn-block{width:100%}.btn-primary{background-color:#4285f4;color:#fff}.btn-primary:hover:not(:disabled){background-color:#357ae8}.divider{margin:16px 0;text-align:center;position:relative}.divider:before{content:\"\";position:absolute;top:50%;left:0;right:0;height:1px;background:#ddd}.divider span{background:#fff;padding:0 12px;position:relative;color:#666;font-size:12px}.oauth-buttons{display:flex;flex-direction:column;gap:12px}.btn-oauth{width:100%;background:#fff;color:#333;border:1px solid #ddd;display:flex;align-items:center;justify-content:center;gap:8px}.btn-oauth:hover:not(:disabled){background:#f8f8f8}.btn-google{border-color:#4285f4}.btn-linkedin{border-color:#0077b5}.btn-apple{border-color:#000}.btn-microsoft{border-color:#00a4ef}.btn-github{border-color:#333}.btn-zoho{background-color:#f0483e;color:#fff;border:1px solid #d63b32}.btn-zoho:hover{background-color:#d63b32}.oauth-icon{font-size:18px}.switch-method{margin-top:12px;text-align:center;font-size:14px}.switch-method a{color:#4285f4;text-decoration:none}.switch-method a:hover{text-decoration:underline}.tenant-list{margin-bottom:20px;display:flex;flex-direction:column;gap:12px}.tenant-item{display:flex;align-items:flex-start;gap:12px;padding:16px;border:2px solid #e0e0e0;border-radius:6px;cursor:pointer;transition:all .2s}.tenant-item:hover{border-color:#4285f4;background:#f8f9ff}.tenant-item.selected{border-color:#4285f4;background:#e8f0fe}.tenant-radio{flex-shrink:0;padding-top:2px}.tenant-radio input[type=radio]{width:18px;height:18px;cursor:pointer}.tenant-info{flex:1}.tenant-name{font-size:16px;font-weight:500;color:#333;margin-bottom:4px}.tenant-meta{font-size:13px;color:#666}.tenant-role{font-weight:500;color:#4285f4}.tenant-separator{margin:0 6px}.error-message{margin-top:16px;padding:12px;background:#fee;color:#c33;border-radius:4px;font-size:14px}.loading-overlay{position:absolute;inset:0;background:#fffc;display:flex;align-items:center;justify-content:center}.spinner{width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #4285f4;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.register-link{margin-top:16px;text-align:center;font-size:14px;color:#666}.register-link a{color:#4285f4;text-decoration:none}.register-link a:hover{text-decoration:underline}.create-tenant-link{margin-top:16px;padding-top:16px;border-top:1px solid #e0e0e0;text-align:center;font-size:14px;color:#666}.create-tenant-link a{color:#4285f4;text-decoration:none;font-weight:500}.create-tenant-link a:hover{text-decoration:underline}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i2.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }] });
1916
+ }
1917
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TenantLoginComponent, decorators: [{
1918
+ type: Component,
1919
+ args: [{ selector: 'lib-tenant-login', standalone: true, imports: [CommonModule, FormsModule], template: `
1920
+ <div class="tenant-login-dialog">
1921
+ @if (!showingTenantSelector) {
1922
+ <!-- Step 1: Authentication -->
1923
+ <h2 class="login-title">{{ title }}</h2>
1465
1924
 
1466
- <!-- OAuth Providers -->
1467
- @if (oauthProviders.length > 0) {
1468
- <div class="oauth-buttons">
1469
- @for (provider of oauthProviders; track provider) {
1925
+ <!-- Email/Password Form (if enabled) -->
1926
+ @if (isProviderEnabled('emailPassword') && !useOAuth) {
1927
+ <form (ngSubmit)="onEmailLogin()" class="email-form">
1928
+ <div class="form-group">
1929
+ <input
1930
+ [(ngModel)]="email"
1931
+ name="email"
1932
+ placeholder="Email"
1933
+ type="email"
1934
+ required
1935
+ class="form-control">
1936
+ </div>
1937
+ <div class="form-group password-group">
1938
+ <input
1939
+ [(ngModel)]="password"
1940
+ name="password"
1941
+ placeholder="Password"
1942
+ [type]="showPassword ? 'text' : 'password'"
1943
+ required
1944
+ class="form-control password-input">
1945
+ <button
1946
+ type="button"
1947
+ class="password-toggle"
1948
+ (click)="showPassword = !showPassword"
1949
+ [attr.aria-label]="showPassword ? 'Hide password' : 'Show password'">
1950
+ {{ showPassword ? '👁️' : '👁️‍🗨️' }}
1951
+ </button>
1952
+ </div>
1470
1953
  <button
1471
- (click)="onOAuthLogin(provider)"
1954
+ type="submit"
1472
1955
  [disabled]="loading"
1473
- class="btn btn-oauth btn-{{ provider }}">
1474
- @if (getProviderIcon(provider)) {
1475
- <span class="oauth-icon">
1476
- {{ getProviderIcon(provider) }}
1477
- </span>
1478
- }
1479
- {{ getProviderLabel(provider) }}
1956
+ class="btn btn-primary btn-block">
1957
+ {{ loading ? 'Signing in...' : 'Sign in with Email' }}
1480
1958
  </button>
1959
+ </form>
1960
+
1961
+ <!-- Divider -->
1962
+ @if (oauthProviders.length > 0) {
1963
+ <div class="divider">
1964
+ <span>OR</span>
1965
+ </div>
1481
1966
  }
1482
- </div>
1483
- }
1967
+ }
1484
1968
 
1485
- <!-- Error Message -->
1486
- @if (error) {
1487
- <div class="error-message">
1488
- {{ error }}
1969
+ <!-- OAuth Providers -->
1970
+ @if (oauthProviders.length > 0 && (useOAuth || !isProviderEnabled('emailPassword'))) {
1971
+ <div class="oauth-buttons">
1972
+ @for (provider of oauthProviders; track provider) {
1973
+ <button
1974
+ type="button"
1975
+ (click)="onOAuthLogin(provider)"
1976
+ [disabled]="loading"
1977
+ class="btn btn-oauth btn-{{ provider }}">
1978
+ @if (getProviderIcon(provider)) {
1979
+ <span class="oauth-icon">
1980
+ {{ getProviderIcon(provider) }}
1981
+ </span>
1982
+ }
1983
+ {{ getProviderLabel(provider) }}
1984
+ </button>
1985
+ }
1986
+ </div>
1987
+
1988
+ <!-- Switch to Email/Password -->
1989
+ @if (isProviderEnabled('emailPassword') && oauthProviders.length > 0) {
1990
+ <div class="switch-method">
1991
+ <a href="#" (click)="toggleAuthMethod($event)">
1992
+ {{ useOAuth ? 'Use email/password instead' : 'Use OAuth instead' }}
1993
+ </a>
1994
+ </div>
1995
+ }
1996
+ }
1997
+
1998
+ <!-- Error Message -->
1999
+ @if (error) {
2000
+ <div class="error-message">
2001
+ {{ error }}
2002
+ </div>
2003
+ }
2004
+
2005
+ <!-- Register Link -->
2006
+ @if (allowTenantCreation) {
2007
+ <div class="register-link">
2008
+ {{ registerLinkText }}
2009
+ <a href="#" (click)="onCreateTenantClick($event)">{{ registerLinkAction }}</a>
2010
+ </div>
2011
+ }
2012
+ } @else {
2013
+ <!-- Step 2: Tenant Selection -->
2014
+ <h2 class="login-title">{{ tenantSelectorTitle }}</h2>
2015
+
2016
+ @if (userName) {
2017
+ <div class="welcome-message">
2018
+ Welcome back, <strong>{{ userName }}</strong>!
2019
+ </div>
2020
+ }
2021
+
2022
+ <p class="selector-description">{{ tenantSelectorDescription }}</p>
2023
+
2024
+ <div class="tenant-list">
2025
+ @for (membership of memberships; track membership.tenant_id) {
2026
+ <div
2027
+ class="tenant-item"
2028
+ [class.selected]="selectedTenantId === membership.tenant_id"
2029
+ (click)="selectTenantItem(membership.tenant_id)">
2030
+ <div class="tenant-radio">
2031
+ <input
2032
+ type="radio"
2033
+ [checked]="selectedTenantId === membership.tenant_id"
2034
+ [name]="'tenant-' + membership.tenant_id"
2035
+ [id]="'tenant-' + membership.tenant_id">
2036
+ </div>
2037
+ <div class="tenant-info">
2038
+ <div class="tenant-name">{{ membership.name }}</div>
2039
+ <div class="tenant-meta">
2040
+ <span class="tenant-role">{{ formatRole(membership.role) }}</span>
2041
+ @if (membership.last_accessed) {
2042
+ <span class="tenant-separator">·</span>
2043
+ <span class="tenant-last-accessed">
2044
+ Last accessed {{ formatLastAccessed(membership.last_accessed) }}
2045
+ </span>
2046
+ }
2047
+ </div>
2048
+ </div>
2049
+ </div>
2050
+ }
1489
2051
  </div>
2052
+
2053
+ <button
2054
+ type="button"
2055
+ (click)="onContinueWithTenant()"
2056
+ [disabled]="!selectedTenantId || loading"
2057
+ class="btn btn-primary btn-block">
2058
+ {{ loading ? 'Loading...' : continueButtonText }}
2059
+ </button>
2060
+
2061
+ <!-- Error Message -->
2062
+ @if (error) {
2063
+ <div class="error-message">
2064
+ {{ error }}
2065
+ </div>
2066
+ }
2067
+
2068
+ <!-- Create New Tenant Link -->
2069
+ @if (allowTenantCreation) {
2070
+ <div class="create-tenant-link">
2071
+ {{ createTenantLinkText }}
2072
+ <a href="#" (click)="onCreateTenantClick($event)">{{ createTenantLinkAction }}</a>
2073
+ </div>
2074
+ }
1490
2075
  }
1491
2076
 
1492
- <!-- Loading State -->
2077
+ <!-- Loading Overlay -->
1493
2078
  @if (loading) {
1494
2079
  <div class="loading-overlay">
1495
2080
  <div class="spinner"></div>
1496
2081
  </div>
1497
2082
  }
1498
-
1499
- <!-- Register Link -->
1500
- <div class="register-link">
1501
- Don't have an account?
1502
- <a href="#" (click)="onRegisterClick($event)">Sign up</a>
1503
- </div>
1504
2083
  </div>
1505
- `, styles: [".login-dialog{padding:24px;max-width:400px;position:relative}.login-title{margin:0 0 24px;font-size:24px;font-weight:500;text-align:center}.email-form,.form-group{margin-bottom:16px}.form-control{width:100%;padding:12px;border:1px solid #ddd;border-radius:4px;font-size:14px;box-sizing:border-box}.form-control:focus{outline:none;border-color:#4285f4}.btn{padding:12px 24px;border:none;border-radius:4px;font-size:14px;font-weight:500;cursor:pointer;transition:background-color .2s}.btn:disabled{opacity:.6;cursor:not-allowed}.btn-block{width:100%}.btn-primary{background-color:#4285f4;color:#fff}.btn-primary:hover:not(:disabled){background-color:#357ae8}.divider{margin:16px 0;text-align:center;position:relative}.divider:before{content:\"\";position:absolute;top:50%;left:0;right:0;height:1px;background:#ddd}.divider span{background:#fff;padding:0 12px;position:relative;color:#666;font-size:12px}.oauth-buttons{display:flex;flex-direction:column;gap:12px}.btn-oauth{width:100%;background:#fff;color:#333;border:1px solid #ddd;display:flex;align-items:center;justify-content:center;gap:8px}.btn-oauth:hover:not(:disabled){background:#f8f8f8}.btn-google{border-color:#4285f4}.btn-linkedin{border-color:#0077b5}.btn-apple{border-color:#000}.btn-microsoft{border-color:#00a4ef}.btn-github{border-color:#333}.oauth-icon{font-size:18px}.error-message{margin-top:16px;padding:12px;background:#fee;color:#c33;border-radius:4px;font-size:14px}.loading-overlay{position:absolute;inset:0;background:#fffc;display:flex;align-items:center;justify-content:center}.spinner{width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #4285f4;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.register-link{margin-top:16px;text-align:center;font-size:14px;color:#666}.register-link a{color:#4285f4;text-decoration:none}.register-link a:hover{text-decoration:underline}\n"] }]
1506
- }], ctorParameters: () => [{ type: AuthService }], propDecorators: { providers: [{
2084
+ `, styles: [".tenant-login-dialog{padding:24px;max-width:450px;position:relative}.login-title{margin:0 0 24px;font-size:24px;font-weight:500;text-align:center}.welcome-message{margin-bottom:16px;padding:12px;background:#e8f5e9;border-radius:4px;text-align:center;font-size:14px;color:#2e7d32}.selector-description{margin-bottom:20px;font-size:14px;color:#666;text-align:center}.email-form,.form-group{margin-bottom:16px}.password-group{position:relative}.form-control{width:100%;padding:12px;border:1px solid #ddd;border-radius:4px;font-size:14px;box-sizing:border-box}.password-input{padding-right:45px}.password-toggle{position:absolute;right:8px;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;font-size:18px;padding:8px;line-height:1;opacity:.6;transition:opacity .2s}.password-toggle:hover{opacity:1}.password-toggle:focus{outline:2px solid #4285f4;outline-offset:2px;border-radius:4px}.form-control:focus{outline:none;border-color:#4285f4}.btn{padding:12px 24px;border:none;border-radius:4px;font-size:14px;font-weight:500;cursor:pointer;transition:background-color .2s}.btn:disabled{opacity:.6;cursor:not-allowed}.btn-block{width:100%}.btn-primary{background-color:#4285f4;color:#fff}.btn-primary:hover:not(:disabled){background-color:#357ae8}.divider{margin:16px 0;text-align:center;position:relative}.divider:before{content:\"\";position:absolute;top:50%;left:0;right:0;height:1px;background:#ddd}.divider span{background:#fff;padding:0 12px;position:relative;color:#666;font-size:12px}.oauth-buttons{display:flex;flex-direction:column;gap:12px}.btn-oauth{width:100%;background:#fff;color:#333;border:1px solid #ddd;display:flex;align-items:center;justify-content:center;gap:8px}.btn-oauth:hover:not(:disabled){background:#f8f8f8}.btn-google{border-color:#4285f4}.btn-linkedin{border-color:#0077b5}.btn-apple{border-color:#000}.btn-microsoft{border-color:#00a4ef}.btn-github{border-color:#333}.btn-zoho{background-color:#f0483e;color:#fff;border:1px solid #d63b32}.btn-zoho:hover{background-color:#d63b32}.oauth-icon{font-size:18px}.switch-method{margin-top:12px;text-align:center;font-size:14px}.switch-method a{color:#4285f4;text-decoration:none}.switch-method a:hover{text-decoration:underline}.tenant-list{margin-bottom:20px;display:flex;flex-direction:column;gap:12px}.tenant-item{display:flex;align-items:flex-start;gap:12px;padding:16px;border:2px solid #e0e0e0;border-radius:6px;cursor:pointer;transition:all .2s}.tenant-item:hover{border-color:#4285f4;background:#f8f9ff}.tenant-item.selected{border-color:#4285f4;background:#e8f0fe}.tenant-radio{flex-shrink:0;padding-top:2px}.tenant-radio input[type=radio]{width:18px;height:18px;cursor:pointer}.tenant-info{flex:1}.tenant-name{font-size:16px;font-weight:500;color:#333;margin-bottom:4px}.tenant-meta{font-size:13px;color:#666}.tenant-role{font-weight:500;color:#4285f4}.tenant-separator{margin:0 6px}.error-message{margin-top:16px;padding:12px;background:#fee;color:#c33;border-radius:4px;font-size:14px}.loading-overlay{position:absolute;inset:0;background:#fffc;display:flex;align-items:center;justify-content:center}.spinner{width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #4285f4;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.register-link{margin-top:16px;text-align:center;font-size:14px;color:#666}.register-link a{color:#4285f4;text-decoration:none}.register-link a:hover{text-decoration:underline}.create-tenant-link{margin-top:16px;padding-top:16px;border-top:1px solid #e0e0e0;text-align:center;font-size:14px;color:#666}.create-tenant-link a{color:#4285f4;text-decoration:none;font-weight:500}.create-tenant-link a:hover{text-decoration:underline}\n"] }]
2085
+ }], ctorParameters: () => [{ type: AuthService }], propDecorators: { title: [{
2086
+ type: Input
2087
+ }], providers: [{
2088
+ type: Input
2089
+ }], showTenantSelector: [{
2090
+ type: Input
2091
+ }], autoSelectSingleTenant: [{
2092
+ type: Input
2093
+ }], prefillEmail: [{
2094
+ type: Input
2095
+ }], allowTenantCreation: [{
2096
+ type: Input
2097
+ }], tenantSelectorTitle: [{
2098
+ type: Input
2099
+ }], tenantSelectorDescription: [{
2100
+ type: Input
2101
+ }], continueButtonText: [{
2102
+ type: Input
2103
+ }], registerLinkText: [{
2104
+ type: Input
2105
+ }], registerLinkAction: [{
2106
+ type: Input
2107
+ }], createTenantLinkText: [{
2108
+ type: Input
2109
+ }], createTenantLinkAction: [{
1507
2110
  type: Input
2111
+ }], tenantSelected: [{
2112
+ type: Output
2113
+ }], createTenant: [{
2114
+ type: Output
1508
2115
  }] } });
1509
2116
 
1510
2117
  class RegisterComponent {
1511
2118
  auth;
2119
+ environment;
2120
+ navigateToLogin = new EventEmitter();
1512
2121
  displayName = '';
1513
2122
  email = '';
1514
2123
  password = '';
@@ -1516,8 +2125,13 @@ class RegisterComponent {
1516
2125
  error = '';
1517
2126
  success = '';
1518
2127
  loading = false;
1519
- constructor(auth) {
2128
+ showAccountLinkPrompt = false;
2129
+ existingEmail = '';
2130
+ showPassword = false;
2131
+ showConfirmPassword = false;
2132
+ constructor(auth, environment) {
1520
2133
  this.auth = auth;
2134
+ this.environment = environment;
1521
2135
  }
1522
2136
  async onRegister() {
1523
2137
  // Reset messages
@@ -1544,14 +2158,40 @@ class RegisterComponent {
1544
2158
  }
1545
2159
  this.loading = true;
1546
2160
  try {
1547
- const result = await this.auth.register(this.email, this.password, this.displayName);
1548
- if (result.success) {
1549
- this.success = result.message || 'Account created successfully!';
1550
- // On success, parent component/dialog should close automatically via user$ subscription
1551
- // or navigate to email verification page
2161
+ // Direct API call to check for email already registered
2162
+ const response = await fetch(`${this.environment.accountsUrl}/api/auth/register`, {
2163
+ method: 'POST',
2164
+ headers: { 'Content-Type': 'application/json' },
2165
+ credentials: 'include',
2166
+ body: JSON.stringify({
2167
+ email: this.email,
2168
+ password: this.password,
2169
+ display_name: this.displayName,
2170
+ platform: this.environment.platformCode
2171
+ })
2172
+ });
2173
+ const data = await response.json();
2174
+ if (response.ok && data.identity_id) {
2175
+ // Registration successful - now login
2176
+ const loginResult = await this.auth.loginWithEmail(this.email, this.password);
2177
+ if (loginResult.success) {
2178
+ this.success = 'Account created successfully!';
2179
+ }
2180
+ else {
2181
+ this.success = 'Account created! Please sign in.';
2182
+ }
1552
2183
  }
1553
2184
  else {
1554
- this.error = result.message || 'Registration failed';
2185
+ // Check if email already registered
2186
+ if (data.error === 'Email already registered' || data.details?.includes('Email already registered')) {
2187
+ this.existingEmail = this.email;
2188
+ this.showAccountLinkPrompt = true;
2189
+ this.error = '';
2190
+ }
2191
+ else {
2192
+ // Other errors
2193
+ this.error = data.error || data.details || 'Registration failed';
2194
+ }
1555
2195
  }
1556
2196
  }
1557
2197
  catch (err) {
@@ -1563,16 +2203,50 @@ class RegisterComponent {
1563
2203
  }
1564
2204
  onLoginClick(event) {
1565
2205
  event.preventDefault();
1566
- // Platforms can override this or listen for a custom event
1567
- // For now, just emit a console message
1568
- console.log('Login clicked - platform should handle navigation');
1569
- }
1570
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: RegisterComponent, deps: [{ token: AuthService }], target: i0.ɵɵFactoryTarget.Component });
1571
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.15", type: RegisterComponent, isStandalone: true, selector: "lib-register", ngImport: i0, template: `
2206
+ this.navigateToLogin.emit('');
2207
+ }
2208
+ linkExistingAccount() {
2209
+ // User confirmed they want to link their existing account
2210
+ this.navigateToLogin.emit(this.existingEmail);
2211
+ }
2212
+ cancelLinking() {
2213
+ // User decided not to link - reset form
2214
+ this.showAccountLinkPrompt = false;
2215
+ this.existingEmail = '';
2216
+ this.email = '';
2217
+ this.password = '';
2218
+ this.confirmPassword = '';
2219
+ this.displayName = '';
2220
+ }
2221
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: RegisterComponent, deps: [{ token: AuthService }, { token: MyEnvironmentModel }], target: i0.ɵɵFactoryTarget.Component });
2222
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.15", type: RegisterComponent, isStandalone: true, selector: "lib-register", outputs: { navigateToLogin: "navigateToLogin" }, ngImport: i0, template: `
1572
2223
  <div class="register-dialog">
1573
2224
  <h2 class="register-title">Create Account</h2>
1574
2225
 
1575
- <form (ngSubmit)="onRegister()" class="register-form">
2226
+ <!-- Account Link Prompt -->
2227
+ @if (showAccountLinkPrompt) {
2228
+ <div class="account-link-prompt">
2229
+ <div class="prompt-icon">🔗</div>
2230
+ <h3>Account Already Exists</h3>
2231
+ <p>
2232
+ You already have an account with <strong>{{ existingEmail }}</strong>,
2233
+ used on another ProGalaxy E-Labs platform.
2234
+ </p>
2235
+ <p>
2236
+ Would you like to use the same account to access this platform?
2237
+ </p>
2238
+ <div class="prompt-actions">
2239
+ <button type="button" class="btn btn-primary btn-block" (click)="linkExistingAccount()">
2240
+ Yes, Use My Existing Account
2241
+ </button>
2242
+ <button type="button" class="btn btn-secondary btn-block" (click)="cancelLinking()">
2243
+ No, Use Different Email
2244
+ </button>
2245
+ </div>
2246
+ </div>
2247
+ }
2248
+
2249
+ <form *ngIf="!showAccountLinkPrompt" (ngSubmit)="onRegister()" class="register-form">
1576
2250
  <div class="form-group">
1577
2251
  <label for="displayName">Full Name</label>
1578
2252
  <input
@@ -1597,30 +2271,44 @@ class RegisterComponent {
1597
2271
  class="form-control">
1598
2272
  </div>
1599
2273
 
1600
- <div class="form-group">
2274
+ <div class="form-group password-group">
1601
2275
  <label for="password">Password</label>
1602
2276
  <input
1603
2277
  id="password"
1604
2278
  [(ngModel)]="password"
1605
2279
  name="password"
1606
2280
  placeholder="Create a password"
1607
- type="password"
2281
+ [type]="showPassword ? 'text' : 'password'"
1608
2282
  required
1609
2283
  minlength="8"
1610
- class="form-control">
2284
+ class="form-control password-input">
2285
+ <button
2286
+ type="button"
2287
+ class="password-toggle"
2288
+ (click)="showPassword = !showPassword"
2289
+ [attr.aria-label]="showPassword ? 'Hide password' : 'Show password'">
2290
+ {{ showPassword ? '👁️' : '👁️‍🗨️' }}
2291
+ </button>
1611
2292
  <small class="form-hint">At least 8 characters</small>
1612
2293
  </div>
1613
2294
 
1614
- <div class="form-group">
2295
+ <div class="form-group password-group">
1615
2296
  <label for="confirmPassword">Confirm Password</label>
1616
2297
  <input
1617
2298
  id="confirmPassword"
1618
2299
  [(ngModel)]="confirmPassword"
1619
2300
  name="confirmPassword"
1620
2301
  placeholder="Confirm your password"
1621
- type="password"
2302
+ [type]="showConfirmPassword ? 'text' : 'password'"
1622
2303
  required
1623
- class="form-control">
2304
+ class="form-control password-input">
2305
+ <button
2306
+ type="button"
2307
+ class="password-toggle"
2308
+ (click)="showConfirmPassword = !showConfirmPassword"
2309
+ [attr.aria-label]="showConfirmPassword ? 'Hide password' : 'Show password'">
2310
+ {{ showConfirmPassword ? '👁️' : '👁️‍🗨️' }}
2311
+ </button>
1624
2312
  </div>
1625
2313
 
1626
2314
  <button
@@ -1632,7 +2320,7 @@ class RegisterComponent {
1632
2320
  </form>
1633
2321
 
1634
2322
  <!-- Error Message -->
1635
- @if (error) {
2323
+ @if (error && !showAccountLinkPrompt) {
1636
2324
  <div class="error-message">
1637
2325
  {{ error }}
1638
2326
  </div>
@@ -1653,12 +2341,12 @@ class RegisterComponent {
1653
2341
  }
1654
2342
 
1655
2343
  <!-- Login Link -->
1656
- <div class="login-link">
2344
+ <div *ngIf="!showAccountLinkPrompt" class="login-link">
1657
2345
  Already have an account?
1658
2346
  <a href="#" (click)="onLoginClick($event)">Sign in</a>
1659
2347
  </div>
1660
2348
  </div>
1661
- `, isInline: true, styles: [".register-dialog{padding:24px;max-width:400px;position:relative}.register-title{margin:0 0 24px;font-size:24px;font-weight:500;text-align:center}.register-form,.form-group{margin-bottom:16px}.form-group label{display:block;margin-bottom:6px;font-size:14px;font-weight:500;color:#333}.form-control{width:100%;padding:12px;border:1px solid #ddd;border-radius:4px;font-size:14px;box-sizing:border-box}.form-control:focus{outline:none;border-color:#4285f4}.form-hint{display:block;margin-top:4px;font-size:12px;color:#666}.btn{padding:12px 24px;border:none;border-radius:4px;font-size:14px;font-weight:500;cursor:pointer;transition:background-color .2s}.btn:disabled{opacity:.6;cursor:not-allowed}.btn-block{width:100%}.btn-primary{background-color:#4285f4;color:#fff}.btn-primary:hover:not(:disabled){background-color:#357ae8}.error-message{margin-top:16px;padding:12px;background:#fee;color:#c33;border-radius:4px;font-size:14px}.success-message{margin-top:16px;padding:12px;background:#efe;color:#3a3;border-radius:4px;font-size:14px}.loading-overlay{position:absolute;inset:0;background:#fffc;display:flex;align-items:center;justify-content:center}.spinner{width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #4285f4;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.login-link{margin-top:16px;text-align:center;font-size:14px;color:#666}.login-link a{color:#4285f4;text-decoration:none}.login-link a:hover{text-decoration:underline}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i2.MinLengthValidator, selector: "[minlength][formControlName],[minlength][formControl],[minlength][ngModel]", inputs: ["minlength"] }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i2.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }] });
2349
+ `, isInline: true, styles: [".register-dialog{padding:24px;max-width:400px;position:relative}.register-title{margin:0 0 24px;font-size:24px;font-weight:500;text-align:center}.register-form,.form-group{margin-bottom:16px}.password-group{position:relative}.form-group label{display:block;margin-bottom:6px;font-size:14px;font-weight:500;color:#333}.form-control{width:100%;padding:12px;border:1px solid #ddd;border-radius:4px;font-size:14px;box-sizing:border-box}.password-input{padding-right:45px}.password-toggle{position:absolute;right:8px;top:38px;background:none;border:none;cursor:pointer;font-size:18px;padding:8px;line-height:1;opacity:.6;transition:opacity .2s}.password-toggle:hover{opacity:1}.password-toggle:focus{outline:2px solid #4285f4;outline-offset:2px;border-radius:4px}.form-control:focus{outline:none;border-color:#4285f4}.form-hint{display:block;margin-top:4px;font-size:12px;color:#666}.btn{padding:12px 24px;border:none;border-radius:4px;font-size:14px;font-weight:500;cursor:pointer;transition:background-color .2s}.btn:disabled{opacity:.6;cursor:not-allowed}.btn-block{width:100%}.btn-primary{background-color:#4285f4;color:#fff}.btn-primary:hover:not(:disabled){background-color:#357ae8}.error-message{margin-top:16px;padding:12px;background:#fee;color:#c33;border-radius:4px;font-size:14px}.success-message{margin-top:16px;padding:12px;background:#efe;color:#3a3;border-radius:4px;font-size:14px}.loading-overlay{position:absolute;inset:0;background:#fffc;display:flex;align-items:center;justify-content:center}.spinner{width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #4285f4;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.login-link{margin-top:16px;text-align:center;font-size:14px;color:#666}.login-link a{color:#4285f4;text-decoration:none}.login-link a:hover{text-decoration:underline}.account-link-prompt{background:#f8f9fa;border:2px solid #4285f4;border-radius:8px;padding:24px;margin-bottom:16px;text-align:center}.prompt-icon{font-size:48px;margin-bottom:12px}.account-link-prompt h3{margin:0 0 12px;color:#333;font-size:20px;font-weight:500}.account-link-prompt p{margin:8px 0;color:#555;font-size:14px;line-height:1.6}.prompt-actions{margin-top:20px;display:flex;flex-direction:column;gap:10px}.btn-secondary{background:#fff;color:#333;border:1px solid #ddd}.btn-secondary:hover:not(:disabled){background:#f8f9fa;border-color:#ccc}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i2.MinLengthValidator, selector: "[minlength][formControlName],[minlength][formControl],[minlength][ngModel]", inputs: ["minlength"] }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i2.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }] });
1662
2350
  }
1663
2351
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: RegisterComponent, decorators: [{
1664
2352
  type: Component,
@@ -1666,7 +2354,30 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
1666
2354
  <div class="register-dialog">
1667
2355
  <h2 class="register-title">Create Account</h2>
1668
2356
 
1669
- <form (ngSubmit)="onRegister()" class="register-form">
2357
+ <!-- Account Link Prompt -->
2358
+ @if (showAccountLinkPrompt) {
2359
+ <div class="account-link-prompt">
2360
+ <div class="prompt-icon">🔗</div>
2361
+ <h3>Account Already Exists</h3>
2362
+ <p>
2363
+ You already have an account with <strong>{{ existingEmail }}</strong>,
2364
+ used on another ProGalaxy E-Labs platform.
2365
+ </p>
2366
+ <p>
2367
+ Would you like to use the same account to access this platform?
2368
+ </p>
2369
+ <div class="prompt-actions">
2370
+ <button type="button" class="btn btn-primary btn-block" (click)="linkExistingAccount()">
2371
+ Yes, Use My Existing Account
2372
+ </button>
2373
+ <button type="button" class="btn btn-secondary btn-block" (click)="cancelLinking()">
2374
+ No, Use Different Email
2375
+ </button>
2376
+ </div>
2377
+ </div>
2378
+ }
2379
+
2380
+ <form *ngIf="!showAccountLinkPrompt" (ngSubmit)="onRegister()" class="register-form">
1670
2381
  <div class="form-group">
1671
2382
  <label for="displayName">Full Name</label>
1672
2383
  <input
@@ -1691,30 +2402,44 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
1691
2402
  class="form-control">
1692
2403
  </div>
1693
2404
 
1694
- <div class="form-group">
2405
+ <div class="form-group password-group">
1695
2406
  <label for="password">Password</label>
1696
2407
  <input
1697
2408
  id="password"
1698
2409
  [(ngModel)]="password"
1699
2410
  name="password"
1700
2411
  placeholder="Create a password"
1701
- type="password"
2412
+ [type]="showPassword ? 'text' : 'password'"
1702
2413
  required
1703
2414
  minlength="8"
1704
- class="form-control">
2415
+ class="form-control password-input">
2416
+ <button
2417
+ type="button"
2418
+ class="password-toggle"
2419
+ (click)="showPassword = !showPassword"
2420
+ [attr.aria-label]="showPassword ? 'Hide password' : 'Show password'">
2421
+ {{ showPassword ? '👁️' : '👁️‍🗨️' }}
2422
+ </button>
1705
2423
  <small class="form-hint">At least 8 characters</small>
1706
2424
  </div>
1707
2425
 
1708
- <div class="form-group">
2426
+ <div class="form-group password-group">
1709
2427
  <label for="confirmPassword">Confirm Password</label>
1710
2428
  <input
1711
2429
  id="confirmPassword"
1712
2430
  [(ngModel)]="confirmPassword"
1713
2431
  name="confirmPassword"
1714
2432
  placeholder="Confirm your password"
1715
- type="password"
2433
+ [type]="showConfirmPassword ? 'text' : 'password'"
1716
2434
  required
1717
- class="form-control">
2435
+ class="form-control password-input">
2436
+ <button
2437
+ type="button"
2438
+ class="password-toggle"
2439
+ (click)="showConfirmPassword = !showConfirmPassword"
2440
+ [attr.aria-label]="showConfirmPassword ? 'Hide password' : 'Show password'">
2441
+ {{ showConfirmPassword ? '👁️' : '👁️‍🗨️' }}
2442
+ </button>
1718
2443
  </div>
1719
2444
 
1720
2445
  <button
@@ -1726,7 +2451,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
1726
2451
  </form>
1727
2452
 
1728
2453
  <!-- Error Message -->
1729
- @if (error) {
2454
+ @if (error && !showAccountLinkPrompt) {
1730
2455
  <div class="error-message">
1731
2456
  {{ error }}
1732
2457
  </div>
@@ -1747,60 +2472,157 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
1747
2472
  }
1748
2473
 
1749
2474
  <!-- Login Link -->
1750
- <div class="login-link">
2475
+ <div *ngIf="!showAccountLinkPrompt" class="login-link">
1751
2476
  Already have an account?
1752
2477
  <a href="#" (click)="onLoginClick($event)">Sign in</a>
1753
2478
  </div>
1754
2479
  </div>
1755
- `, styles: [".register-dialog{padding:24px;max-width:400px;position:relative}.register-title{margin:0 0 24px;font-size:24px;font-weight:500;text-align:center}.register-form,.form-group{margin-bottom:16px}.form-group label{display:block;margin-bottom:6px;font-size:14px;font-weight:500;color:#333}.form-control{width:100%;padding:12px;border:1px solid #ddd;border-radius:4px;font-size:14px;box-sizing:border-box}.form-control:focus{outline:none;border-color:#4285f4}.form-hint{display:block;margin-top:4px;font-size:12px;color:#666}.btn{padding:12px 24px;border:none;border-radius:4px;font-size:14px;font-weight:500;cursor:pointer;transition:background-color .2s}.btn:disabled{opacity:.6;cursor:not-allowed}.btn-block{width:100%}.btn-primary{background-color:#4285f4;color:#fff}.btn-primary:hover:not(:disabled){background-color:#357ae8}.error-message{margin-top:16px;padding:12px;background:#fee;color:#c33;border-radius:4px;font-size:14px}.success-message{margin-top:16px;padding:12px;background:#efe;color:#3a3;border-radius:4px;font-size:14px}.loading-overlay{position:absolute;inset:0;background:#fffc;display:flex;align-items:center;justify-content:center}.spinner{width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #4285f4;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.login-link{margin-top:16px;text-align:center;font-size:14px;color:#666}.login-link a{color:#4285f4;text-decoration:none}.login-link a:hover{text-decoration:underline}\n"] }]
1756
- }], ctorParameters: () => [{ type: AuthService }] });
2480
+ `, styles: [".register-dialog{padding:24px;max-width:400px;position:relative}.register-title{margin:0 0 24px;font-size:24px;font-weight:500;text-align:center}.register-form,.form-group{margin-bottom:16px}.password-group{position:relative}.form-group label{display:block;margin-bottom:6px;font-size:14px;font-weight:500;color:#333}.form-control{width:100%;padding:12px;border:1px solid #ddd;border-radius:4px;font-size:14px;box-sizing:border-box}.password-input{padding-right:45px}.password-toggle{position:absolute;right:8px;top:38px;background:none;border:none;cursor:pointer;font-size:18px;padding:8px;line-height:1;opacity:.6;transition:opacity .2s}.password-toggle:hover{opacity:1}.password-toggle:focus{outline:2px solid #4285f4;outline-offset:2px;border-radius:4px}.form-control:focus{outline:none;border-color:#4285f4}.form-hint{display:block;margin-top:4px;font-size:12px;color:#666}.btn{padding:12px 24px;border:none;border-radius:4px;font-size:14px;font-weight:500;cursor:pointer;transition:background-color .2s}.btn:disabled{opacity:.6;cursor:not-allowed}.btn-block{width:100%}.btn-primary{background-color:#4285f4;color:#fff}.btn-primary:hover:not(:disabled){background-color:#357ae8}.error-message{margin-top:16px;padding:12px;background:#fee;color:#c33;border-radius:4px;font-size:14px}.success-message{margin-top:16px;padding:12px;background:#efe;color:#3a3;border-radius:4px;font-size:14px}.loading-overlay{position:absolute;inset:0;background:#fffc;display:flex;align-items:center;justify-content:center}.spinner{width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #4285f4;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.login-link{margin-top:16px;text-align:center;font-size:14px;color:#666}.login-link a{color:#4285f4;text-decoration:none}.login-link a:hover{text-decoration:underline}.account-link-prompt{background:#f8f9fa;border:2px solid #4285f4;border-radius:8px;padding:24px;margin-bottom:16px;text-align:center}.prompt-icon{font-size:48px;margin-bottom:12px}.account-link-prompt h3{margin:0 0 12px;color:#333;font-size:20px;font-weight:500}.account-link-prompt p{margin:8px 0;color:#555;font-size:14px;line-height:1.6}.prompt-actions{margin-top:20px;display:flex;flex-direction:column;gap:10px}.btn-secondary{background:#fff;color:#333;border:1px solid #ddd}.btn-secondary:hover:not(:disabled){background:#f8f9fa;border-color:#ccc}\n"] }]
2481
+ }], ctorParameters: () => [{ type: AuthService }, { type: MyEnvironmentModel, decorators: [{
2482
+ type: Inject,
2483
+ args: [MyEnvironmentModel]
2484
+ }] }], propDecorators: { navigateToLogin: [{
2485
+ type: Output
2486
+ }] } });
1757
2487
 
1758
- class TenantLoginComponent {
2488
+ class AuthPageComponent {
2489
+ environment;
2490
+ providers = ['google', 'emailPassword'];
2491
+ authenticated = new EventEmitter();
2492
+ mode = 'login';
2493
+ appName = '';
2494
+ logo;
2495
+ subtitle;
2496
+ gradientStyle = 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)';
2497
+ constructor(environment) {
2498
+ this.environment = environment;
2499
+ }
2500
+ ngOnInit() {
2501
+ const branding = this.environment.branding;
2502
+ if (branding) {
2503
+ this.appName = branding.appName || 'Sign In';
2504
+ this.logo = branding.logo;
2505
+ this.subtitle = branding.subtitle;
2506
+ if (branding.gradientStart && branding.gradientEnd) {
2507
+ this.gradientStyle = `linear-gradient(135deg, ${branding.gradientStart} 0%, ${branding.gradientEnd} 100%)`;
2508
+ }
2509
+ else if (branding.primaryColor) {
2510
+ const color = branding.primaryColor;
2511
+ this.gradientStyle = `linear-gradient(135deg, ${color} 0%, ${this.adjustColor(color, -20)} 100%)`;
2512
+ }
2513
+ }
2514
+ else {
2515
+ this.appName = 'Sign In';
2516
+ }
2517
+ }
2518
+ onAuthenticated(event) {
2519
+ this.authenticated.emit(event);
2520
+ }
2521
+ /**
2522
+ * Adjust color brightness (simple implementation)
2523
+ * @param color Hex color (e.g., '#667eea')
2524
+ * @param percent Percentage to darken (negative) or lighten (positive)
2525
+ */
2526
+ adjustColor(color, percent) {
2527
+ const num = parseInt(color.replace('#', ''), 16);
2528
+ const amt = Math.round(2.55 * percent);
2529
+ const R = (num >> 16) + amt;
2530
+ const G = (num >> 8 & 0x00FF) + amt;
2531
+ const B = (num & 0x0000FF) + amt;
2532
+ return '#' + (0x1000000 + (R < 255 ? R < 1 ? 0 : R : 255) * 0x10000 +
2533
+ (G < 255 ? G < 1 ? 0 : G : 255) * 0x100 +
2534
+ (B < 255 ? B < 1 ? 0 : B : 255))
2535
+ .toString(16).slice(1);
2536
+ }
2537
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: AuthPageComponent, deps: [{ token: MyEnvironmentModel }], target: i0.ɵɵFactoryTarget.Component });
2538
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.15", type: AuthPageComponent, isStandalone: true, selector: "lib-auth-page", inputs: { providers: "providers" }, outputs: { authenticated: "authenticated" }, ngImport: i0, template: `
2539
+ <div class="auth-container" [style.background]="gradientStyle">
2540
+ <div class="auth-card">
2541
+ @if (logo) {
2542
+ <img [src]="logo" [alt]="appName + ' logo'" class="logo">
2543
+ }
2544
+ <h1 class="app-name">{{ appName }}</h1>
2545
+ @if (subtitle) {
2546
+ <p class="subtitle">{{ subtitle }}</p>
2547
+ }
2548
+
2549
+ @if (mode === 'login') {
2550
+ <lib-tenant-login
2551
+ [providers]="providers"
2552
+ [allowTenantCreation]="false"
2553
+ (tenantSelected)="onAuthenticated($event)"
2554
+ (createTenant)="mode = 'register'">
2555
+ </lib-tenant-login>
2556
+ } @else {
2557
+ <lib-register
2558
+ (navigateToLogin)="mode = 'login'">
2559
+ </lib-register>
2560
+ }
2561
+ </div>
2562
+ </div>
2563
+ `, isInline: true, styles: [".auth-container{min-height:100vh;display:flex;align-items:center;justify-content:center;padding:20px;background:linear-gradient(135deg,#667eea,#764ba2)}.auth-card{background:#fff;border-radius:12px;box-shadow:0 10px 40px #0000001a;padding:40px;width:100%;max-width:480px}.logo{display:block;max-width:200px;max-height:80px;margin:0 auto 24px}.app-name{margin:0 0 12px;font-size:28px;font-weight:600;text-align:center;color:#1a202c}.subtitle{margin:0 0 32px;font-size:16px;text-align:center;color:#718096}:host ::ng-deep .tenant-login-dialog,:host ::ng-deep .register-dialog{padding:0;max-width:none}:host ::ng-deep .login-title,:host ::ng-deep .register-title{display:none}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: TenantLoginComponent, selector: "lib-tenant-login", inputs: ["title", "providers", "showTenantSelector", "autoSelectSingleTenant", "prefillEmail", "allowTenantCreation", "tenantSelectorTitle", "tenantSelectorDescription", "continueButtonText", "registerLinkText", "registerLinkAction", "createTenantLinkText", "createTenantLinkAction"], outputs: ["tenantSelected", "createTenant"] }, { kind: "component", type: RegisterComponent, selector: "lib-register", outputs: ["navigateToLogin"] }] });
2564
+ }
2565
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: AuthPageComponent, decorators: [{
2566
+ type: Component,
2567
+ args: [{ selector: 'lib-auth-page', standalone: true, imports: [CommonModule, TenantLoginComponent, RegisterComponent], template: `
2568
+ <div class="auth-container" [style.background]="gradientStyle">
2569
+ <div class="auth-card">
2570
+ @if (logo) {
2571
+ <img [src]="logo" [alt]="appName + ' logo'" class="logo">
2572
+ }
2573
+ <h1 class="app-name">{{ appName }}</h1>
2574
+ @if (subtitle) {
2575
+ <p class="subtitle">{{ subtitle }}</p>
2576
+ }
2577
+
2578
+ @if (mode === 'login') {
2579
+ <lib-tenant-login
2580
+ [providers]="providers"
2581
+ [allowTenantCreation]="false"
2582
+ (tenantSelected)="onAuthenticated($event)"
2583
+ (createTenant)="mode = 'register'">
2584
+ </lib-tenant-login>
2585
+ } @else {
2586
+ <lib-register
2587
+ (navigateToLogin)="mode = 'login'">
2588
+ </lib-register>
2589
+ }
2590
+ </div>
2591
+ </div>
2592
+ `, styles: [".auth-container{min-height:100vh;display:flex;align-items:center;justify-content:center;padding:20px;background:linear-gradient(135deg,#667eea,#764ba2)}.auth-card{background:#fff;border-radius:12px;box-shadow:0 10px 40px #0000001a;padding:40px;width:100%;max-width:480px}.logo{display:block;max-width:200px;max-height:80px;margin:0 auto 24px}.app-name{margin:0 0 12px;font-size:28px;font-weight:600;text-align:center;color:#1a202c}.subtitle{margin:0 0 32px;font-size:16px;text-align:center;color:#718096}:host ::ng-deep .tenant-login-dialog,:host ::ng-deep .register-dialog{padding:0;max-width:none}:host ::ng-deep .login-title,:host ::ng-deep .register-title{display:none}\n"] }]
2593
+ }], ctorParameters: () => [{ type: MyEnvironmentModel, decorators: [{
2594
+ type: Inject,
2595
+ args: [MyEnvironmentModel]
2596
+ }] }], propDecorators: { providers: [{
2597
+ type: Input
2598
+ }], authenticated: [{
2599
+ type: Output
2600
+ }] } });
2601
+
2602
+ class LoginDialogComponent {
1759
2603
  auth;
1760
- // Component Configuration
1761
- title = 'Sign In';
1762
- providers = ['google'];
1763
- showTenantSelector = true;
1764
- autoSelectSingleTenant = true;
1765
- allowTenantCreation = true;
1766
- // Tenant Selector Labels
1767
- tenantSelectorTitle = 'Select Organization';
1768
- tenantSelectorDescription = 'Choose which organization you want to access:';
1769
- continueButtonText = 'Continue';
1770
- // Link Labels
1771
- registerLinkText = "Don't have an account?";
1772
- registerLinkAction = 'Sign up';
1773
- createTenantLinkText = "Don't see your organization?";
1774
- createTenantLinkAction = 'Create New Organization';
1775
- // Outputs
1776
- tenantSelected = new EventEmitter();
1777
- createTenant = new EventEmitter();
1778
- // Form Fields
2604
+ /**
2605
+ * REQUIRED: Which authentication providers to show in this dialog
2606
+ * @example ['google', 'linkedin', 'emailPassword']
2607
+ */
2608
+ providers = [];
1779
2609
  email = '';
1780
2610
  password = '';
1781
- // State
1782
2611
  error = '';
1783
2612
  loading = false;
1784
- useOAuth = true;
2613
+ showPassword = false;
1785
2614
  oauthProviders = [];
1786
- // Tenant Selection State
1787
- showingTenantSelector = false;
1788
- memberships = [];
1789
- selectedTenantId = null;
1790
- userName = '';
1791
2615
  constructor(auth) {
1792
2616
  this.auth = auth;
1793
2617
  }
1794
2618
  ngOnInit() {
1795
2619
  if (!this.providers || this.providers.length === 0) {
1796
- this.error = 'Configuration Error: No authentication providers specified.';
1797
- throw new Error('TenantLoginComponent requires providers input.');
1798
- }
1799
- this.oauthProviders = this.providers.filter(p => p !== 'emailPassword');
1800
- // If only emailPassword is available, use it by default
1801
- if (this.oauthProviders.length === 0 && this.isProviderEnabled('emailPassword')) {
1802
- this.useOAuth = false;
2620
+ this.error = 'Configuration Error: No authentication providers specified. Please pass providers to LoginDialogComponent.';
2621
+ throw new Error('LoginDialogComponent requires providers input. Example: dialogRef.componentInstance.providers = [\'google\', \'emailPassword\']');
1803
2622
  }
2623
+ // Get OAuth providers (excluding emailPassword)
2624
+ this.oauthProviders = this.providers
2625
+ .filter(p => p !== 'emailPassword');
1804
2626
  }
1805
2627
  isProviderEnabled(provider) {
1806
2628
  return this.providers.includes(provider);
@@ -1812,519 +2634,241 @@ class TenantLoginComponent {
1812
2634
  apple: 'Sign in with Apple',
1813
2635
  microsoft: 'Sign in with Microsoft',
1814
2636
  github: 'Sign in with GitHub',
2637
+ zoho: 'Sign in with Zoho',
1815
2638
  emailPassword: 'Sign in with Email'
1816
2639
  };
1817
2640
  return labels[provider];
1818
2641
  }
1819
2642
  getProviderIcon(provider) {
1820
- return undefined;
1821
- }
1822
- toggleAuthMethod(event) {
1823
- event.preventDefault();
1824
- this.useOAuth = !this.useOAuth;
1825
- this.error = '';
1826
- }
1827
- async onEmailLogin() {
1828
- if (!this.email || !this.password) {
1829
- this.error = 'Please enter email and password';
1830
- return;
1831
- }
1832
- this.loading = true;
1833
- this.error = '';
1834
- try {
1835
- const result = await this.auth.loginWithEmail(this.email, this.password);
1836
- if (!result.success) {
1837
- this.error = result.message || 'Login failed';
1838
- return;
1839
- }
1840
- // Authentication successful, now handle tenant selection
1841
- await this.handlePostAuthFlow();
1842
- }
1843
- catch (err) {
1844
- this.error = err.message || 'An unexpected error occurred';
1845
- }
1846
- finally {
1847
- this.loading = false;
1848
- }
1849
- }
1850
- async onOAuthLogin(provider) {
1851
- this.loading = true;
1852
- this.error = '';
1853
- try {
1854
- const result = await this.auth.loginWithProvider(provider);
1855
- if (!result.success) {
1856
- this.error = result.message || 'OAuth login failed';
1857
- return;
1858
- }
1859
- // Authentication successful, now handle tenant selection
1860
- await this.handlePostAuthFlow();
1861
- }
1862
- catch (err) {
1863
- this.error = err.message || 'An unexpected error occurred';
1864
- }
1865
- finally {
1866
- this.loading = false;
1867
- }
1868
- }
1869
- async handlePostAuthFlow() {
1870
- if (!this.showTenantSelector) {
1871
- // Tenant selection is disabled, emit event immediately
1872
- this.tenantSelected.emit({
1873
- tenantId: '',
1874
- tenantSlug: '',
1875
- role: ''
1876
- });
1877
- return;
1878
- }
1879
- // Fetch user's tenant memberships
1880
- this.loading = true;
1881
- try {
1882
- const result = await this.auth.getTenantMemberships();
1883
- if (!result.memberships || result.memberships.length === 0) {
1884
- // User has no tenants, prompt to create one
1885
- this.error = 'You are not a member of any organization. Please create one.';
1886
- if (this.allowTenantCreation) {
1887
- setTimeout(() => this.createTenant.emit(), 2000);
1888
- }
1889
- return;
1890
- }
1891
- this.memberships = result.memberships;
1892
- // Get user name if available
1893
- const currentUser = this.auth.getCurrentUser();
1894
- if (currentUser) {
1895
- this.userName = currentUser.display_name || currentUser.email;
1896
- }
1897
- // Auto-select if user has only one tenant
1898
- if (this.memberships.length === 1 && this.autoSelectSingleTenant) {
1899
- await this.selectAndContinue(this.memberships[0]);
1900
- }
1901
- else {
1902
- // Show tenant selector
1903
- this.showingTenantSelector = true;
1904
- }
1905
- }
1906
- catch (err) {
1907
- this.error = err.message || 'Failed to load organizations';
1908
- }
1909
- finally {
1910
- this.loading = false;
1911
- }
1912
- }
1913
- selectTenantItem(tenantId) {
1914
- this.selectedTenantId = tenantId;
1915
- }
1916
- async onContinueWithTenant() {
1917
- if (!this.selectedTenantId) {
1918
- this.error = 'Please select an organization';
1919
- return;
1920
- }
1921
- const membership = this.memberships.find(m => m.tenant_id === this.selectedTenantId);
1922
- if (!membership) {
1923
- this.error = 'Selected organization not found';
2643
+ // Platforms can customize icons via CSS classes: .btn-google, .btn-linkedin, etc.
2644
+ return undefined;
2645
+ }
2646
+ async onEmailLogin() {
2647
+ if (!this.email || !this.password) {
2648
+ this.error = 'Please enter email and password';
1924
2649
  return;
1925
2650
  }
1926
- await this.selectAndContinue(membership);
1927
- }
1928
- async selectAndContinue(membership) {
1929
2651
  this.loading = true;
1930
2652
  this.error = '';
1931
2653
  try {
1932
- const result = await this.auth.selectTenant(membership.tenant_id);
2654
+ const result = await this.auth.loginWithEmail(this.email, this.password);
1933
2655
  if (!result.success) {
1934
- this.error = result.message || 'Failed to select organization';
1935
- return;
2656
+ this.error = result.message || 'Login failed';
1936
2657
  }
1937
- // Emit tenant selected event
1938
- this.tenantSelected.emit({
1939
- tenantId: membership.tenant_id,
1940
- tenantSlug: membership.slug,
1941
- role: membership.role
1942
- });
2658
+ // On success, parent component/dialog should close automatically via user$ subscription
1943
2659
  }
1944
2660
  catch (err) {
1945
- this.error = err.message || 'An unexpected error occurred';
2661
+ this.error = 'An unexpected error occurred';
1946
2662
  }
1947
2663
  finally {
1948
2664
  this.loading = false;
1949
2665
  }
1950
2666
  }
1951
- formatRole(role) {
1952
- return role.charAt(0).toUpperCase() + role.slice(1);
1953
- }
1954
- formatLastAccessed(dateStr) {
2667
+ async onOAuthLogin(provider) {
2668
+ this.loading = true;
2669
+ this.error = '';
1955
2670
  try {
1956
- const date = new Date(dateStr);
1957
- const now = new Date();
1958
- const diffMs = now.getTime() - date.getTime();
1959
- const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
1960
- if (diffDays === 0)
1961
- return 'today';
1962
- if (diffDays === 1)
1963
- return 'yesterday';
1964
- if (diffDays < 7)
1965
- return `${diffDays} days ago`;
1966
- if (diffDays < 30)
1967
- return `${Math.floor(diffDays / 7)} weeks ago`;
1968
- return `${Math.floor(diffDays / 30)} months ago`;
2671
+ const result = await this.auth.loginWithProvider(provider);
2672
+ if (!result.success) {
2673
+ this.error = result.message || 'OAuth login failed';
2674
+ }
2675
+ // On success, parent component/dialog should close automatically via user$ subscription
1969
2676
  }
1970
- catch {
1971
- return dateStr;
2677
+ catch (err) {
2678
+ this.error = 'An unexpected error occurred';
2679
+ }
2680
+ finally {
2681
+ this.loading = false;
1972
2682
  }
1973
2683
  }
1974
- onCreateTenantClick(event) {
2684
+ onRegisterClick(event) {
1975
2685
  event.preventDefault();
1976
- this.createTenant.emit();
2686
+ // Platforms can override this or listen for a custom event
2687
+ // For now, just emit a console message
2688
+ console.log('Register clicked - platform should handle navigation');
1977
2689
  }
1978
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TenantLoginComponent, deps: [{ token: AuthService }], target: i0.ɵɵFactoryTarget.Component });
1979
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.15", type: TenantLoginComponent, isStandalone: true, selector: "lib-tenant-login", inputs: { title: "title", providers: "providers", showTenantSelector: "showTenantSelector", autoSelectSingleTenant: "autoSelectSingleTenant", allowTenantCreation: "allowTenantCreation", tenantSelectorTitle: "tenantSelectorTitle", tenantSelectorDescription: "tenantSelectorDescription", continueButtonText: "continueButtonText", registerLinkText: "registerLinkText", registerLinkAction: "registerLinkAction", createTenantLinkText: "createTenantLinkText", createTenantLinkAction: "createTenantLinkAction" }, outputs: { tenantSelected: "tenantSelected", createTenant: "createTenant" }, ngImport: i0, template: `
1980
- <div class="tenant-login-dialog">
1981
- @if (!showingTenantSelector) {
1982
- <!-- Step 1: Authentication -->
1983
- <h2 class="login-title">{{ title }}</h2>
2690
+ static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: LoginDialogComponent, deps: [{ token: AuthService }], target: i0.ɵɵFactoryTarget.Component });
2691
+ static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.15", type: LoginDialogComponent, isStandalone: true, selector: "lib-login-dialog", inputs: { providers: "providers" }, ngImport: i0, template: `
2692
+ <div class="login-dialog">
2693
+ <h2 class="login-title">Sign In</h2>
1984
2694
 
1985
- <!-- Email/Password Form (if enabled) -->
1986
- @if (isProviderEnabled('emailPassword') && !useOAuth) {
1987
- <form (ngSubmit)="onEmailLogin()" class="email-form">
1988
- <div class="form-group">
1989
- <input
1990
- [(ngModel)]="email"
1991
- name="email"
1992
- placeholder="Email"
1993
- type="email"
1994
- required
1995
- class="form-control">
1996
- </div>
1997
- <div class="form-group">
1998
- <input
1999
- [(ngModel)]="password"
2000
- name="password"
2001
- placeholder="Password"
2002
- type="password"
2003
- required
2004
- class="form-control">
2005
- </div>
2695
+ <!-- Email/Password Form (if enabled) -->
2696
+ @if (isProviderEnabled('emailPassword')) {
2697
+ <form (ngSubmit)="onEmailLogin()" class="email-form">
2698
+ <div class="form-group">
2699
+ <input
2700
+ [(ngModel)]="email"
2701
+ name="email"
2702
+ placeholder="Email"
2703
+ type="email"
2704
+ required
2705
+ class="form-control">
2706
+ </div>
2707
+ <div class="form-group password-group">
2708
+ <input
2709
+ [(ngModel)]="password"
2710
+ name="password"
2711
+ placeholder="Password"
2712
+ [type]="showPassword ? 'text' : 'password'"
2713
+ required
2714
+ class="form-control password-input">
2006
2715
  <button
2007
- type="submit"
2008
- [disabled]="loading"
2009
- class="btn btn-primary btn-block">
2010
- {{ loading ? 'Signing in...' : 'Sign in with Email' }}
2716
+ type="button"
2717
+ class="password-toggle"
2718
+ (click)="showPassword = !showPassword"
2719
+ [attr.aria-label]="showPassword ? 'Hide password' : 'Show password'">
2720
+ {{ showPassword ? '👁️' : '👁️‍🗨️' }}
2011
2721
  </button>
2012
- </form>
2013
-
2014
- <!-- Divider -->
2015
- @if (oauthProviders.length > 0) {
2016
- <div class="divider">
2017
- <span>OR</span>
2018
- </div>
2019
- }
2020
- }
2021
-
2022
- <!-- OAuth Providers -->
2023
- @if (oauthProviders.length > 0 && (useOAuth || !isProviderEnabled('emailPassword'))) {
2024
- <div class="oauth-buttons">
2025
- @for (provider of oauthProviders; track provider) {
2026
- <button
2027
- type="button"
2028
- (click)="onOAuthLogin(provider)"
2029
- [disabled]="loading"
2030
- class="btn btn-oauth btn-{{ provider }}">
2031
- @if (getProviderIcon(provider)) {
2032
- <span class="oauth-icon">
2033
- {{ getProviderIcon(provider) }}
2034
- </span>
2035
- }
2036
- {{ getProviderLabel(provider) }}
2037
- </button>
2038
- }
2039
- </div>
2040
-
2041
- <!-- Switch to Email/Password -->
2042
- @if (isProviderEnabled('emailPassword') && oauthProviders.length > 0) {
2043
- <div class="switch-method">
2044
- <a href="#" (click)="toggleAuthMethod($event)">
2045
- {{ useOAuth ? 'Use email/password instead' : 'Use OAuth instead' }}
2046
- </a>
2047
- </div>
2048
- }
2049
- }
2050
-
2051
- <!-- Error Message -->
2052
- @if (error) {
2053
- <div class="error-message">
2054
- {{ error }}
2055
- </div>
2056
- }
2057
-
2058
- <!-- Register Link -->
2059
- @if (allowTenantCreation) {
2060
- <div class="register-link">
2061
- {{ registerLinkText }}
2062
- <a href="#" (click)="onCreateTenantClick($event)">{{ registerLinkAction }}</a>
2063
- </div>
2064
- }
2065
- } @else {
2066
- <!-- Step 2: Tenant Selection -->
2067
- <h2 class="login-title">{{ tenantSelectorTitle }}</h2>
2068
-
2069
- @if (userName) {
2070
- <div class="welcome-message">
2071
- Welcome back, <strong>{{ userName }}</strong>!
2072
2722
  </div>
2073
- }
2723
+ <button
2724
+ type="submit"
2725
+ [disabled]="loading"
2726
+ class="btn btn-primary btn-block">
2727
+ {{ loading ? 'Signing in...' : getProviderLabel('emailPassword') }}
2728
+ </button>
2729
+ </form>
2730
+ }
2074
2731
 
2075
- <p class="selector-description">{{ tenantSelectorDescription }}</p>
2732
+ <!-- Divider if both email and OAuth are present -->
2733
+ @if (isProviderEnabled('emailPassword') && oauthProviders.length > 0) {
2734
+ <div class="divider">
2735
+ <span>OR</span>
2736
+ </div>
2737
+ }
2076
2738
 
2077
- <div class="tenant-list">
2078
- @for (membership of memberships; track membership.tenant_id) {
2079
- <div
2080
- class="tenant-item"
2081
- [class.selected]="selectedTenantId === membership.tenant_id"
2082
- (click)="selectTenantItem(membership.tenant_id)">
2083
- <div class="tenant-radio">
2084
- <input
2085
- type="radio"
2086
- [checked]="selectedTenantId === membership.tenant_id"
2087
- [name]="'tenant-' + membership.tenant_id"
2088
- [id]="'tenant-' + membership.tenant_id">
2089
- </div>
2090
- <div class="tenant-info">
2091
- <div class="tenant-name">{{ membership.name }}</div>
2092
- <div class="tenant-meta">
2093
- <span class="tenant-role">{{ formatRole(membership.role) }}</span>
2094
- @if (membership.last_accessed) {
2095
- <span class="tenant-separator">·</span>
2096
- <span class="tenant-last-accessed">
2097
- Last accessed {{ formatLastAccessed(membership.last_accessed) }}
2098
- </span>
2099
- }
2100
- </div>
2101
- </div>
2102
- </div>
2739
+ <!-- OAuth Providers -->
2740
+ @if (oauthProviders.length > 0) {
2741
+ <div class="oauth-buttons">
2742
+ @for (provider of oauthProviders; track provider) {
2743
+ <button
2744
+ (click)="onOAuthLogin(provider)"
2745
+ [disabled]="loading"
2746
+ class="btn btn-oauth btn-{{ provider }}">
2747
+ @if (getProviderIcon(provider)) {
2748
+ <span class="oauth-icon">
2749
+ {{ getProviderIcon(provider) }}
2750
+ </span>
2751
+ }
2752
+ {{ getProviderLabel(provider) }}
2753
+ </button>
2103
2754
  }
2104
2755
  </div>
2756
+ }
2105
2757
 
2106
- <button
2107
- type="button"
2108
- (click)="onContinueWithTenant()"
2109
- [disabled]="!selectedTenantId || loading"
2110
- class="btn btn-primary btn-block">
2111
- {{ loading ? 'Loading...' : continueButtonText }}
2112
- </button>
2113
-
2114
- <!-- Error Message -->
2115
- @if (error) {
2116
- <div class="error-message">
2117
- {{ error }}
2118
- </div>
2119
- }
2120
-
2121
- <!-- Create New Tenant Link -->
2122
- @if (allowTenantCreation) {
2123
- <div class="create-tenant-link">
2124
- {{ createTenantLinkText }}
2125
- <a href="#" (click)="onCreateTenantClick($event)">{{ createTenantLinkAction }}</a>
2126
- </div>
2127
- }
2758
+ <!-- Error Message -->
2759
+ @if (error) {
2760
+ <div class="error-message">
2761
+ {{ error }}
2762
+ </div>
2128
2763
  }
2129
2764
 
2130
- <!-- Loading Overlay -->
2765
+ <!-- Loading State -->
2131
2766
  @if (loading) {
2132
2767
  <div class="loading-overlay">
2133
2768
  <div class="spinner"></div>
2134
2769
  </div>
2135
2770
  }
2771
+
2772
+ <!-- Register Link -->
2773
+ <div class="register-link">
2774
+ Don't have an account?
2775
+ <a href="#" (click)="onRegisterClick($event)">Sign up</a>
2776
+ </div>
2136
2777
  </div>
2137
- `, isInline: true, styles: [".tenant-login-dialog{padding:24px;max-width:450px;position:relative}.login-title{margin:0 0 24px;font-size:24px;font-weight:500;text-align:center}.welcome-message{margin-bottom:16px;padding:12px;background:#e8f5e9;border-radius:4px;text-align:center;font-size:14px;color:#2e7d32}.selector-description{margin-bottom:20px;font-size:14px;color:#666;text-align:center}.email-form,.form-group{margin-bottom:16px}.form-control{width:100%;padding:12px;border:1px solid #ddd;border-radius:4px;font-size:14px;box-sizing:border-box}.form-control:focus{outline:none;border-color:#4285f4}.btn{padding:12px 24px;border:none;border-radius:4px;font-size:14px;font-weight:500;cursor:pointer;transition:background-color .2s}.btn:disabled{opacity:.6;cursor:not-allowed}.btn-block{width:100%}.btn-primary{background-color:#4285f4;color:#fff}.btn-primary:hover:not(:disabled){background-color:#357ae8}.divider{margin:16px 0;text-align:center;position:relative}.divider:before{content:\"\";position:absolute;top:50%;left:0;right:0;height:1px;background:#ddd}.divider span{background:#fff;padding:0 12px;position:relative;color:#666;font-size:12px}.oauth-buttons{display:flex;flex-direction:column;gap:12px}.btn-oauth{width:100%;background:#fff;color:#333;border:1px solid #ddd;display:flex;align-items:center;justify-content:center;gap:8px}.btn-oauth:hover:not(:disabled){background:#f8f8f8}.btn-google{border-color:#4285f4}.btn-linkedin{border-color:#0077b5}.btn-apple{border-color:#000}.btn-microsoft{border-color:#00a4ef}.btn-github{border-color:#333}.oauth-icon{font-size:18px}.switch-method{margin-top:12px;text-align:center;font-size:14px}.switch-method a{color:#4285f4;text-decoration:none}.switch-method a:hover{text-decoration:underline}.tenant-list{margin-bottom:20px;display:flex;flex-direction:column;gap:12px}.tenant-item{display:flex;align-items:flex-start;gap:12px;padding:16px;border:2px solid #e0e0e0;border-radius:6px;cursor:pointer;transition:all .2s}.tenant-item:hover{border-color:#4285f4;background:#f8f9ff}.tenant-item.selected{border-color:#4285f4;background:#e8f0fe}.tenant-radio{flex-shrink:0;padding-top:2px}.tenant-radio input[type=radio]{width:18px;height:18px;cursor:pointer}.tenant-info{flex:1}.tenant-name{font-size:16px;font-weight:500;color:#333;margin-bottom:4px}.tenant-meta{font-size:13px;color:#666}.tenant-role{font-weight:500;color:#4285f4}.tenant-separator{margin:0 6px}.error-message{margin-top:16px;padding:12px;background:#fee;color:#c33;border-radius:4px;font-size:14px}.loading-overlay{position:absolute;inset:0;background:#fffc;display:flex;align-items:center;justify-content:center}.spinner{width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #4285f4;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.register-link{margin-top:16px;text-align:center;font-size:14px;color:#666}.register-link a{color:#4285f4;text-decoration:none}.register-link a:hover{text-decoration:underline}.create-tenant-link{margin-top:16px;padding-top:16px;border-top:1px solid #e0e0e0;text-align:center;font-size:14px;color:#666}.create-tenant-link a{color:#4285f4;text-decoration:none;font-weight:500}.create-tenant-link a:hover{text-decoration:underline}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i2.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }] });
2778
+ `, isInline: true, styles: [".login-dialog{padding:24px;max-width:400px;position:relative}.login-title{margin:0 0 24px;font-size:24px;font-weight:500;text-align:center}.email-form,.form-group{margin-bottom:16px}.password-group{position:relative}.form-control{width:100%;padding:12px;border:1px solid #ddd;border-radius:4px;font-size:14px;box-sizing:border-box}.password-input{padding-right:45px}.password-toggle{position:absolute;right:8px;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;font-size:18px;padding:8px;line-height:1;opacity:.6;transition:opacity .2s}.password-toggle:hover{opacity:1}.password-toggle:focus{outline:2px solid #4285f4;outline-offset:2px;border-radius:4px}.form-control:focus{outline:none;border-color:#4285f4}.btn{padding:12px 24px;border:none;border-radius:4px;font-size:14px;font-weight:500;cursor:pointer;transition:background-color .2s}.btn:disabled{opacity:.6;cursor:not-allowed}.btn-block{width:100%}.btn-primary{background-color:#4285f4;color:#fff}.btn-primary:hover:not(:disabled){background-color:#357ae8}.divider{margin:16px 0;text-align:center;position:relative}.divider:before{content:\"\";position:absolute;top:50%;left:0;right:0;height:1px;background:#ddd}.divider span{background:#fff;padding:0 12px;position:relative;color:#666;font-size:12px}.oauth-buttons{display:flex;flex-direction:column;gap:12px}.btn-oauth{width:100%;background:#fff;color:#333;border:1px solid #ddd;display:flex;align-items:center;justify-content:center;gap:8px}.btn-oauth:hover:not(:disabled){background:#f8f8f8}.btn-google{border-color:#4285f4}.btn-linkedin{border-color:#0077b5}.btn-apple{border-color:#000}.btn-microsoft{border-color:#00a4ef}.btn-github{border-color:#333}.oauth-icon{font-size:18px}.error-message{margin-top:16px;padding:12px;background:#fee;color:#c33;border-radius:4px;font-size:14px}.loading-overlay{position:absolute;inset:0;background:#fffc;display:flex;align-items:center;justify-content:center}.spinner{width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #4285f4;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.register-link{margin-top:16px;text-align:center;font-size:14px;color:#666}.register-link a{color:#4285f4;text-decoration:none}.register-link a:hover{text-decoration:underline}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i2.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }] });
2138
2779
  }
2139
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TenantLoginComponent, decorators: [{
2780
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: LoginDialogComponent, decorators: [{
2140
2781
  type: Component,
2141
- args: [{ selector: 'lib-tenant-login', standalone: true, imports: [CommonModule, FormsModule], template: `
2142
- <div class="tenant-login-dialog">
2143
- @if (!showingTenantSelector) {
2144
- <!-- Step 1: Authentication -->
2145
- <h2 class="login-title">{{ title }}</h2>
2782
+ args: [{ selector: 'lib-login-dialog', standalone: true, imports: [CommonModule, FormsModule], template: `
2783
+ <div class="login-dialog">
2784
+ <h2 class="login-title">Sign In</h2>
2146
2785
 
2147
- <!-- Email/Password Form (if enabled) -->
2148
- @if (isProviderEnabled('emailPassword') && !useOAuth) {
2149
- <form (ngSubmit)="onEmailLogin()" class="email-form">
2150
- <div class="form-group">
2151
- <input
2152
- [(ngModel)]="email"
2153
- name="email"
2154
- placeholder="Email"
2155
- type="email"
2156
- required
2157
- class="form-control">
2158
- </div>
2159
- <div class="form-group">
2160
- <input
2161
- [(ngModel)]="password"
2162
- name="password"
2163
- placeholder="Password"
2164
- type="password"
2165
- required
2166
- class="form-control">
2167
- </div>
2786
+ <!-- Email/Password Form (if enabled) -->
2787
+ @if (isProviderEnabled('emailPassword')) {
2788
+ <form (ngSubmit)="onEmailLogin()" class="email-form">
2789
+ <div class="form-group">
2790
+ <input
2791
+ [(ngModel)]="email"
2792
+ name="email"
2793
+ placeholder="Email"
2794
+ type="email"
2795
+ required
2796
+ class="form-control">
2797
+ </div>
2798
+ <div class="form-group password-group">
2799
+ <input
2800
+ [(ngModel)]="password"
2801
+ name="password"
2802
+ placeholder="Password"
2803
+ [type]="showPassword ? 'text' : 'password'"
2804
+ required
2805
+ class="form-control password-input">
2168
2806
  <button
2169
- type="submit"
2170
- [disabled]="loading"
2171
- class="btn btn-primary btn-block">
2172
- {{ loading ? 'Signing in...' : 'Sign in with Email' }}
2807
+ type="button"
2808
+ class="password-toggle"
2809
+ (click)="showPassword = !showPassword"
2810
+ [attr.aria-label]="showPassword ? 'Hide password' : 'Show password'">
2811
+ {{ showPassword ? '👁️' : '👁️‍🗨️' }}
2173
2812
  </button>
2174
- </form>
2175
-
2176
- <!-- Divider -->
2177
- @if (oauthProviders.length > 0) {
2178
- <div class="divider">
2179
- <span>OR</span>
2180
- </div>
2181
- }
2182
- }
2183
-
2184
- <!-- OAuth Providers -->
2185
- @if (oauthProviders.length > 0 && (useOAuth || !isProviderEnabled('emailPassword'))) {
2186
- <div class="oauth-buttons">
2187
- @for (provider of oauthProviders; track provider) {
2188
- <button
2189
- type="button"
2190
- (click)="onOAuthLogin(provider)"
2191
- [disabled]="loading"
2192
- class="btn btn-oauth btn-{{ provider }}">
2193
- @if (getProviderIcon(provider)) {
2194
- <span class="oauth-icon">
2195
- {{ getProviderIcon(provider) }}
2196
- </span>
2197
- }
2198
- {{ getProviderLabel(provider) }}
2199
- </button>
2200
- }
2201
- </div>
2202
-
2203
- <!-- Switch to Email/Password -->
2204
- @if (isProviderEnabled('emailPassword') && oauthProviders.length > 0) {
2205
- <div class="switch-method">
2206
- <a href="#" (click)="toggleAuthMethod($event)">
2207
- {{ useOAuth ? 'Use email/password instead' : 'Use OAuth instead' }}
2208
- </a>
2209
- </div>
2210
- }
2211
- }
2212
-
2213
- <!-- Error Message -->
2214
- @if (error) {
2215
- <div class="error-message">
2216
- {{ error }}
2217
- </div>
2218
- }
2219
-
2220
- <!-- Register Link -->
2221
- @if (allowTenantCreation) {
2222
- <div class="register-link">
2223
- {{ registerLinkText }}
2224
- <a href="#" (click)="onCreateTenantClick($event)">{{ registerLinkAction }}</a>
2225
- </div>
2226
- }
2227
- } @else {
2228
- <!-- Step 2: Tenant Selection -->
2229
- <h2 class="login-title">{{ tenantSelectorTitle }}</h2>
2230
-
2231
- @if (userName) {
2232
- <div class="welcome-message">
2233
- Welcome back, <strong>{{ userName }}</strong>!
2234
2813
  </div>
2235
- }
2814
+ <button
2815
+ type="submit"
2816
+ [disabled]="loading"
2817
+ class="btn btn-primary btn-block">
2818
+ {{ loading ? 'Signing in...' : getProviderLabel('emailPassword') }}
2819
+ </button>
2820
+ </form>
2821
+ }
2236
2822
 
2237
- <p class="selector-description">{{ tenantSelectorDescription }}</p>
2823
+ <!-- Divider if both email and OAuth are present -->
2824
+ @if (isProviderEnabled('emailPassword') && oauthProviders.length > 0) {
2825
+ <div class="divider">
2826
+ <span>OR</span>
2827
+ </div>
2828
+ }
2238
2829
 
2239
- <div class="tenant-list">
2240
- @for (membership of memberships; track membership.tenant_id) {
2241
- <div
2242
- class="tenant-item"
2243
- [class.selected]="selectedTenantId === membership.tenant_id"
2244
- (click)="selectTenantItem(membership.tenant_id)">
2245
- <div class="tenant-radio">
2246
- <input
2247
- type="radio"
2248
- [checked]="selectedTenantId === membership.tenant_id"
2249
- [name]="'tenant-' + membership.tenant_id"
2250
- [id]="'tenant-' + membership.tenant_id">
2251
- </div>
2252
- <div class="tenant-info">
2253
- <div class="tenant-name">{{ membership.name }}</div>
2254
- <div class="tenant-meta">
2255
- <span class="tenant-role">{{ formatRole(membership.role) }}</span>
2256
- @if (membership.last_accessed) {
2257
- <span class="tenant-separator">·</span>
2258
- <span class="tenant-last-accessed">
2259
- Last accessed {{ formatLastAccessed(membership.last_accessed) }}
2260
- </span>
2261
- }
2262
- </div>
2263
- </div>
2264
- </div>
2830
+ <!-- OAuth Providers -->
2831
+ @if (oauthProviders.length > 0) {
2832
+ <div class="oauth-buttons">
2833
+ @for (provider of oauthProviders; track provider) {
2834
+ <button
2835
+ (click)="onOAuthLogin(provider)"
2836
+ [disabled]="loading"
2837
+ class="btn btn-oauth btn-{{ provider }}">
2838
+ @if (getProviderIcon(provider)) {
2839
+ <span class="oauth-icon">
2840
+ {{ getProviderIcon(provider) }}
2841
+ </span>
2842
+ }
2843
+ {{ getProviderLabel(provider) }}
2844
+ </button>
2265
2845
  }
2266
2846
  </div>
2847
+ }
2267
2848
 
2268
- <button
2269
- type="button"
2270
- (click)="onContinueWithTenant()"
2271
- [disabled]="!selectedTenantId || loading"
2272
- class="btn btn-primary btn-block">
2273
- {{ loading ? 'Loading...' : continueButtonText }}
2274
- </button>
2275
-
2276
- <!-- Error Message -->
2277
- @if (error) {
2278
- <div class="error-message">
2279
- {{ error }}
2280
- </div>
2281
- }
2282
-
2283
- <!-- Create New Tenant Link -->
2284
- @if (allowTenantCreation) {
2285
- <div class="create-tenant-link">
2286
- {{ createTenantLinkText }}
2287
- <a href="#" (click)="onCreateTenantClick($event)">{{ createTenantLinkAction }}</a>
2288
- </div>
2289
- }
2849
+ <!-- Error Message -->
2850
+ @if (error) {
2851
+ <div class="error-message">
2852
+ {{ error }}
2853
+ </div>
2290
2854
  }
2291
2855
 
2292
- <!-- Loading Overlay -->
2856
+ <!-- Loading State -->
2293
2857
  @if (loading) {
2294
2858
  <div class="loading-overlay">
2295
2859
  <div class="spinner"></div>
2296
2860
  </div>
2297
2861
  }
2862
+
2863
+ <!-- Register Link -->
2864
+ <div class="register-link">
2865
+ Don't have an account?
2866
+ <a href="#" (click)="onRegisterClick($event)">Sign up</a>
2867
+ </div>
2298
2868
  </div>
2299
- `, styles: [".tenant-login-dialog{padding:24px;max-width:450px;position:relative}.login-title{margin:0 0 24px;font-size:24px;font-weight:500;text-align:center}.welcome-message{margin-bottom:16px;padding:12px;background:#e8f5e9;border-radius:4px;text-align:center;font-size:14px;color:#2e7d32}.selector-description{margin-bottom:20px;font-size:14px;color:#666;text-align:center}.email-form,.form-group{margin-bottom:16px}.form-control{width:100%;padding:12px;border:1px solid #ddd;border-radius:4px;font-size:14px;box-sizing:border-box}.form-control:focus{outline:none;border-color:#4285f4}.btn{padding:12px 24px;border:none;border-radius:4px;font-size:14px;font-weight:500;cursor:pointer;transition:background-color .2s}.btn:disabled{opacity:.6;cursor:not-allowed}.btn-block{width:100%}.btn-primary{background-color:#4285f4;color:#fff}.btn-primary:hover:not(:disabled){background-color:#357ae8}.divider{margin:16px 0;text-align:center;position:relative}.divider:before{content:\"\";position:absolute;top:50%;left:0;right:0;height:1px;background:#ddd}.divider span{background:#fff;padding:0 12px;position:relative;color:#666;font-size:12px}.oauth-buttons{display:flex;flex-direction:column;gap:12px}.btn-oauth{width:100%;background:#fff;color:#333;border:1px solid #ddd;display:flex;align-items:center;justify-content:center;gap:8px}.btn-oauth:hover:not(:disabled){background:#f8f8f8}.btn-google{border-color:#4285f4}.btn-linkedin{border-color:#0077b5}.btn-apple{border-color:#000}.btn-microsoft{border-color:#00a4ef}.btn-github{border-color:#333}.oauth-icon{font-size:18px}.switch-method{margin-top:12px;text-align:center;font-size:14px}.switch-method a{color:#4285f4;text-decoration:none}.switch-method a:hover{text-decoration:underline}.tenant-list{margin-bottom:20px;display:flex;flex-direction:column;gap:12px}.tenant-item{display:flex;align-items:flex-start;gap:12px;padding:16px;border:2px solid #e0e0e0;border-radius:6px;cursor:pointer;transition:all .2s}.tenant-item:hover{border-color:#4285f4;background:#f8f9ff}.tenant-item.selected{border-color:#4285f4;background:#e8f0fe}.tenant-radio{flex-shrink:0;padding-top:2px}.tenant-radio input[type=radio]{width:18px;height:18px;cursor:pointer}.tenant-info{flex:1}.tenant-name{font-size:16px;font-weight:500;color:#333;margin-bottom:4px}.tenant-meta{font-size:13px;color:#666}.tenant-role{font-weight:500;color:#4285f4}.tenant-separator{margin:0 6px}.error-message{margin-top:16px;padding:12px;background:#fee;color:#c33;border-radius:4px;font-size:14px}.loading-overlay{position:absolute;inset:0;background:#fffc;display:flex;align-items:center;justify-content:center}.spinner{width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #4285f4;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.register-link{margin-top:16px;text-align:center;font-size:14px;color:#666}.register-link a{color:#4285f4;text-decoration:none}.register-link a:hover{text-decoration:underline}.create-tenant-link{margin-top:16px;padding-top:16px;border-top:1px solid #e0e0e0;text-align:center;font-size:14px;color:#666}.create-tenant-link a{color:#4285f4;text-decoration:none;font-weight:500}.create-tenant-link a:hover{text-decoration:underline}\n"] }]
2300
- }], ctorParameters: () => [{ type: AuthService }], propDecorators: { title: [{
2301
- type: Input
2302
- }], providers: [{
2303
- type: Input
2304
- }], showTenantSelector: [{
2305
- type: Input
2306
- }], autoSelectSingleTenant: [{
2307
- type: Input
2308
- }], allowTenantCreation: [{
2309
- type: Input
2310
- }], tenantSelectorTitle: [{
2311
- type: Input
2312
- }], tenantSelectorDescription: [{
2313
- type: Input
2314
- }], continueButtonText: [{
2315
- type: Input
2316
- }], registerLinkText: [{
2317
- type: Input
2318
- }], registerLinkAction: [{
2319
- type: Input
2320
- }], createTenantLinkText: [{
2321
- type: Input
2322
- }], createTenantLinkAction: [{
2869
+ `, styles: [".login-dialog{padding:24px;max-width:400px;position:relative}.login-title{margin:0 0 24px;font-size:24px;font-weight:500;text-align:center}.email-form,.form-group{margin-bottom:16px}.password-group{position:relative}.form-control{width:100%;padding:12px;border:1px solid #ddd;border-radius:4px;font-size:14px;box-sizing:border-box}.password-input{padding-right:45px}.password-toggle{position:absolute;right:8px;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;font-size:18px;padding:8px;line-height:1;opacity:.6;transition:opacity .2s}.password-toggle:hover{opacity:1}.password-toggle:focus{outline:2px solid #4285f4;outline-offset:2px;border-radius:4px}.form-control:focus{outline:none;border-color:#4285f4}.btn{padding:12px 24px;border:none;border-radius:4px;font-size:14px;font-weight:500;cursor:pointer;transition:background-color .2s}.btn:disabled{opacity:.6;cursor:not-allowed}.btn-block{width:100%}.btn-primary{background-color:#4285f4;color:#fff}.btn-primary:hover:not(:disabled){background-color:#357ae8}.divider{margin:16px 0;text-align:center;position:relative}.divider:before{content:\"\";position:absolute;top:50%;left:0;right:0;height:1px;background:#ddd}.divider span{background:#fff;padding:0 12px;position:relative;color:#666;font-size:12px}.oauth-buttons{display:flex;flex-direction:column;gap:12px}.btn-oauth{width:100%;background:#fff;color:#333;border:1px solid #ddd;display:flex;align-items:center;justify-content:center;gap:8px}.btn-oauth:hover:not(:disabled){background:#f8f8f8}.btn-google{border-color:#4285f4}.btn-linkedin{border-color:#0077b5}.btn-apple{border-color:#000}.btn-microsoft{border-color:#00a4ef}.btn-github{border-color:#333}.oauth-icon{font-size:18px}.error-message{margin-top:16px;padding:12px;background:#fee;color:#c33;border-radius:4px;font-size:14px}.loading-overlay{position:absolute;inset:0;background:#fffc;display:flex;align-items:center;justify-content:center}.spinner{width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #4285f4;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.register-link{margin-top:16px;text-align:center;font-size:14px;color:#666}.register-link a{color:#4285f4;text-decoration:none}.register-link a:hover{text-decoration:underline}\n"] }]
2870
+ }], ctorParameters: () => [{ type: AuthService }], propDecorators: { providers: [{
2323
2871
  type: Input
2324
- }], tenantSelected: [{
2325
- type: Output
2326
- }], createTenant: [{
2327
- type: Output
2328
2872
  }] } });
2329
2873
 
2330
2874
  class TenantRegisterComponent {
@@ -2371,6 +2915,8 @@ class TenantRegisterComponent {
2371
2915
  slugError = '';
2372
2916
  useEmailPassword = false;
2373
2917
  oauthProviders = [];
2918
+ showPassword = false;
2919
+ showConfirmPassword = false;
2374
2920
  constructor(auth) {
2375
2921
  this.auth = auth;
2376
2922
  }
@@ -2395,6 +2941,7 @@ class TenantRegisterComponent {
2395
2941
  apple: 'Sign up with Apple',
2396
2942
  microsoft: 'Sign up with Microsoft',
2397
2943
  github: 'Sign up with GitHub',
2944
+ zoho: 'Sign up with Zoho',
2398
2945
  emailPassword: 'Sign up with Email'
2399
2946
  };
2400
2947
  return labels[provider];
@@ -2668,30 +3215,44 @@ class TenantRegisterComponent {
2668
3215
  class="form-control">
2669
3216
  </div>
2670
3217
 
2671
- <div class="form-group">
3218
+ <div class="form-group password-group">
2672
3219
  <label for="password">Password *</label>
2673
3220
  <input
2674
3221
  id="password"
2675
3222
  [(ngModel)]="password"
2676
3223
  name="password"
2677
3224
  placeholder="Create a password"
2678
- type="password"
3225
+ [type]="showPassword ? 'text' : 'password'"
2679
3226
  required
2680
3227
  minlength="8"
2681
- class="form-control">
3228
+ class="form-control password-input">
3229
+ <button
3230
+ type="button"
3231
+ class="password-toggle"
3232
+ (click)="showPassword = !showPassword"
3233
+ [attr.aria-label]="showPassword ? 'Hide password' : 'Show password'">
3234
+ {{ showPassword ? '👁️' : '👁️‍🗨️' }}
3235
+ </button>
2682
3236
  <small class="form-hint">At least 8 characters</small>
2683
3237
  </div>
2684
3238
 
2685
- <div class="form-group">
3239
+ <div class="form-group password-group">
2686
3240
  <label for="confirmPassword">Confirm Password *</label>
2687
3241
  <input
2688
3242
  id="confirmPassword"
2689
3243
  [(ngModel)]="confirmPassword"
2690
3244
  name="confirmPassword"
2691
3245
  placeholder="Confirm your password"
2692
- type="password"
3246
+ [type]="showConfirmPassword ? 'text' : 'password'"
2693
3247
  required
2694
- class="form-control">
3248
+ class="form-control password-input">
3249
+ <button
3250
+ type="button"
3251
+ class="password-toggle"
3252
+ (click)="showConfirmPassword = !showConfirmPassword"
3253
+ [attr.aria-label]="showConfirmPassword ? 'Hide password' : 'Show password'">
3254
+ {{ showConfirmPassword ? '👁️' : '👁️‍🗨️' }}
3255
+ </button>
2695
3256
  </div>
2696
3257
 
2697
3258
  <button
@@ -2740,7 +3301,7 @@ class TenantRegisterComponent {
2740
3301
  <a href="#" (click)="onLoginClick($event)">{{ loginLinkAction }}</a>
2741
3302
  </div>
2742
3303
  </div>
2743
- `, isInline: true, styles: [".tenant-register-dialog{padding:24px;max-width:500px;position:relative}.register-title{margin:0 0 20px;font-size:24px;font-weight:500;text-align:center}.warning-box{display:flex;gap:12px;padding:16px;background:#fff3cd;border:1px solid #ffc107;border-radius:6px;margin-bottom:24px}.warning-icon{font-size:24px;line-height:1}.warning-content{flex:1}.warning-content strong{display:block;margin-bottom:4px;color:#856404;font-size:14px}.warning-content p{margin:0;color:#856404;font-size:13px;line-height:1.5}.section-header{font-size:16px;font-weight:600;margin:20px 0 12px;padding-bottom:8px;border-bottom:2px solid #e0e0e0;color:#333}.register-form,.form-group{margin-bottom:16px}.form-group label{display:block;margin-bottom:6px;font-size:14px;font-weight:500;color:#333}.form-control{width:100%;padding:12px;border:1px solid #ddd;border-radius:4px;font-size:14px;box-sizing:border-box;transition:border-color .2s}.form-control:focus{outline:none;border-color:#4285f4}.form-control.input-error{border-color:#dc3545}.form-control.input-success{border-color:#28a745}.form-hint{display:block;margin-top:4px;font-size:12px;color:#666}.form-hint .checking{color:#666}.form-hint .available{color:#28a745;font-weight:500}.form-hint .error-text{color:#dc3545}.oauth-section{margin:16px 0}.oauth-description{margin-bottom:12px;font-size:14px;color:#666;text-align:center}.oauth-buttons{display:flex;flex-direction:column;gap:12px}.btn{padding:12px 24px;border:none;border-radius:4px;font-size:14px;font-weight:500;cursor:pointer;transition:background-color .2s,opacity .2s}.btn:disabled{opacity:.6;cursor:not-allowed}.btn-block{width:100%}.btn-primary{background-color:#4285f4;color:#fff}.btn-primary:hover:not(:disabled){background-color:#357ae8}.btn-oauth{width:100%;background:#fff;color:#333;border:1px solid #ddd;display:flex;align-items:center;justify-content:center;gap:8px}.btn-oauth:hover:not(:disabled){background:#f8f8f8}.btn-google{border-color:#4285f4}.btn-linkedin{border-color:#0077b5}.btn-apple{border-color:#000}.btn-microsoft{border-color:#00a4ef}.btn-github{border-color:#333}.oauth-icon{font-size:18px}.switch-method{margin-top:12px;text-align:center;font-size:14px}.switch-method a{color:#4285f4;text-decoration:none}.switch-method a:hover{text-decoration:underline}.error-message{margin-top:16px;padding:12px;background:#fee;color:#c33;border-radius:4px;font-size:14px}.success-message{margin-top:16px;padding:12px;background:#efe;color:#3a3;border-radius:4px;font-size:14px}.loading-overlay{position:absolute;inset:0;background:#fffffff2;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:16px}.spinner{width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #4285f4;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.loading-text{margin:0;font-size:14px;color:#666}.login-link{margin-top:16px;text-align:center;font-size:14px;color:#666}.login-link a{color:#4285f4;text-decoration:none}.login-link a:hover{text-decoration:underline}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i2.MinLengthValidator, selector: "[minlength][formControlName],[minlength][formControl],[minlength][ngModel]", inputs: ["minlength"] }, { kind: "directive", type: i2.PatternValidator, selector: "[pattern][formControlName],[pattern][formControl],[pattern][ngModel]", inputs: ["pattern"] }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i2.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }] });
3304
+ `, isInline: true, styles: [".tenant-register-dialog{padding:24px;max-width:500px;position:relative}.register-title{margin:0 0 20px;font-size:24px;font-weight:500;text-align:center}.warning-box{display:flex;gap:12px;padding:16px;background:#fff3cd;border:1px solid #ffc107;border-radius:6px;margin-bottom:24px}.warning-icon{font-size:24px;line-height:1}.warning-content{flex:1}.warning-content strong{display:block;margin-bottom:4px;color:#856404;font-size:14px}.warning-content p{margin:0;color:#856404;font-size:13px;line-height:1.5}.section-header{font-size:16px;font-weight:600;margin:20px 0 12px;padding-bottom:8px;border-bottom:2px solid #e0e0e0;color:#333}.register-form,.form-group{margin-bottom:16px}.password-group{position:relative}.form-group label{display:block;margin-bottom:6px;font-size:14px;font-weight:500;color:#333}.form-control{width:100%;padding:12px;border:1px solid #ddd;border-radius:4px;font-size:14px;box-sizing:border-box;transition:border-color .2s}.password-input{padding-right:45px}.password-toggle{position:absolute;right:8px;top:38px;background:none;border:none;cursor:pointer;font-size:18px;padding:8px;line-height:1;opacity:.6;transition:opacity .2s}.password-toggle:hover{opacity:1}.password-toggle:focus{outline:2px solid #4285f4;outline-offset:2px;border-radius:4px}.form-control:focus{outline:none;border-color:#4285f4}.form-control.input-error{border-color:#dc3545}.form-control.input-success{border-color:#28a745}.form-hint{display:block;margin-top:4px;font-size:12px;color:#666}.form-hint .checking{color:#666}.form-hint .available{color:#28a745;font-weight:500}.form-hint .error-text{color:#dc3545}.oauth-section{margin:16px 0}.oauth-description{margin-bottom:12px;font-size:14px;color:#666;text-align:center}.oauth-buttons{display:flex;flex-direction:column;gap:12px}.btn{padding:12px 24px;border:none;border-radius:4px;font-size:14px;font-weight:500;cursor:pointer;transition:background-color .2s,opacity .2s}.btn:disabled{opacity:.6;cursor:not-allowed}.btn-block{width:100%}.btn-primary{background-color:#4285f4;color:#fff}.btn-primary:hover:not(:disabled){background-color:#357ae8}.btn-oauth{width:100%;background:#fff;color:#333;border:1px solid #ddd;display:flex;align-items:center;justify-content:center;gap:8px}.btn-oauth:hover:not(:disabled){background:#f8f8f8}.btn-google{border-color:#4285f4}.btn-linkedin{border-color:#0077b5}.btn-apple{border-color:#000}.btn-microsoft{border-color:#00a4ef}.btn-github{border-color:#333}.oauth-icon{font-size:18px}.switch-method{margin-top:12px;text-align:center;font-size:14px}.switch-method a{color:#4285f4;text-decoration:none}.switch-method a:hover{text-decoration:underline}.error-message{margin-top:16px;padding:12px;background:#fee;color:#c33;border-radius:4px;font-size:14px}.success-message{margin-top:16px;padding:12px;background:#efe;color:#3a3;border-radius:4px;font-size:14px}.loading-overlay{position:absolute;inset:0;background:#fffffff2;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:16px}.spinner{width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #4285f4;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.loading-text{margin:0;font-size:14px;color:#666}.login-link{margin-top:16px;text-align:center;font-size:14px;color:#666}.login-link a{color:#4285f4;text-decoration:none}.login-link a:hover{text-decoration:underline}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i2.MinLengthValidator, selector: "[minlength][formControlName],[minlength][formControl],[minlength][ngModel]", inputs: ["minlength"] }, { kind: "directive", type: i2.PatternValidator, selector: "[pattern][formControlName],[pattern][formControl],[pattern][ngModel]", inputs: ["pattern"] }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i2.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }] });
2744
3305
  }
2745
3306
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TenantRegisterComponent, decorators: [{
2746
3307
  type: Component,
@@ -2865,30 +3426,44 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
2865
3426
  class="form-control">
2866
3427
  </div>
2867
3428
 
2868
- <div class="form-group">
3429
+ <div class="form-group password-group">
2869
3430
  <label for="password">Password *</label>
2870
3431
  <input
2871
3432
  id="password"
2872
3433
  [(ngModel)]="password"
2873
3434
  name="password"
2874
3435
  placeholder="Create a password"
2875
- type="password"
3436
+ [type]="showPassword ? 'text' : 'password'"
2876
3437
  required
2877
3438
  minlength="8"
2878
- class="form-control">
3439
+ class="form-control password-input">
3440
+ <button
3441
+ type="button"
3442
+ class="password-toggle"
3443
+ (click)="showPassword = !showPassword"
3444
+ [attr.aria-label]="showPassword ? 'Hide password' : 'Show password'">
3445
+ {{ showPassword ? '👁️' : '👁️‍🗨️' }}
3446
+ </button>
2879
3447
  <small class="form-hint">At least 8 characters</small>
2880
3448
  </div>
2881
3449
 
2882
- <div class="form-group">
3450
+ <div class="form-group password-group">
2883
3451
  <label for="confirmPassword">Confirm Password *</label>
2884
3452
  <input
2885
3453
  id="confirmPassword"
2886
3454
  [(ngModel)]="confirmPassword"
2887
3455
  name="confirmPassword"
2888
3456
  placeholder="Confirm your password"
2889
- type="password"
3457
+ [type]="showConfirmPassword ? 'text' : 'password'"
2890
3458
  required
2891
- class="form-control">
3459
+ class="form-control password-input">
3460
+ <button
3461
+ type="button"
3462
+ class="password-toggle"
3463
+ (click)="showConfirmPassword = !showConfirmPassword"
3464
+ [attr.aria-label]="showConfirmPassword ? 'Hide password' : 'Show password'">
3465
+ {{ showConfirmPassword ? '👁️' : '👁️‍🗨️' }}
3466
+ </button>
2892
3467
  </div>
2893
3468
 
2894
3469
  <button
@@ -2937,7 +3512,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
2937
3512
  <a href="#" (click)="onLoginClick($event)">{{ loginLinkAction }}</a>
2938
3513
  </div>
2939
3514
  </div>
2940
- `, styles: [".tenant-register-dialog{padding:24px;max-width:500px;position:relative}.register-title{margin:0 0 20px;font-size:24px;font-weight:500;text-align:center}.warning-box{display:flex;gap:12px;padding:16px;background:#fff3cd;border:1px solid #ffc107;border-radius:6px;margin-bottom:24px}.warning-icon{font-size:24px;line-height:1}.warning-content{flex:1}.warning-content strong{display:block;margin-bottom:4px;color:#856404;font-size:14px}.warning-content p{margin:0;color:#856404;font-size:13px;line-height:1.5}.section-header{font-size:16px;font-weight:600;margin:20px 0 12px;padding-bottom:8px;border-bottom:2px solid #e0e0e0;color:#333}.register-form,.form-group{margin-bottom:16px}.form-group label{display:block;margin-bottom:6px;font-size:14px;font-weight:500;color:#333}.form-control{width:100%;padding:12px;border:1px solid #ddd;border-radius:4px;font-size:14px;box-sizing:border-box;transition:border-color .2s}.form-control:focus{outline:none;border-color:#4285f4}.form-control.input-error{border-color:#dc3545}.form-control.input-success{border-color:#28a745}.form-hint{display:block;margin-top:4px;font-size:12px;color:#666}.form-hint .checking{color:#666}.form-hint .available{color:#28a745;font-weight:500}.form-hint .error-text{color:#dc3545}.oauth-section{margin:16px 0}.oauth-description{margin-bottom:12px;font-size:14px;color:#666;text-align:center}.oauth-buttons{display:flex;flex-direction:column;gap:12px}.btn{padding:12px 24px;border:none;border-radius:4px;font-size:14px;font-weight:500;cursor:pointer;transition:background-color .2s,opacity .2s}.btn:disabled{opacity:.6;cursor:not-allowed}.btn-block{width:100%}.btn-primary{background-color:#4285f4;color:#fff}.btn-primary:hover:not(:disabled){background-color:#357ae8}.btn-oauth{width:100%;background:#fff;color:#333;border:1px solid #ddd;display:flex;align-items:center;justify-content:center;gap:8px}.btn-oauth:hover:not(:disabled){background:#f8f8f8}.btn-google{border-color:#4285f4}.btn-linkedin{border-color:#0077b5}.btn-apple{border-color:#000}.btn-microsoft{border-color:#00a4ef}.btn-github{border-color:#333}.oauth-icon{font-size:18px}.switch-method{margin-top:12px;text-align:center;font-size:14px}.switch-method a{color:#4285f4;text-decoration:none}.switch-method a:hover{text-decoration:underline}.error-message{margin-top:16px;padding:12px;background:#fee;color:#c33;border-radius:4px;font-size:14px}.success-message{margin-top:16px;padding:12px;background:#efe;color:#3a3;border-radius:4px;font-size:14px}.loading-overlay{position:absolute;inset:0;background:#fffffff2;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:16px}.spinner{width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #4285f4;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.loading-text{margin:0;font-size:14px;color:#666}.login-link{margin-top:16px;text-align:center;font-size:14px;color:#666}.login-link a{color:#4285f4;text-decoration:none}.login-link a:hover{text-decoration:underline}\n"] }]
3515
+ `, styles: [".tenant-register-dialog{padding:24px;max-width:500px;position:relative}.register-title{margin:0 0 20px;font-size:24px;font-weight:500;text-align:center}.warning-box{display:flex;gap:12px;padding:16px;background:#fff3cd;border:1px solid #ffc107;border-radius:6px;margin-bottom:24px}.warning-icon{font-size:24px;line-height:1}.warning-content{flex:1}.warning-content strong{display:block;margin-bottom:4px;color:#856404;font-size:14px}.warning-content p{margin:0;color:#856404;font-size:13px;line-height:1.5}.section-header{font-size:16px;font-weight:600;margin:20px 0 12px;padding-bottom:8px;border-bottom:2px solid #e0e0e0;color:#333}.register-form,.form-group{margin-bottom:16px}.password-group{position:relative}.form-group label{display:block;margin-bottom:6px;font-size:14px;font-weight:500;color:#333}.form-control{width:100%;padding:12px;border:1px solid #ddd;border-radius:4px;font-size:14px;box-sizing:border-box;transition:border-color .2s}.password-input{padding-right:45px}.password-toggle{position:absolute;right:8px;top:38px;background:none;border:none;cursor:pointer;font-size:18px;padding:8px;line-height:1;opacity:.6;transition:opacity .2s}.password-toggle:hover{opacity:1}.password-toggle:focus{outline:2px solid #4285f4;outline-offset:2px;border-radius:4px}.form-control:focus{outline:none;border-color:#4285f4}.form-control.input-error{border-color:#dc3545}.form-control.input-success{border-color:#28a745}.form-hint{display:block;margin-top:4px;font-size:12px;color:#666}.form-hint .checking{color:#666}.form-hint .available{color:#28a745;font-weight:500}.form-hint .error-text{color:#dc3545}.oauth-section{margin:16px 0}.oauth-description{margin-bottom:12px;font-size:14px;color:#666;text-align:center}.oauth-buttons{display:flex;flex-direction:column;gap:12px}.btn{padding:12px 24px;border:none;border-radius:4px;font-size:14px;font-weight:500;cursor:pointer;transition:background-color .2s,opacity .2s}.btn:disabled{opacity:.6;cursor:not-allowed}.btn-block{width:100%}.btn-primary{background-color:#4285f4;color:#fff}.btn-primary:hover:not(:disabled){background-color:#357ae8}.btn-oauth{width:100%;background:#fff;color:#333;border:1px solid #ddd;display:flex;align-items:center;justify-content:center;gap:8px}.btn-oauth:hover:not(:disabled){background:#f8f8f8}.btn-google{border-color:#4285f4}.btn-linkedin{border-color:#0077b5}.btn-apple{border-color:#000}.btn-microsoft{border-color:#00a4ef}.btn-github{border-color:#333}.oauth-icon{font-size:18px}.switch-method{margin-top:12px;text-align:center;font-size:14px}.switch-method a{color:#4285f4;text-decoration:none}.switch-method a:hover{text-decoration:underline}.error-message{margin-top:16px;padding:12px;background:#fee;color:#c33;border-radius:4px;font-size:14px}.success-message{margin-top:16px;padding:12px;background:#efe;color:#3a3;border-radius:4px;font-size:14px}.loading-overlay{position:absolute;inset:0;background:#fffffff2;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:16px}.spinner{width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #4285f4;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.loading-text{margin:0;font-size:14px;color:#666}.login-link{margin-top:16px;text-align:center;font-size:14px;color:#666}.login-link a{color:#4285f4;text-decoration:none}.login-link a:hover{text-decoration:underline}\n"] }]
2941
3516
  }], ctorParameters: () => [{ type: AuthService }], propDecorators: { title: [{
2942
3517
  type: Input
2943
3518
  }], providers: [{
@@ -3047,7 +3622,7 @@ class TenantLoginDialogComponent {
3047
3622
  (createTenant)="onCreateTenant()">
3048
3623
  </lib-tenant-login>
3049
3624
  </div>
3050
- `, isInline: true, styles: [".dialog-wrapper{padding:0}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: TenantLoginComponent, selector: "lib-tenant-login", inputs: ["title", "providers", "showTenantSelector", "autoSelectSingleTenant", "allowTenantCreation", "tenantSelectorTitle", "tenantSelectorDescription", "continueButtonText", "registerLinkText", "registerLinkAction", "createTenantLinkText", "createTenantLinkAction"], outputs: ["tenantSelected", "createTenant"] }] });
3625
+ `, isInline: true, styles: [".dialog-wrapper{padding:0}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: TenantLoginComponent, selector: "lib-tenant-login", inputs: ["title", "providers", "showTenantSelector", "autoSelectSingleTenant", "prefillEmail", "allowTenantCreation", "tenantSelectorTitle", "tenantSelectorDescription", "continueButtonText", "registerLinkText", "registerLinkAction", "createTenantLinkText", "createTenantLinkAction"], outputs: ["tenantSelected", "createTenant"] }] });
3051
3626
  }
3052
3627
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TenantLoginDialogComponent, decorators: [{
3053
3628
  type: Component,
@@ -3204,5 +3779,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
3204
3779
  * Generated bundle index. Do not edit.
3205
3780
  */
3206
3781
 
3207
- export { ApiConnectionService, ApiResponse, AuthService, CsrfService, DbService, LoginDialogComponent, MyEnvironmentModel, NgxStoneScriptPhpClientModule, RegisterComponent, SigninStatusService, TenantLoginComponent, TenantLoginDialogComponent, TenantRegisterComponent, TenantRegisterDialogComponent, TokenService, VerifyStatus };
3782
+ export { ApiConnectionService, ApiResponse, AuthPageComponent, AuthService, CsrfService, DbService, LoginDialogComponent, MyEnvironmentModel, NgxStoneScriptPhpClientModule, RegisterComponent, SigninStatusService, TenantLoginComponent, TenantLoginDialogComponent, TenantRegisterComponent, TenantRegisterDialogComponent, TokenService, VerifyStatus };
3208
3783
  //# sourceMappingURL=progalaxyelabs-ngx-stonescriptphp-client.mjs.map