@progalaxyelabs/ngx-stonescriptphp-client 1.4.0 → 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.
@@ -166,10 +166,23 @@ class MyEnvironmentModel {
166
166
  */
167
167
  platformCode = '';
168
168
  /**
169
- * Accounts platform URL for centralized authentication
169
+ * Accounts platform URL for centralized authentication (single-server mode)
170
170
  * @example 'https://accounts.progalaxyelabs.com'
171
+ * @deprecated Use authServers for multi-server support
171
172
  */
172
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;
173
186
  firebase = {
174
187
  projectId: '',
175
188
  appId: '',
@@ -583,15 +596,157 @@ class AuthService {
583
596
  signinStatus;
584
597
  environment;
585
598
  USER_STORAGE_KEY = 'progalaxyapi_user';
599
+ ACTIVE_AUTH_SERVER_KEY = 'progalaxyapi_active_auth_server';
586
600
  // Observable user state
587
601
  userSubject = new BehaviorSubject(null);
588
602
  user$ = this.userSubject.asObservable();
603
+ // Current active auth server name (for multi-server mode)
604
+ activeAuthServer = null;
589
605
  constructor(tokens, signinStatus, environment) {
590
606
  this.tokens = tokens;
591
607
  this.signinStatus = signinStatus;
592
608
  this.environment = environment;
593
609
  // Restore user from localStorage on initialization
594
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);
595
750
  }
596
751
  /**
597
752
  * Restore user from localStorage
@@ -628,15 +783,19 @@ class AuthService {
628
783
  * Update user subject and persist to localStorage
629
784
  */
630
785
  updateUser(user) {
631
- this.updateUser(user);
786
+ this.userSubject.next(user);
632
787
  this.saveUser(user);
633
788
  }
634
789
  /**
635
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)
636
794
  */
637
- async loginWithEmail(email, password) {
795
+ async loginWithEmail(email, password, serverName) {
638
796
  try {
639
- 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`, {
640
799
  method: 'POST',
641
800
  headers: { 'Content-Type': 'application/json' },
642
801
  credentials: 'include', // Include cookies for refresh token
@@ -650,8 +809,17 @@ class AuthService {
650
809
  if (data.success && data.access_token) {
651
810
  this.tokens.setAccessToken(data.access_token);
652
811
  this.signinStatus.setSigninStatus(true);
653
- this.updateUser(data.user);
654
- 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 };
655
823
  }
656
824
  return {
657
825
  success: false,
@@ -667,55 +835,71 @@ class AuthService {
667
835
  }
668
836
  /**
669
837
  * Login with Google OAuth (popup window)
838
+ * @param serverName - Optional: Specify which auth server to use (for multi-server mode)
670
839
  */
671
- async loginWithGoogle() {
672
- return this.loginWithOAuth('google');
840
+ async loginWithGoogle(serverName) {
841
+ return this.loginWithOAuth('google', serverName);
673
842
  }
674
843
  /**
675
844
  * Login with GitHub OAuth (popup window)
845
+ * @param serverName - Optional: Specify which auth server to use (for multi-server mode)
676
846
  */
677
- async loginWithGitHub() {
678
- return this.loginWithOAuth('github');
847
+ async loginWithGitHub(serverName) {
848
+ return this.loginWithOAuth('github', serverName);
679
849
  }
680
850
  /**
681
851
  * Login with LinkedIn OAuth (popup window)
852
+ * @param serverName - Optional: Specify which auth server to use (for multi-server mode)
682
853
  */
683
- async loginWithLinkedIn() {
684
- return this.loginWithOAuth('linkedin');
854
+ async loginWithLinkedIn(serverName) {
855
+ return this.loginWithOAuth('linkedin', serverName);
685
856
  }
686
857
  /**
687
858
  * Login with Apple OAuth (popup window)
859
+ * @param serverName - Optional: Specify which auth server to use (for multi-server mode)
688
860
  */
689
- async loginWithApple() {
690
- return this.loginWithOAuth('apple');
861
+ async loginWithApple(serverName) {
862
+ return this.loginWithOAuth('apple', serverName);
691
863
  }
692
864
  /**
693
865
  * Login with Microsoft OAuth (popup window)
866
+ * @param serverName - Optional: Specify which auth server to use (for multi-server mode)
694
867
  */
695
- async loginWithMicrosoft() {
696
- return this.loginWithOAuth('microsoft');
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)
874
+ */
875
+ async loginWithZoho(serverName) {
876
+ return this.loginWithOAuth('zoho', serverName);
697
877
  }
698
878
  /**
699
879
  * Generic provider-based login (supports all OAuth providers)
700
880
  * @param provider - The provider identifier
881
+ * @param serverName - Optional: Specify which auth server to use (for multi-server mode)
701
882
  */
702
- async loginWithProvider(provider) {
883
+ async loginWithProvider(provider, serverName) {
703
884
  if (provider === 'emailPassword') {
704
885
  throw new Error('Use loginWithEmail() for email/password authentication');
705
886
  }
706
- return this.loginWithOAuth(provider);
887
+ return this.loginWithOAuth(provider, serverName);
707
888
  }
708
889
  /**
709
890
  * Generic OAuth login handler
710
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)
711
894
  */
712
- async loginWithOAuth(provider) {
895
+ async loginWithOAuth(provider, serverName) {
713
896
  return new Promise((resolve) => {
714
897
  const width = 500;
715
898
  const height = 600;
716
899
  const left = (window.screen.width - width) / 2;
717
900
  const top = (window.screen.height - height) / 2;
718
- const oauthUrl = `${this.environment.accountsUrl}/oauth/${provider}?` +
901
+ const accountsUrl = this.getAccountsUrl(serverName);
902
+ const oauthUrl = `${accountsUrl}/oauth/${provider}?` +
719
903
  `platform=${this.environment.platformCode}&` +
720
904
  `mode=popup`;
721
905
  const popup = window.open(oauthUrl, `${provider}_login`, `width=${width},height=${height},left=${left},top=${top}`);
@@ -729,18 +913,27 @@ class AuthService {
729
913
  // Listen for message from popup
730
914
  const messageHandler = (event) => {
731
915
  // Verify origin
732
- if (event.origin !== new URL(this.environment.accountsUrl).origin) {
916
+ if (event.origin !== new URL(accountsUrl).origin) {
733
917
  return;
734
918
  }
735
919
  if (event.data.type === 'oauth_success') {
736
920
  this.tokens.setAccessToken(event.data.access_token);
737
921
  this.signinStatus.setSigninStatus(true);
738
- 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);
739
932
  window.removeEventListener('message', messageHandler);
740
933
  popup.close();
741
934
  resolve({
742
935
  success: true,
743
- user: event.data.user
936
+ user: normalizedUser
744
937
  });
745
938
  }
746
939
  else if (event.data.type === 'oauth_error') {
@@ -768,10 +961,15 @@ class AuthService {
768
961
  }
769
962
  /**
770
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)
771
968
  */
772
- async register(email, password, displayName) {
969
+ async register(email, password, displayName, serverName) {
773
970
  try {
774
- 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`, {
775
973
  method: 'POST',
776
974
  headers: { 'Content-Type': 'application/json' },
777
975
  credentials: 'include',
@@ -786,10 +984,19 @@ class AuthService {
786
984
  if (data.success && data.access_token) {
787
985
  this.tokens.setAccessToken(data.access_token);
788
986
  this.signinStatus.setSigninStatus(true);
789
- 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);
790
997
  return {
791
998
  success: true,
792
- user: data.user,
999
+ user: normalizedUser,
793
1000
  message: data.needs_verification ? 'Please verify your email' : undefined
794
1001
  };
795
1002
  }
@@ -807,12 +1014,14 @@ class AuthService {
807
1014
  }
808
1015
  /**
809
1016
  * Sign out user
1017
+ * @param serverName - Optional: Specify which auth server to logout from (for multi-server mode)
810
1018
  */
811
- async signout() {
1019
+ async signout(serverName) {
812
1020
  try {
813
1021
  const refreshToken = this.tokens.getRefreshToken();
814
1022
  if (refreshToken) {
815
- await fetch(`${this.environment.accountsUrl}/api/auth/logout`, {
1023
+ const accountsUrl = this.getAccountsUrl(serverName);
1024
+ await fetch(`${accountsUrl}/api/auth/logout`, {
816
1025
  method: 'POST',
817
1026
  headers: {
818
1027
  'Content-Type': 'application/json'
@@ -835,15 +1044,17 @@ class AuthService {
835
1044
  }
836
1045
  /**
837
1046
  * Check for active session (call on app init)
1047
+ * @param serverName - Optional: Specify which auth server to check (for multi-server mode)
838
1048
  */
839
- async checkSession() {
1049
+ async checkSession(serverName) {
840
1050
  if (this.tokens.hasValidAccessToken()) {
841
1051
  this.signinStatus.setSigninStatus(true);
842
1052
  return true;
843
1053
  }
844
1054
  // Try to refresh using httpOnly cookie
845
1055
  try {
846
- 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`, {
847
1058
  method: 'POST',
848
1059
  credentials: 'include'
849
1060
  });
@@ -854,7 +1065,18 @@ class AuthService {
854
1065
  const data = await response.json();
855
1066
  if (data.access_token) {
856
1067
  this.tokens.setAccessToken(data.access_token);
857
- 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
+ }
858
1080
  this.signinStatus.setSigninStatus(true);
859
1081
  return true;
860
1082
  }
@@ -889,7 +1111,8 @@ class AuthService {
889
1111
  return await this.registerTenantWithOAuth(data.tenantName, data.tenantSlug, data.provider);
890
1112
  }
891
1113
  // Email/password registration
892
- 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`, {
893
1116
  method: 'POST',
894
1117
  headers: { 'Content-Type': 'application/json' },
895
1118
  credentials: 'include',
@@ -908,7 +1131,16 @@ class AuthService {
908
1131
  this.tokens.setAccessToken(result.access_token);
909
1132
  this.signinStatus.setSigninStatus(true);
910
1133
  if (result.user) {
911
- 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);
912
1144
  }
913
1145
  }
914
1146
  return result;
@@ -931,7 +1163,8 @@ class AuthService {
931
1163
  const left = (window.screen.width - width) / 2;
932
1164
  const top = (window.screen.height - height) / 2;
933
1165
  // Build OAuth URL with tenant registration params
934
- const oauthUrl = `${this.environment.accountsUrl}/oauth/${provider}?` +
1166
+ const accountsUrl = this.getAccountsUrl();
1167
+ const oauthUrl = `${accountsUrl}/oauth/${provider}?` +
935
1168
  `platform=${this.environment.platformCode}&` +
936
1169
  `mode=popup&` +
937
1170
  `action=register_tenant&` +
@@ -948,7 +1181,7 @@ class AuthService {
948
1181
  // Listen for message from popup
949
1182
  const messageHandler = (event) => {
950
1183
  // Verify origin
951
- if (event.origin !== new URL(this.environment.accountsUrl).origin) {
1184
+ if (event.origin !== new URL(accountsUrl).origin) {
952
1185
  return;
953
1186
  }
954
1187
  if (event.data.type === 'tenant_register_success') {
@@ -958,7 +1191,16 @@ class AuthService {
958
1191
  this.signinStatus.setSigninStatus(true);
959
1192
  }
960
1193
  if (event.data.user) {
961
- 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);
962
1204
  }
963
1205
  window.removeEventListener('message', messageHandler);
964
1206
  popup.close();
@@ -993,10 +1235,12 @@ class AuthService {
993
1235
  }
994
1236
  /**
995
1237
  * Get all tenant memberships for the authenticated user
1238
+ * @param serverName - Optional: Specify which auth server to query (for multi-server mode)
996
1239
  */
997
- async getTenantMemberships() {
1240
+ async getTenantMemberships(serverName) {
998
1241
  try {
999
- 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`, {
1000
1244
  method: 'GET',
1001
1245
  headers: {
1002
1246
  'Authorization': `Bearer ${this.tokens.getAccessToken()}`,
@@ -1016,10 +1260,13 @@ class AuthService {
1016
1260
  /**
1017
1261
  * Select a tenant for the current session
1018
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)
1019
1265
  */
1020
- async selectTenant(tenantId) {
1266
+ async selectTenant(tenantId, serverName) {
1021
1267
  try {
1022
- 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`, {
1023
1270
  method: 'POST',
1024
1271
  headers: {
1025
1272
  'Authorization': `Bearer ${this.tokens.getAccessToken()}`,
@@ -1050,10 +1297,13 @@ class AuthService {
1050
1297
  }
1051
1298
  /**
1052
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)
1053
1302
  */
1054
- async checkTenantSlugAvailable(slug) {
1303
+ async checkTenantSlugAvailable(slug, serverName) {
1055
1304
  try {
1056
- 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}`, {
1057
1307
  method: 'GET',
1058
1308
  headers: { 'Content-Type': 'application/json' }
1059
1309
  });
@@ -1136,9 +1386,10 @@ class AuthService {
1136
1386
  /**
1137
1387
  * @deprecated Check if user exists by calling /api/auth/check-email endpoint
1138
1388
  */
1139
- async getUserProfile(email) {
1389
+ async getUserProfile(email, serverName) {
1140
1390
  try {
1141
- 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`, {
1142
1393
  method: 'POST',
1143
1394
  headers: { 'Content-Type': 'application/json' },
1144
1395
  body: JSON.stringify({ email })
@@ -1152,10 +1403,13 @@ class AuthService {
1152
1403
  }
1153
1404
  /**
1154
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)
1155
1408
  */
1156
- async checkOnboardingStatus(identityId) {
1409
+ async checkOnboardingStatus(identityId, serverName) {
1157
1410
  try {
1158
- 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}`, {
1159
1413
  method: 'GET',
1160
1414
  headers: { 'Content-Type': 'application/json' },
1161
1415
  credentials: 'include'
@@ -1171,14 +1425,18 @@ class AuthService {
1171
1425
  }
1172
1426
  /**
1173
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)
1174
1431
  */
1175
- async completeTenantOnboarding(countryCode, tenantName) {
1432
+ async completeTenantOnboarding(countryCode, tenantName, serverName) {
1176
1433
  try {
1177
1434
  const accessToken = this.tokens.getAccessToken();
1178
1435
  if (!accessToken) {
1179
1436
  throw new Error('Not authenticated');
1180
1437
  }
1181
- 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`, {
1182
1440
  method: 'POST',
1183
1441
  headers: {
1184
1442
  'Content-Type': 'application/json',
@@ -1321,12 +1579,16 @@ class TenantLoginComponent {
1321
1579
  apple: 'Sign in with Apple',
1322
1580
  microsoft: 'Sign in with Microsoft',
1323
1581
  github: 'Sign in with GitHub',
1582
+ zoho: 'Sign in with Zoho',
1324
1583
  emailPassword: 'Sign in with Email'
1325
1584
  };
1326
1585
  return labels[provider];
1327
1586
  }
1328
1587
  getProviderIcon(provider) {
1329
- return undefined;
1588
+ const icons = {
1589
+ zoho: '🔶'
1590
+ };
1591
+ return icons[provider];
1330
1592
  }
1331
1593
  toggleAuthMethod(event) {
1332
1594
  event.preventDefault();
@@ -1650,7 +1912,7 @@ class TenantLoginComponent {
1650
1912
  </div>
1651
1913
  }
1652
1914
  </div>
1653
- `, 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}.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"] }] });
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"] }] });
1654
1916
  }
1655
1917
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TenantLoginComponent, decorators: [{
1656
1918
  type: Component,
@@ -1819,7 +2081,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
1819
2081
  </div>
1820
2082
  }
1821
2083
  </div>
1822
- `, 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}.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"] }]
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"] }]
1823
2085
  }], ctorParameters: () => [{ type: AuthService }], propDecorators: { title: [{
1824
2086
  type: Input
1825
2087
  }], providers: [{
@@ -2372,6 +2634,7 @@ class LoginDialogComponent {
2372
2634
  apple: 'Sign in with Apple',
2373
2635
  microsoft: 'Sign in with Microsoft',
2374
2636
  github: 'Sign in with GitHub',
2637
+ zoho: 'Sign in with Zoho',
2375
2638
  emailPassword: 'Sign in with Email'
2376
2639
  };
2377
2640
  return labels[provider];
@@ -2678,6 +2941,7 @@ class TenantRegisterComponent {
2678
2941
  apple: 'Sign up with Apple',
2679
2942
  microsoft: 'Sign up with Microsoft',
2680
2943
  github: 'Sign up with GitHub',
2944
+ zoho: 'Sign up with Zoho',
2681
2945
  emailPassword: 'Sign up with Email'
2682
2946
  };
2683
2947
  return labels[provider];