@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,
|
|
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.
|
|
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
|
|
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
|
-
|
|
648
|
-
|
|
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
|
|
690
|
-
return this.loginWithOAuth('
|
|
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
|
|
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(
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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(
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
1518
|
+
class TenantLoginComponent {
|
|
1255
1519
|
auth;
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
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.
|
|
1272
|
-
throw new Error('
|
|
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
|
-
|
|
1294
|
-
|
|
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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
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
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
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
|
-
|
|
1402
|
-
|
|
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
|
-
|
|
1409
|
-
|
|
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
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
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
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
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
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
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
|
-
<!--
|
|
1460
|
-
@if (
|
|
1461
|
-
<div class="
|
|
1462
|
-
<
|
|
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
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
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
|
-
|
|
1954
|
+
type="submit"
|
|
1472
1955
|
[disabled]="loading"
|
|
1473
|
-
class="btn btn-
|
|
1474
|
-
|
|
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
|
-
|
|
1483
|
-
}
|
|
1967
|
+
}
|
|
1484
1968
|
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
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
|
|
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:
|
|
1506
|
-
}], ctorParameters: () => [{ type: AuthService }], propDecorators: {
|
|
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
|
-
|
|
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
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
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
|
-
|
|
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('
|
|
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
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
this.
|
|
1825
|
-
|
|
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.
|
|
2654
|
+
const result = await this.auth.loginWithEmail(this.email, this.password);
|
|
1933
2655
|
if (!result.success) {
|
|
1934
|
-
this.error = result.message || '
|
|
1935
|
-
return;
|
|
2656
|
+
this.error = result.message || 'Login failed';
|
|
1936
2657
|
}
|
|
1937
|
-
//
|
|
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 =
|
|
2661
|
+
this.error = 'An unexpected error occurred';
|
|
1946
2662
|
}
|
|
1947
2663
|
finally {
|
|
1948
2664
|
this.loading = false;
|
|
1949
2665
|
}
|
|
1950
2666
|
}
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
formatLastAccessed(dateStr) {
|
|
2667
|
+
async onOAuthLogin(provider) {
|
|
2668
|
+
this.loading = true;
|
|
2669
|
+
this.error = '';
|
|
1955
2670
|
try {
|
|
1956
|
-
const
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
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
|
-
|
|
2677
|
+
catch (err) {
|
|
2678
|
+
this.error = 'An unexpected error occurred';
|
|
2679
|
+
}
|
|
2680
|
+
finally {
|
|
2681
|
+
this.loading = false;
|
|
1972
2682
|
}
|
|
1973
2683
|
}
|
|
1974
|
-
|
|
2684
|
+
onRegisterClick(event) {
|
|
1975
2685
|
event.preventDefault();
|
|
1976
|
-
this
|
|
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:
|
|
1979
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.15", type:
|
|
1980
|
-
<div class="
|
|
1981
|
-
|
|
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
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
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="
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
(click)="
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
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
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
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
|
|
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: [".
|
|
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:
|
|
2780
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: LoginDialogComponent, decorators: [{
|
|
2140
2781
|
type: Component,
|
|
2141
|
-
args: [{ selector: 'lib-
|
|
2142
|
-
<div class="
|
|
2143
|
-
|
|
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
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
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="
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
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
|
-
|
|
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
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
(click)="
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
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
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
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
|
|
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: [".
|
|
2300
|
-
}], ctorParameters: () => [{ type: AuthService }], propDecorators: {
|
|
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
|