@progalaxyelabs/ngx-stonescriptphp-client 1.2.0 → 1.3.1
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,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { Injectable, Inject, NgModule, Input, Component } from '@angular/core';
|
|
2
|
+
import { Injectable, Inject, NgModule, Input, Component, EventEmitter, Output, Optional } from '@angular/core';
|
|
3
3
|
import { BehaviorSubject } from 'rxjs';
|
|
4
4
|
import { CommonModule } from '@angular/common';
|
|
5
5
|
import * as i2 from '@angular/forms';
|
|
@@ -576,6 +576,7 @@ class AuthService {
|
|
|
576
576
|
tokens;
|
|
577
577
|
signinStatus;
|
|
578
578
|
environment;
|
|
579
|
+
USER_STORAGE_KEY = 'progalaxyapi_user';
|
|
579
580
|
// Observable user state
|
|
580
581
|
userSubject = new BehaviorSubject(null);
|
|
581
582
|
user$ = this.userSubject.asObservable();
|
|
@@ -583,6 +584,46 @@ class AuthService {
|
|
|
583
584
|
this.tokens = tokens;
|
|
584
585
|
this.signinStatus = signinStatus;
|
|
585
586
|
this.environment = environment;
|
|
587
|
+
// Restore user from localStorage on initialization
|
|
588
|
+
this.restoreUser();
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Restore user from localStorage
|
|
592
|
+
*/
|
|
593
|
+
restoreUser() {
|
|
594
|
+
try {
|
|
595
|
+
const userJson = localStorage.getItem(this.USER_STORAGE_KEY);
|
|
596
|
+
if (userJson) {
|
|
597
|
+
const user = JSON.parse(userJson);
|
|
598
|
+
this.updateUser(user);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
catch (error) {
|
|
602
|
+
console.error('Failed to restore user from localStorage:', error);
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Save user to localStorage
|
|
607
|
+
*/
|
|
608
|
+
saveUser(user) {
|
|
609
|
+
try {
|
|
610
|
+
if (user) {
|
|
611
|
+
localStorage.setItem(this.USER_STORAGE_KEY, JSON.stringify(user));
|
|
612
|
+
}
|
|
613
|
+
else {
|
|
614
|
+
localStorage.removeItem(this.USER_STORAGE_KEY);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
catch (error) {
|
|
618
|
+
console.error('Failed to save user to localStorage:', error);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
/**
|
|
622
|
+
* Update user subject and persist to localStorage
|
|
623
|
+
*/
|
|
624
|
+
updateUser(user) {
|
|
625
|
+
this.updateUser(user);
|
|
626
|
+
this.saveUser(user);
|
|
586
627
|
}
|
|
587
628
|
/**
|
|
588
629
|
* Login with email and password
|
|
@@ -603,7 +644,7 @@ class AuthService {
|
|
|
603
644
|
if (data.success && data.access_token) {
|
|
604
645
|
this.tokens.setAccessToken(data.access_token);
|
|
605
646
|
this.signinStatus.setSigninStatus(true);
|
|
606
|
-
this.
|
|
647
|
+
this.updateUser(data.user);
|
|
607
648
|
return { success: true, user: data.user };
|
|
608
649
|
}
|
|
609
650
|
return {
|
|
@@ -688,7 +729,7 @@ class AuthService {
|
|
|
688
729
|
if (event.data.type === 'oauth_success') {
|
|
689
730
|
this.tokens.setAccessToken(event.data.access_token);
|
|
690
731
|
this.signinStatus.setSigninStatus(true);
|
|
691
|
-
this.
|
|
732
|
+
this.updateUser(event.data.user);
|
|
692
733
|
window.removeEventListener('message', messageHandler);
|
|
693
734
|
popup.close();
|
|
694
735
|
resolve({
|
|
@@ -739,7 +780,7 @@ class AuthService {
|
|
|
739
780
|
if (data.success && data.access_token) {
|
|
740
781
|
this.tokens.setAccessToken(data.access_token);
|
|
741
782
|
this.signinStatus.setSigninStatus(true);
|
|
742
|
-
this.
|
|
783
|
+
this.updateUser(data.user);
|
|
743
784
|
return {
|
|
744
785
|
success: true,
|
|
745
786
|
user: data.user,
|
|
@@ -763,10 +804,19 @@ class AuthService {
|
|
|
763
804
|
*/
|
|
764
805
|
async signout() {
|
|
765
806
|
try {
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
807
|
+
const refreshToken = this.tokens.getRefreshToken();
|
|
808
|
+
if (refreshToken) {
|
|
809
|
+
await fetch(`${this.environment.accountsUrl}/api/auth/logout`, {
|
|
810
|
+
method: 'POST',
|
|
811
|
+
headers: {
|
|
812
|
+
'Content-Type': 'application/json'
|
|
813
|
+
},
|
|
814
|
+
credentials: 'include',
|
|
815
|
+
body: JSON.stringify({
|
|
816
|
+
refresh_token: refreshToken
|
|
817
|
+
})
|
|
818
|
+
});
|
|
819
|
+
}
|
|
770
820
|
}
|
|
771
821
|
catch (error) {
|
|
772
822
|
console.error('Logout API call failed:', error);
|
|
@@ -774,7 +824,7 @@ class AuthService {
|
|
|
774
824
|
finally {
|
|
775
825
|
this.tokens.clear();
|
|
776
826
|
this.signinStatus.setSigninStatus(false);
|
|
777
|
-
this.
|
|
827
|
+
this.updateUser(null);
|
|
778
828
|
}
|
|
779
829
|
}
|
|
780
830
|
/**
|
|
@@ -798,7 +848,7 @@ class AuthService {
|
|
|
798
848
|
const data = await response.json();
|
|
799
849
|
if (data.access_token) {
|
|
800
850
|
this.tokens.setAccessToken(data.access_token);
|
|
801
|
-
this.
|
|
851
|
+
this.updateUser(data.user);
|
|
802
852
|
this.signinStatus.setSigninStatus(true);
|
|
803
853
|
return true;
|
|
804
854
|
}
|
|
@@ -821,6 +871,197 @@ class AuthService {
|
|
|
821
871
|
getCurrentUser() {
|
|
822
872
|
return this.userSubject.value;
|
|
823
873
|
}
|
|
874
|
+
// ===== Multi-Tenant Methods =====
|
|
875
|
+
/**
|
|
876
|
+
* Register a new user AND create a new tenant (organization)
|
|
877
|
+
* This is used when a user wants to create their own organization
|
|
878
|
+
*/
|
|
879
|
+
async registerTenant(data) {
|
|
880
|
+
try {
|
|
881
|
+
// If using OAuth, initiate OAuth flow first
|
|
882
|
+
if (data.provider !== 'emailPassword') {
|
|
883
|
+
return await this.registerTenantWithOAuth(data.tenantName, data.tenantSlug, data.provider);
|
|
884
|
+
}
|
|
885
|
+
// Email/password registration
|
|
886
|
+
const response = await fetch(`${this.environment.accountsUrl}/api/auth/register-tenant`, {
|
|
887
|
+
method: 'POST',
|
|
888
|
+
headers: { 'Content-Type': 'application/json' },
|
|
889
|
+
credentials: 'include',
|
|
890
|
+
body: JSON.stringify({
|
|
891
|
+
platform: this.environment.platformCode,
|
|
892
|
+
tenant_name: data.tenantName,
|
|
893
|
+
tenant_slug: data.tenantSlug,
|
|
894
|
+
display_name: data.displayName,
|
|
895
|
+
email: data.email,
|
|
896
|
+
password: data.password,
|
|
897
|
+
provider: 'emailPassword'
|
|
898
|
+
})
|
|
899
|
+
});
|
|
900
|
+
const result = await response.json();
|
|
901
|
+
if (result.success && result.access_token) {
|
|
902
|
+
this.tokens.setAccessToken(result.access_token);
|
|
903
|
+
this.signinStatus.setSigninStatus(true);
|
|
904
|
+
if (result.user) {
|
|
905
|
+
this.updateUser(result.user);
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
return result;
|
|
909
|
+
}
|
|
910
|
+
catch (error) {
|
|
911
|
+
return {
|
|
912
|
+
success: false,
|
|
913
|
+
message: 'Network error. Please try again.'
|
|
914
|
+
};
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
/**
|
|
918
|
+
* Register tenant with OAuth provider
|
|
919
|
+
* Opens popup window for OAuth flow
|
|
920
|
+
*/
|
|
921
|
+
async registerTenantWithOAuth(tenantName, tenantSlug, provider) {
|
|
922
|
+
return new Promise((resolve) => {
|
|
923
|
+
const width = 500;
|
|
924
|
+
const height = 600;
|
|
925
|
+
const left = (window.screen.width - width) / 2;
|
|
926
|
+
const top = (window.screen.height - height) / 2;
|
|
927
|
+
// Build OAuth URL with tenant registration params
|
|
928
|
+
const oauthUrl = `${this.environment.accountsUrl}/oauth/${provider}?` +
|
|
929
|
+
`platform=${this.environment.platformCode}&` +
|
|
930
|
+
`mode=popup&` +
|
|
931
|
+
`action=register_tenant&` +
|
|
932
|
+
`tenant_name=${encodeURIComponent(tenantName)}&` +
|
|
933
|
+
`tenant_slug=${encodeURIComponent(tenantSlug)}`;
|
|
934
|
+
const popup = window.open(oauthUrl, `${provider}_register_tenant`, `width=${width},height=${height},left=${left},top=${top}`);
|
|
935
|
+
if (!popup) {
|
|
936
|
+
resolve({
|
|
937
|
+
success: false,
|
|
938
|
+
message: 'Popup blocked. Please allow popups for this site.'
|
|
939
|
+
});
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
// Listen for message from popup
|
|
943
|
+
const messageHandler = (event) => {
|
|
944
|
+
// Verify origin
|
|
945
|
+
if (event.origin !== new URL(this.environment.accountsUrl).origin) {
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
if (event.data.type === 'tenant_register_success') {
|
|
949
|
+
// Set tokens and user
|
|
950
|
+
if (event.data.access_token) {
|
|
951
|
+
this.tokens.setAccessToken(event.data.access_token);
|
|
952
|
+
this.signinStatus.setSigninStatus(true);
|
|
953
|
+
}
|
|
954
|
+
if (event.data.user) {
|
|
955
|
+
this.updateUser(event.data.user);
|
|
956
|
+
}
|
|
957
|
+
window.removeEventListener('message', messageHandler);
|
|
958
|
+
popup.close();
|
|
959
|
+
resolve({
|
|
960
|
+
success: true,
|
|
961
|
+
tenant: event.data.tenant,
|
|
962
|
+
user: event.data.user
|
|
963
|
+
});
|
|
964
|
+
}
|
|
965
|
+
else if (event.data.type === 'tenant_register_error') {
|
|
966
|
+
window.removeEventListener('message', messageHandler);
|
|
967
|
+
popup.close();
|
|
968
|
+
resolve({
|
|
969
|
+
success: false,
|
|
970
|
+
message: event.data.message || 'Tenant registration failed'
|
|
971
|
+
});
|
|
972
|
+
}
|
|
973
|
+
};
|
|
974
|
+
window.addEventListener('message', messageHandler);
|
|
975
|
+
// Check if popup was closed manually
|
|
976
|
+
const checkClosed = setInterval(() => {
|
|
977
|
+
if (popup.closed) {
|
|
978
|
+
clearInterval(checkClosed);
|
|
979
|
+
window.removeEventListener('message', messageHandler);
|
|
980
|
+
resolve({
|
|
981
|
+
success: false,
|
|
982
|
+
message: 'Registration cancelled'
|
|
983
|
+
});
|
|
984
|
+
}
|
|
985
|
+
}, 500);
|
|
986
|
+
});
|
|
987
|
+
}
|
|
988
|
+
/**
|
|
989
|
+
* Get all tenant memberships for the authenticated user
|
|
990
|
+
*/
|
|
991
|
+
async getTenantMemberships() {
|
|
992
|
+
try {
|
|
993
|
+
const response = await fetch(`${this.environment.accountsUrl}/api/auth/memberships`, {
|
|
994
|
+
method: 'GET',
|
|
995
|
+
headers: {
|
|
996
|
+
'Authorization': `Bearer ${this.tokens.getAccessToken()}`,
|
|
997
|
+
'Content-Type': 'application/json'
|
|
998
|
+
},
|
|
999
|
+
credentials: 'include'
|
|
1000
|
+
});
|
|
1001
|
+
const data = await response.json();
|
|
1002
|
+
return {
|
|
1003
|
+
memberships: data.memberships || []
|
|
1004
|
+
};
|
|
1005
|
+
}
|
|
1006
|
+
catch (error) {
|
|
1007
|
+
return { memberships: [] };
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
1010
|
+
/**
|
|
1011
|
+
* Select a tenant for the current session
|
|
1012
|
+
* Updates the JWT token with tenant context
|
|
1013
|
+
*/
|
|
1014
|
+
async selectTenant(tenantId) {
|
|
1015
|
+
try {
|
|
1016
|
+
const response = await fetch(`${this.environment.accountsUrl}/api/auth/select-tenant`, {
|
|
1017
|
+
method: 'POST',
|
|
1018
|
+
headers: {
|
|
1019
|
+
'Authorization': `Bearer ${this.tokens.getAccessToken()}`,
|
|
1020
|
+
'Content-Type': 'application/json'
|
|
1021
|
+
},
|
|
1022
|
+
credentials: 'include',
|
|
1023
|
+
body: JSON.stringify({ tenant_id: tenantId })
|
|
1024
|
+
});
|
|
1025
|
+
const data = await response.json();
|
|
1026
|
+
if (data.success && data.access_token) {
|
|
1027
|
+
this.tokens.setAccessToken(data.access_token);
|
|
1028
|
+
return {
|
|
1029
|
+
success: true,
|
|
1030
|
+
access_token: data.access_token
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
return {
|
|
1034
|
+
success: false,
|
|
1035
|
+
message: data.message || 'Failed to select tenant'
|
|
1036
|
+
};
|
|
1037
|
+
}
|
|
1038
|
+
catch (error) {
|
|
1039
|
+
return {
|
|
1040
|
+
success: false,
|
|
1041
|
+
message: 'Network error. Please try again.'
|
|
1042
|
+
};
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
/**
|
|
1046
|
+
* Check if a tenant slug is available
|
|
1047
|
+
*/
|
|
1048
|
+
async checkTenantSlugAvailable(slug) {
|
|
1049
|
+
try {
|
|
1050
|
+
const response = await fetch(`${this.environment.accountsUrl}/api/auth/check-tenant-slug/${slug}`, {
|
|
1051
|
+
method: 'GET',
|
|
1052
|
+
headers: { 'Content-Type': 'application/json' }
|
|
1053
|
+
});
|
|
1054
|
+
const data = await response.json();
|
|
1055
|
+
return {
|
|
1056
|
+
available: data.available || false,
|
|
1057
|
+
suggestion: data.suggestion
|
|
1058
|
+
};
|
|
1059
|
+
}
|
|
1060
|
+
catch (error) {
|
|
1061
|
+
// On error, assume available (don't block registration)
|
|
1062
|
+
return { available: true };
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
824
1065
|
// ===== Backward Compatibility Methods =====
|
|
825
1066
|
// These methods are deprecated and maintained for backward compatibility
|
|
826
1067
|
// with existing platform code. New code should use the methods above.
|
|
@@ -903,6 +1144,65 @@ class AuthService {
|
|
|
903
1144
|
return null;
|
|
904
1145
|
}
|
|
905
1146
|
}
|
|
1147
|
+
/**
|
|
1148
|
+
* Check if user has completed onboarding (has a tenant)
|
|
1149
|
+
*/
|
|
1150
|
+
async checkOnboardingStatus(identityId) {
|
|
1151
|
+
try {
|
|
1152
|
+
const response = await fetch(`${this.environment.accountsUrl}/api/auth/onboarding/status?platform_code=${this.environment.platformCode}&identity_id=${identityId}`, {
|
|
1153
|
+
method: 'GET',
|
|
1154
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1155
|
+
credentials: 'include'
|
|
1156
|
+
});
|
|
1157
|
+
if (!response.ok) {
|
|
1158
|
+
throw new Error('Failed to check onboarding status');
|
|
1159
|
+
}
|
|
1160
|
+
return await response.json();
|
|
1161
|
+
}
|
|
1162
|
+
catch (error) {
|
|
1163
|
+
throw error;
|
|
1164
|
+
}
|
|
1165
|
+
}
|
|
1166
|
+
/**
|
|
1167
|
+
* Complete tenant onboarding (create tenant with country + org name)
|
|
1168
|
+
*/
|
|
1169
|
+
async completeTenantOnboarding(countryCode, tenantName) {
|
|
1170
|
+
try {
|
|
1171
|
+
const accessToken = this.tokens.getAccessToken();
|
|
1172
|
+
if (!accessToken) {
|
|
1173
|
+
throw new Error('Not authenticated');
|
|
1174
|
+
}
|
|
1175
|
+
const response = await fetch(`${this.environment.accountsUrl}/api/auth/register-tenant`, {
|
|
1176
|
+
method: 'POST',
|
|
1177
|
+
headers: {
|
|
1178
|
+
'Content-Type': 'application/json',
|
|
1179
|
+
'Authorization': `Bearer ${accessToken}`
|
|
1180
|
+
},
|
|
1181
|
+
credentials: 'include',
|
|
1182
|
+
body: JSON.stringify({
|
|
1183
|
+
platform: this.environment.platformCode,
|
|
1184
|
+
tenant_name: tenantName,
|
|
1185
|
+
country_code: countryCode,
|
|
1186
|
+
provider: 'google', // Assuming OAuth
|
|
1187
|
+
oauth_token: accessToken
|
|
1188
|
+
})
|
|
1189
|
+
});
|
|
1190
|
+
if (!response.ok) {
|
|
1191
|
+
const errorData = await response.json();
|
|
1192
|
+
throw new Error(errorData.message || 'Failed to create tenant');
|
|
1193
|
+
}
|
|
1194
|
+
const data = await response.json();
|
|
1195
|
+
// Update tokens with new tenant-scoped tokens
|
|
1196
|
+
if (data.access_token) {
|
|
1197
|
+
this.tokens.setAccessToken(data.access_token);
|
|
1198
|
+
this.signinStatus.setSigninStatus(true);
|
|
1199
|
+
}
|
|
1200
|
+
return data;
|
|
1201
|
+
}
|
|
1202
|
+
catch (error) {
|
|
1203
|
+
throw error;
|
|
1204
|
+
}
|
|
1205
|
+
}
|
|
906
1206
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: AuthService, deps: [{ token: TokenService }, { token: SigninStatusService }, { token: MyEnvironmentModel }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
907
1207
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: AuthService, providedIn: 'root' });
|
|
908
1208
|
}
|
|
@@ -1455,6 +1755,1447 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
|
|
|
1455
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"] }]
|
|
1456
1756
|
}], ctorParameters: () => [{ type: AuthService }] });
|
|
1457
1757
|
|
|
1758
|
+
class TenantLoginComponent {
|
|
1759
|
+
auth;
|
|
1760
|
+
// Component Configuration
|
|
1761
|
+
title = 'Sign In';
|
|
1762
|
+
providers = ['google'];
|
|
1763
|
+
showTenantSelector = true;
|
|
1764
|
+
autoSelectSingleTenant = true;
|
|
1765
|
+
allowTenantCreation = true;
|
|
1766
|
+
// Tenant Selector Labels
|
|
1767
|
+
tenantSelectorTitle = 'Select Organization';
|
|
1768
|
+
tenantSelectorDescription = 'Choose which organization you want to access:';
|
|
1769
|
+
continueButtonText = 'Continue';
|
|
1770
|
+
// Link Labels
|
|
1771
|
+
registerLinkText = "Don't have an account?";
|
|
1772
|
+
registerLinkAction = 'Sign up';
|
|
1773
|
+
createTenantLinkText = "Don't see your organization?";
|
|
1774
|
+
createTenantLinkAction = 'Create New Organization';
|
|
1775
|
+
// Outputs
|
|
1776
|
+
tenantSelected = new EventEmitter();
|
|
1777
|
+
createTenant = new EventEmitter();
|
|
1778
|
+
// Form Fields
|
|
1779
|
+
email = '';
|
|
1780
|
+
password = '';
|
|
1781
|
+
// State
|
|
1782
|
+
error = '';
|
|
1783
|
+
loading = false;
|
|
1784
|
+
useOAuth = true;
|
|
1785
|
+
oauthProviders = [];
|
|
1786
|
+
// Tenant Selection State
|
|
1787
|
+
showingTenantSelector = false;
|
|
1788
|
+
memberships = [];
|
|
1789
|
+
selectedTenantId = null;
|
|
1790
|
+
userName = '';
|
|
1791
|
+
constructor(auth) {
|
|
1792
|
+
this.auth = auth;
|
|
1793
|
+
}
|
|
1794
|
+
ngOnInit() {
|
|
1795
|
+
if (!this.providers || this.providers.length === 0) {
|
|
1796
|
+
this.error = 'Configuration Error: No authentication providers specified.';
|
|
1797
|
+
throw new Error('TenantLoginComponent requires providers input.');
|
|
1798
|
+
}
|
|
1799
|
+
this.oauthProviders = this.providers.filter(p => p !== 'emailPassword');
|
|
1800
|
+
// If only emailPassword is available, use it by default
|
|
1801
|
+
if (this.oauthProviders.length === 0 && this.isProviderEnabled('emailPassword')) {
|
|
1802
|
+
this.useOAuth = false;
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
isProviderEnabled(provider) {
|
|
1806
|
+
return this.providers.includes(provider);
|
|
1807
|
+
}
|
|
1808
|
+
getProviderLabel(provider) {
|
|
1809
|
+
const labels = {
|
|
1810
|
+
google: 'Sign in with Google',
|
|
1811
|
+
linkedin: 'Sign in with LinkedIn',
|
|
1812
|
+
apple: 'Sign in with Apple',
|
|
1813
|
+
microsoft: 'Sign in with Microsoft',
|
|
1814
|
+
github: 'Sign in with GitHub',
|
|
1815
|
+
emailPassword: 'Sign in with Email'
|
|
1816
|
+
};
|
|
1817
|
+
return labels[provider];
|
|
1818
|
+
}
|
|
1819
|
+
getProviderIcon(provider) {
|
|
1820
|
+
return undefined;
|
|
1821
|
+
}
|
|
1822
|
+
toggleAuthMethod(event) {
|
|
1823
|
+
event.preventDefault();
|
|
1824
|
+
this.useOAuth = !this.useOAuth;
|
|
1825
|
+
this.error = '';
|
|
1826
|
+
}
|
|
1827
|
+
async onEmailLogin() {
|
|
1828
|
+
if (!this.email || !this.password) {
|
|
1829
|
+
this.error = 'Please enter email and password';
|
|
1830
|
+
return;
|
|
1831
|
+
}
|
|
1832
|
+
this.loading = true;
|
|
1833
|
+
this.error = '';
|
|
1834
|
+
try {
|
|
1835
|
+
const result = await this.auth.loginWithEmail(this.email, this.password);
|
|
1836
|
+
if (!result.success) {
|
|
1837
|
+
this.error = result.message || 'Login failed';
|
|
1838
|
+
return;
|
|
1839
|
+
}
|
|
1840
|
+
// Authentication successful, now handle tenant selection
|
|
1841
|
+
await this.handlePostAuthFlow();
|
|
1842
|
+
}
|
|
1843
|
+
catch (err) {
|
|
1844
|
+
this.error = err.message || 'An unexpected error occurred';
|
|
1845
|
+
}
|
|
1846
|
+
finally {
|
|
1847
|
+
this.loading = false;
|
|
1848
|
+
}
|
|
1849
|
+
}
|
|
1850
|
+
async onOAuthLogin(provider) {
|
|
1851
|
+
this.loading = true;
|
|
1852
|
+
this.error = '';
|
|
1853
|
+
try {
|
|
1854
|
+
const result = await this.auth.loginWithProvider(provider);
|
|
1855
|
+
if (!result.success) {
|
|
1856
|
+
this.error = result.message || 'OAuth login failed';
|
|
1857
|
+
return;
|
|
1858
|
+
}
|
|
1859
|
+
// Authentication successful, now handle tenant selection
|
|
1860
|
+
await this.handlePostAuthFlow();
|
|
1861
|
+
}
|
|
1862
|
+
catch (err) {
|
|
1863
|
+
this.error = err.message || 'An unexpected error occurred';
|
|
1864
|
+
}
|
|
1865
|
+
finally {
|
|
1866
|
+
this.loading = false;
|
|
1867
|
+
}
|
|
1868
|
+
}
|
|
1869
|
+
async handlePostAuthFlow() {
|
|
1870
|
+
if (!this.showTenantSelector) {
|
|
1871
|
+
// Tenant selection is disabled, emit event immediately
|
|
1872
|
+
this.tenantSelected.emit({
|
|
1873
|
+
tenantId: '',
|
|
1874
|
+
tenantSlug: '',
|
|
1875
|
+
role: ''
|
|
1876
|
+
});
|
|
1877
|
+
return;
|
|
1878
|
+
}
|
|
1879
|
+
// Fetch user's tenant memberships
|
|
1880
|
+
this.loading = true;
|
|
1881
|
+
try {
|
|
1882
|
+
const result = await this.auth.getTenantMemberships();
|
|
1883
|
+
if (!result.memberships || result.memberships.length === 0) {
|
|
1884
|
+
// User has no tenants, prompt to create one
|
|
1885
|
+
this.error = 'You are not a member of any organization. Please create one.';
|
|
1886
|
+
if (this.allowTenantCreation) {
|
|
1887
|
+
setTimeout(() => this.createTenant.emit(), 2000);
|
|
1888
|
+
}
|
|
1889
|
+
return;
|
|
1890
|
+
}
|
|
1891
|
+
this.memberships = result.memberships;
|
|
1892
|
+
// Get user name if available
|
|
1893
|
+
const currentUser = this.auth.getCurrentUser();
|
|
1894
|
+
if (currentUser) {
|
|
1895
|
+
this.userName = currentUser.display_name || currentUser.email;
|
|
1896
|
+
}
|
|
1897
|
+
// Auto-select if user has only one tenant
|
|
1898
|
+
if (this.memberships.length === 1 && this.autoSelectSingleTenant) {
|
|
1899
|
+
await this.selectAndContinue(this.memberships[0]);
|
|
1900
|
+
}
|
|
1901
|
+
else {
|
|
1902
|
+
// Show tenant selector
|
|
1903
|
+
this.showingTenantSelector = true;
|
|
1904
|
+
}
|
|
1905
|
+
}
|
|
1906
|
+
catch (err) {
|
|
1907
|
+
this.error = err.message || 'Failed to load organizations';
|
|
1908
|
+
}
|
|
1909
|
+
finally {
|
|
1910
|
+
this.loading = false;
|
|
1911
|
+
}
|
|
1912
|
+
}
|
|
1913
|
+
selectTenantItem(tenantId) {
|
|
1914
|
+
this.selectedTenantId = tenantId;
|
|
1915
|
+
}
|
|
1916
|
+
async onContinueWithTenant() {
|
|
1917
|
+
if (!this.selectedTenantId) {
|
|
1918
|
+
this.error = 'Please select an organization';
|
|
1919
|
+
return;
|
|
1920
|
+
}
|
|
1921
|
+
const membership = this.memberships.find(m => m.tenant_id === this.selectedTenantId);
|
|
1922
|
+
if (!membership) {
|
|
1923
|
+
this.error = 'Selected organization not found';
|
|
1924
|
+
return;
|
|
1925
|
+
}
|
|
1926
|
+
await this.selectAndContinue(membership);
|
|
1927
|
+
}
|
|
1928
|
+
async selectAndContinue(membership) {
|
|
1929
|
+
this.loading = true;
|
|
1930
|
+
this.error = '';
|
|
1931
|
+
try {
|
|
1932
|
+
const result = await this.auth.selectTenant(membership.tenant_id);
|
|
1933
|
+
if (!result.success) {
|
|
1934
|
+
this.error = result.message || 'Failed to select organization';
|
|
1935
|
+
return;
|
|
1936
|
+
}
|
|
1937
|
+
// Emit tenant selected event
|
|
1938
|
+
this.tenantSelected.emit({
|
|
1939
|
+
tenantId: membership.tenant_id,
|
|
1940
|
+
tenantSlug: membership.slug,
|
|
1941
|
+
role: membership.role
|
|
1942
|
+
});
|
|
1943
|
+
}
|
|
1944
|
+
catch (err) {
|
|
1945
|
+
this.error = err.message || 'An unexpected error occurred';
|
|
1946
|
+
}
|
|
1947
|
+
finally {
|
|
1948
|
+
this.loading = false;
|
|
1949
|
+
}
|
|
1950
|
+
}
|
|
1951
|
+
formatRole(role) {
|
|
1952
|
+
return role.charAt(0).toUpperCase() + role.slice(1);
|
|
1953
|
+
}
|
|
1954
|
+
formatLastAccessed(dateStr) {
|
|
1955
|
+
try {
|
|
1956
|
+
const date = new Date(dateStr);
|
|
1957
|
+
const now = new Date();
|
|
1958
|
+
const diffMs = now.getTime() - date.getTime();
|
|
1959
|
+
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
|
1960
|
+
if (diffDays === 0)
|
|
1961
|
+
return 'today';
|
|
1962
|
+
if (diffDays === 1)
|
|
1963
|
+
return 'yesterday';
|
|
1964
|
+
if (diffDays < 7)
|
|
1965
|
+
return `${diffDays} days ago`;
|
|
1966
|
+
if (diffDays < 30)
|
|
1967
|
+
return `${Math.floor(diffDays / 7)} weeks ago`;
|
|
1968
|
+
return `${Math.floor(diffDays / 30)} months ago`;
|
|
1969
|
+
}
|
|
1970
|
+
catch {
|
|
1971
|
+
return dateStr;
|
|
1972
|
+
}
|
|
1973
|
+
}
|
|
1974
|
+
onCreateTenantClick(event) {
|
|
1975
|
+
event.preventDefault();
|
|
1976
|
+
this.createTenant.emit();
|
|
1977
|
+
}
|
|
1978
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TenantLoginComponent, deps: [{ token: AuthService }], target: i0.ɵɵFactoryTarget.Component });
|
|
1979
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.15", type: TenantLoginComponent, isStandalone: true, selector: "lib-tenant-login", inputs: { title: "title", providers: "providers", showTenantSelector: "showTenantSelector", autoSelectSingleTenant: "autoSelectSingleTenant", allowTenantCreation: "allowTenantCreation", tenantSelectorTitle: "tenantSelectorTitle", tenantSelectorDescription: "tenantSelectorDescription", continueButtonText: "continueButtonText", registerLinkText: "registerLinkText", registerLinkAction: "registerLinkAction", createTenantLinkText: "createTenantLinkText", createTenantLinkAction: "createTenantLinkAction" }, outputs: { tenantSelected: "tenantSelected", createTenant: "createTenant" }, ngImport: i0, template: `
|
|
1980
|
+
<div class="tenant-login-dialog">
|
|
1981
|
+
@if (!showingTenantSelector) {
|
|
1982
|
+
<!-- Step 1: Authentication -->
|
|
1983
|
+
<h2 class="login-title">{{ title }}</h2>
|
|
1984
|
+
|
|
1985
|
+
<!-- Email/Password Form (if enabled) -->
|
|
1986
|
+
@if (isProviderEnabled('emailPassword') && !useOAuth) {
|
|
1987
|
+
<form (ngSubmit)="onEmailLogin()" class="email-form">
|
|
1988
|
+
<div class="form-group">
|
|
1989
|
+
<input
|
|
1990
|
+
[(ngModel)]="email"
|
|
1991
|
+
name="email"
|
|
1992
|
+
placeholder="Email"
|
|
1993
|
+
type="email"
|
|
1994
|
+
required
|
|
1995
|
+
class="form-control">
|
|
1996
|
+
</div>
|
|
1997
|
+
<div class="form-group">
|
|
1998
|
+
<input
|
|
1999
|
+
[(ngModel)]="password"
|
|
2000
|
+
name="password"
|
|
2001
|
+
placeholder="Password"
|
|
2002
|
+
type="password"
|
|
2003
|
+
required
|
|
2004
|
+
class="form-control">
|
|
2005
|
+
</div>
|
|
2006
|
+
<button
|
|
2007
|
+
type="submit"
|
|
2008
|
+
[disabled]="loading"
|
|
2009
|
+
class="btn btn-primary btn-block">
|
|
2010
|
+
{{ loading ? 'Signing in...' : 'Sign in with Email' }}
|
|
2011
|
+
</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
|
+
</div>
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
<p class="selector-description">{{ tenantSelectorDescription }}</p>
|
|
2076
|
+
|
|
2077
|
+
<div class="tenant-list">
|
|
2078
|
+
@for (membership of memberships; track membership.tenant_id) {
|
|
2079
|
+
<div
|
|
2080
|
+
class="tenant-item"
|
|
2081
|
+
[class.selected]="selectedTenantId === membership.tenant_id"
|
|
2082
|
+
(click)="selectTenantItem(membership.tenant_id)">
|
|
2083
|
+
<div class="tenant-radio">
|
|
2084
|
+
<input
|
|
2085
|
+
type="radio"
|
|
2086
|
+
[checked]="selectedTenantId === membership.tenant_id"
|
|
2087
|
+
[name]="'tenant-' + membership.tenant_id"
|
|
2088
|
+
[id]="'tenant-' + membership.tenant_id">
|
|
2089
|
+
</div>
|
|
2090
|
+
<div class="tenant-info">
|
|
2091
|
+
<div class="tenant-name">{{ membership.name }}</div>
|
|
2092
|
+
<div class="tenant-meta">
|
|
2093
|
+
<span class="tenant-role">{{ formatRole(membership.role) }}</span>
|
|
2094
|
+
@if (membership.last_accessed) {
|
|
2095
|
+
<span class="tenant-separator">·</span>
|
|
2096
|
+
<span class="tenant-last-accessed">
|
|
2097
|
+
Last accessed {{ formatLastAccessed(membership.last_accessed) }}
|
|
2098
|
+
</span>
|
|
2099
|
+
}
|
|
2100
|
+
</div>
|
|
2101
|
+
</div>
|
|
2102
|
+
</div>
|
|
2103
|
+
}
|
|
2104
|
+
</div>
|
|
2105
|
+
|
|
2106
|
+
<button
|
|
2107
|
+
type="button"
|
|
2108
|
+
(click)="onContinueWithTenant()"
|
|
2109
|
+
[disabled]="!selectedTenantId || loading"
|
|
2110
|
+
class="btn btn-primary btn-block">
|
|
2111
|
+
{{ loading ? 'Loading...' : continueButtonText }}
|
|
2112
|
+
</button>
|
|
2113
|
+
|
|
2114
|
+
<!-- Error Message -->
|
|
2115
|
+
@if (error) {
|
|
2116
|
+
<div class="error-message">
|
|
2117
|
+
{{ error }}
|
|
2118
|
+
</div>
|
|
2119
|
+
}
|
|
2120
|
+
|
|
2121
|
+
<!-- Create New Tenant Link -->
|
|
2122
|
+
@if (allowTenantCreation) {
|
|
2123
|
+
<div class="create-tenant-link">
|
|
2124
|
+
{{ createTenantLinkText }}
|
|
2125
|
+
<a href="#" (click)="onCreateTenantClick($event)">{{ createTenantLinkAction }}</a>
|
|
2126
|
+
</div>
|
|
2127
|
+
}
|
|
2128
|
+
}
|
|
2129
|
+
|
|
2130
|
+
<!-- Loading Overlay -->
|
|
2131
|
+
@if (loading) {
|
|
2132
|
+
<div class="loading-overlay">
|
|
2133
|
+
<div class="spinner"></div>
|
|
2134
|
+
</div>
|
|
2135
|
+
}
|
|
2136
|
+
</div>
|
|
2137
|
+
`, isInline: true, styles: [".tenant-login-dialog{padding:24px;max-width:450px;position:relative}.login-title{margin:0 0 24px;font-size:24px;font-weight:500;text-align:center}.welcome-message{margin-bottom:16px;padding:12px;background:#e8f5e9;border-radius:4px;text-align:center;font-size:14px;color:#2e7d32}.selector-description{margin-bottom:20px;font-size:14px;color:#666;text-align:center}.email-form,.form-group{margin-bottom:16px}.form-control{width:100%;padding:12px;border:1px solid #ddd;border-radius:4px;font-size:14px;box-sizing:border-box}.form-control:focus{outline:none;border-color:#4285f4}.btn{padding:12px 24px;border:none;border-radius:4px;font-size:14px;font-weight:500;cursor:pointer;transition:background-color .2s}.btn:disabled{opacity:.6;cursor:not-allowed}.btn-block{width:100%}.btn-primary{background-color:#4285f4;color:#fff}.btn-primary:hover:not(:disabled){background-color:#357ae8}.divider{margin:16px 0;text-align:center;position:relative}.divider:before{content:\"\";position:absolute;top:50%;left:0;right:0;height:1px;background:#ddd}.divider span{background:#fff;padding:0 12px;position:relative;color:#666;font-size:12px}.oauth-buttons{display:flex;flex-direction:column;gap:12px}.btn-oauth{width:100%;background:#fff;color:#333;border:1px solid #ddd;display:flex;align-items:center;justify-content:center;gap:8px}.btn-oauth:hover:not(:disabled){background:#f8f8f8}.btn-google{border-color:#4285f4}.btn-linkedin{border-color:#0077b5}.btn-apple{border-color:#000}.btn-microsoft{border-color:#00a4ef}.btn-github{border-color:#333}.oauth-icon{font-size:18px}.switch-method{margin-top:12px;text-align:center;font-size:14px}.switch-method a{color:#4285f4;text-decoration:none}.switch-method a:hover{text-decoration:underline}.tenant-list{margin-bottom:20px;display:flex;flex-direction:column;gap:12px}.tenant-item{display:flex;align-items:flex-start;gap:12px;padding:16px;border:2px solid #e0e0e0;border-radius:6px;cursor:pointer;transition:all .2s}.tenant-item:hover{border-color:#4285f4;background:#f8f9ff}.tenant-item.selected{border-color:#4285f4;background:#e8f0fe}.tenant-radio{flex-shrink:0;padding-top:2px}.tenant-radio input[type=radio]{width:18px;height:18px;cursor:pointer}.tenant-info{flex:1}.tenant-name{font-size:16px;font-weight:500;color:#333;margin-bottom:4px}.tenant-meta{font-size:13px;color:#666}.tenant-role{font-weight:500;color:#4285f4}.tenant-separator{margin:0 6px}.error-message{margin-top:16px;padding:12px;background:#fee;color:#c33;border-radius:4px;font-size:14px}.loading-overlay{position:absolute;inset:0;background:#fffc;display:flex;align-items:center;justify-content:center}.spinner{width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #4285f4;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.register-link{margin-top:16px;text-align:center;font-size:14px;color:#666}.register-link a{color:#4285f4;text-decoration:none}.register-link a:hover{text-decoration:underline}.create-tenant-link{margin-top:16px;padding-top:16px;border-top:1px solid #e0e0e0;text-align:center;font-size:14px;color:#666}.create-tenant-link a{color:#4285f4;text-decoration:none;font-weight:500}.create-tenant-link a:hover{text-decoration:underline}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i2.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }] });
|
|
2138
|
+
}
|
|
2139
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TenantLoginComponent, decorators: [{
|
|
2140
|
+
type: Component,
|
|
2141
|
+
args: [{ selector: 'lib-tenant-login', standalone: true, imports: [CommonModule, FormsModule], template: `
|
|
2142
|
+
<div class="tenant-login-dialog">
|
|
2143
|
+
@if (!showingTenantSelector) {
|
|
2144
|
+
<!-- Step 1: Authentication -->
|
|
2145
|
+
<h2 class="login-title">{{ title }}</h2>
|
|
2146
|
+
|
|
2147
|
+
<!-- Email/Password Form (if enabled) -->
|
|
2148
|
+
@if (isProviderEnabled('emailPassword') && !useOAuth) {
|
|
2149
|
+
<form (ngSubmit)="onEmailLogin()" class="email-form">
|
|
2150
|
+
<div class="form-group">
|
|
2151
|
+
<input
|
|
2152
|
+
[(ngModel)]="email"
|
|
2153
|
+
name="email"
|
|
2154
|
+
placeholder="Email"
|
|
2155
|
+
type="email"
|
|
2156
|
+
required
|
|
2157
|
+
class="form-control">
|
|
2158
|
+
</div>
|
|
2159
|
+
<div class="form-group">
|
|
2160
|
+
<input
|
|
2161
|
+
[(ngModel)]="password"
|
|
2162
|
+
name="password"
|
|
2163
|
+
placeholder="Password"
|
|
2164
|
+
type="password"
|
|
2165
|
+
required
|
|
2166
|
+
class="form-control">
|
|
2167
|
+
</div>
|
|
2168
|
+
<button
|
|
2169
|
+
type="submit"
|
|
2170
|
+
[disabled]="loading"
|
|
2171
|
+
class="btn btn-primary btn-block">
|
|
2172
|
+
{{ loading ? 'Signing in...' : 'Sign in with Email' }}
|
|
2173
|
+
</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
|
+
</div>
|
|
2235
|
+
}
|
|
2236
|
+
|
|
2237
|
+
<p class="selector-description">{{ tenantSelectorDescription }}</p>
|
|
2238
|
+
|
|
2239
|
+
<div class="tenant-list">
|
|
2240
|
+
@for (membership of memberships; track membership.tenant_id) {
|
|
2241
|
+
<div
|
|
2242
|
+
class="tenant-item"
|
|
2243
|
+
[class.selected]="selectedTenantId === membership.tenant_id"
|
|
2244
|
+
(click)="selectTenantItem(membership.tenant_id)">
|
|
2245
|
+
<div class="tenant-radio">
|
|
2246
|
+
<input
|
|
2247
|
+
type="radio"
|
|
2248
|
+
[checked]="selectedTenantId === membership.tenant_id"
|
|
2249
|
+
[name]="'tenant-' + membership.tenant_id"
|
|
2250
|
+
[id]="'tenant-' + membership.tenant_id">
|
|
2251
|
+
</div>
|
|
2252
|
+
<div class="tenant-info">
|
|
2253
|
+
<div class="tenant-name">{{ membership.name }}</div>
|
|
2254
|
+
<div class="tenant-meta">
|
|
2255
|
+
<span class="tenant-role">{{ formatRole(membership.role) }}</span>
|
|
2256
|
+
@if (membership.last_accessed) {
|
|
2257
|
+
<span class="tenant-separator">·</span>
|
|
2258
|
+
<span class="tenant-last-accessed">
|
|
2259
|
+
Last accessed {{ formatLastAccessed(membership.last_accessed) }}
|
|
2260
|
+
</span>
|
|
2261
|
+
}
|
|
2262
|
+
</div>
|
|
2263
|
+
</div>
|
|
2264
|
+
</div>
|
|
2265
|
+
}
|
|
2266
|
+
</div>
|
|
2267
|
+
|
|
2268
|
+
<button
|
|
2269
|
+
type="button"
|
|
2270
|
+
(click)="onContinueWithTenant()"
|
|
2271
|
+
[disabled]="!selectedTenantId || loading"
|
|
2272
|
+
class="btn btn-primary btn-block">
|
|
2273
|
+
{{ loading ? 'Loading...' : continueButtonText }}
|
|
2274
|
+
</button>
|
|
2275
|
+
|
|
2276
|
+
<!-- Error Message -->
|
|
2277
|
+
@if (error) {
|
|
2278
|
+
<div class="error-message">
|
|
2279
|
+
{{ error }}
|
|
2280
|
+
</div>
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
<!-- Create New Tenant Link -->
|
|
2284
|
+
@if (allowTenantCreation) {
|
|
2285
|
+
<div class="create-tenant-link">
|
|
2286
|
+
{{ createTenantLinkText }}
|
|
2287
|
+
<a href="#" (click)="onCreateTenantClick($event)">{{ createTenantLinkAction }}</a>
|
|
2288
|
+
</div>
|
|
2289
|
+
}
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
<!-- Loading Overlay -->
|
|
2293
|
+
@if (loading) {
|
|
2294
|
+
<div class="loading-overlay">
|
|
2295
|
+
<div class="spinner"></div>
|
|
2296
|
+
</div>
|
|
2297
|
+
}
|
|
2298
|
+
</div>
|
|
2299
|
+
`, styles: [".tenant-login-dialog{padding:24px;max-width:450px;position:relative}.login-title{margin:0 0 24px;font-size:24px;font-weight:500;text-align:center}.welcome-message{margin-bottom:16px;padding:12px;background:#e8f5e9;border-radius:4px;text-align:center;font-size:14px;color:#2e7d32}.selector-description{margin-bottom:20px;font-size:14px;color:#666;text-align:center}.email-form,.form-group{margin-bottom:16px}.form-control{width:100%;padding:12px;border:1px solid #ddd;border-radius:4px;font-size:14px;box-sizing:border-box}.form-control:focus{outline:none;border-color:#4285f4}.btn{padding:12px 24px;border:none;border-radius:4px;font-size:14px;font-weight:500;cursor:pointer;transition:background-color .2s}.btn:disabled{opacity:.6;cursor:not-allowed}.btn-block{width:100%}.btn-primary{background-color:#4285f4;color:#fff}.btn-primary:hover:not(:disabled){background-color:#357ae8}.divider{margin:16px 0;text-align:center;position:relative}.divider:before{content:\"\";position:absolute;top:50%;left:0;right:0;height:1px;background:#ddd}.divider span{background:#fff;padding:0 12px;position:relative;color:#666;font-size:12px}.oauth-buttons{display:flex;flex-direction:column;gap:12px}.btn-oauth{width:100%;background:#fff;color:#333;border:1px solid #ddd;display:flex;align-items:center;justify-content:center;gap:8px}.btn-oauth:hover:not(:disabled){background:#f8f8f8}.btn-google{border-color:#4285f4}.btn-linkedin{border-color:#0077b5}.btn-apple{border-color:#000}.btn-microsoft{border-color:#00a4ef}.btn-github{border-color:#333}.oauth-icon{font-size:18px}.switch-method{margin-top:12px;text-align:center;font-size:14px}.switch-method a{color:#4285f4;text-decoration:none}.switch-method a:hover{text-decoration:underline}.tenant-list{margin-bottom:20px;display:flex;flex-direction:column;gap:12px}.tenant-item{display:flex;align-items:flex-start;gap:12px;padding:16px;border:2px solid #e0e0e0;border-radius:6px;cursor:pointer;transition:all .2s}.tenant-item:hover{border-color:#4285f4;background:#f8f9ff}.tenant-item.selected{border-color:#4285f4;background:#e8f0fe}.tenant-radio{flex-shrink:0;padding-top:2px}.tenant-radio input[type=radio]{width:18px;height:18px;cursor:pointer}.tenant-info{flex:1}.tenant-name{font-size:16px;font-weight:500;color:#333;margin-bottom:4px}.tenant-meta{font-size:13px;color:#666}.tenant-role{font-weight:500;color:#4285f4}.tenant-separator{margin:0 6px}.error-message{margin-top:16px;padding:12px;background:#fee;color:#c33;border-radius:4px;font-size:14px}.loading-overlay{position:absolute;inset:0;background:#fffc;display:flex;align-items:center;justify-content:center}.spinner{width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #4285f4;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.register-link{margin-top:16px;text-align:center;font-size:14px;color:#666}.register-link a{color:#4285f4;text-decoration:none}.register-link a:hover{text-decoration:underline}.create-tenant-link{margin-top:16px;padding-top:16px;border-top:1px solid #e0e0e0;text-align:center;font-size:14px;color:#666}.create-tenant-link a{color:#4285f4;text-decoration:none;font-weight:500}.create-tenant-link a:hover{text-decoration:underline}\n"] }]
|
|
2300
|
+
}], ctorParameters: () => [{ type: AuthService }], propDecorators: { title: [{
|
|
2301
|
+
type: Input
|
|
2302
|
+
}], providers: [{
|
|
2303
|
+
type: Input
|
|
2304
|
+
}], showTenantSelector: [{
|
|
2305
|
+
type: Input
|
|
2306
|
+
}], autoSelectSingleTenant: [{
|
|
2307
|
+
type: Input
|
|
2308
|
+
}], allowTenantCreation: [{
|
|
2309
|
+
type: Input
|
|
2310
|
+
}], tenantSelectorTitle: [{
|
|
2311
|
+
type: Input
|
|
2312
|
+
}], tenantSelectorDescription: [{
|
|
2313
|
+
type: Input
|
|
2314
|
+
}], continueButtonText: [{
|
|
2315
|
+
type: Input
|
|
2316
|
+
}], registerLinkText: [{
|
|
2317
|
+
type: Input
|
|
2318
|
+
}], registerLinkAction: [{
|
|
2319
|
+
type: Input
|
|
2320
|
+
}], createTenantLinkText: [{
|
|
2321
|
+
type: Input
|
|
2322
|
+
}], createTenantLinkAction: [{
|
|
2323
|
+
type: Input
|
|
2324
|
+
}], tenantSelected: [{
|
|
2325
|
+
type: Output
|
|
2326
|
+
}], createTenant: [{
|
|
2327
|
+
type: Output
|
|
2328
|
+
}] } });
|
|
2329
|
+
|
|
2330
|
+
class TenantRegisterComponent {
|
|
2331
|
+
auth;
|
|
2332
|
+
// Component Configuration
|
|
2333
|
+
title = 'Create New Organization';
|
|
2334
|
+
providers = ['google'];
|
|
2335
|
+
requireTenantName = true;
|
|
2336
|
+
// Tenant Labels
|
|
2337
|
+
tenantSectionTitle = 'Organization Information';
|
|
2338
|
+
tenantNameLabel = 'Organization Name';
|
|
2339
|
+
tenantNamePlaceholder = 'Enter your organization name';
|
|
2340
|
+
tenantSlugLabel = 'Organization URL';
|
|
2341
|
+
tenantSlugPlaceholder = 'organization-name';
|
|
2342
|
+
urlPreviewEnabled = true;
|
|
2343
|
+
urlPreviewPrefix = 'medstoreapp.in/';
|
|
2344
|
+
// User Labels
|
|
2345
|
+
userSectionTitle = 'Your Information';
|
|
2346
|
+
oauthDescription = 'Recommended: Sign up with your Google account';
|
|
2347
|
+
// Warning Message
|
|
2348
|
+
ownershipTitle = 'CREATING A NEW ORGANIZATION';
|
|
2349
|
+
ownershipMessage = 'You are registering as an organization owner. This will create a new organization that you will manage. If you are an employee, DO NOT use this form. Ask your organization owner to invite you, then use the Login page.';
|
|
2350
|
+
// Buttons and Links
|
|
2351
|
+
submitButtonText = 'Create Organization';
|
|
2352
|
+
loginLinkText = 'Already have an account?';
|
|
2353
|
+
loginLinkAction = 'Sign in';
|
|
2354
|
+
// Outputs
|
|
2355
|
+
tenantCreated = new EventEmitter();
|
|
2356
|
+
navigateToLogin = new EventEmitter();
|
|
2357
|
+
// Form Fields
|
|
2358
|
+
tenantName = '';
|
|
2359
|
+
tenantSlug = '';
|
|
2360
|
+
displayName = '';
|
|
2361
|
+
email = '';
|
|
2362
|
+
password = '';
|
|
2363
|
+
confirmPassword = '';
|
|
2364
|
+
// State
|
|
2365
|
+
error = '';
|
|
2366
|
+
success = '';
|
|
2367
|
+
loading = false;
|
|
2368
|
+
loadingText = 'Creating your organization...';
|
|
2369
|
+
checkingSlug = false;
|
|
2370
|
+
slugAvailable = false;
|
|
2371
|
+
slugError = '';
|
|
2372
|
+
useEmailPassword = false;
|
|
2373
|
+
oauthProviders = [];
|
|
2374
|
+
constructor(auth) {
|
|
2375
|
+
this.auth = auth;
|
|
2376
|
+
}
|
|
2377
|
+
ngOnInit() {
|
|
2378
|
+
if (!this.providers || this.providers.length === 0) {
|
|
2379
|
+
this.error = 'Configuration Error: No authentication providers specified.';
|
|
2380
|
+
throw new Error('TenantRegisterComponent requires providers input.');
|
|
2381
|
+
}
|
|
2382
|
+
this.oauthProviders = this.providers.filter(p => p !== 'emailPassword');
|
|
2383
|
+
// If only emailPassword is available, show it by default
|
|
2384
|
+
if (this.oauthProviders.length === 0 && this.isProviderEnabled('emailPassword')) {
|
|
2385
|
+
this.useEmailPassword = true;
|
|
2386
|
+
}
|
|
2387
|
+
}
|
|
2388
|
+
isProviderEnabled(provider) {
|
|
2389
|
+
return this.providers.includes(provider);
|
|
2390
|
+
}
|
|
2391
|
+
getProviderLabel(provider) {
|
|
2392
|
+
const labels = {
|
|
2393
|
+
google: 'Sign up with Google',
|
|
2394
|
+
linkedin: 'Sign up with LinkedIn',
|
|
2395
|
+
apple: 'Sign up with Apple',
|
|
2396
|
+
microsoft: 'Sign up with Microsoft',
|
|
2397
|
+
github: 'Sign up with GitHub',
|
|
2398
|
+
emailPassword: 'Sign up with Email'
|
|
2399
|
+
};
|
|
2400
|
+
return labels[provider];
|
|
2401
|
+
}
|
|
2402
|
+
getProviderIcon(provider) {
|
|
2403
|
+
return undefined;
|
|
2404
|
+
}
|
|
2405
|
+
onTenantNameChange() {
|
|
2406
|
+
// Auto-generate slug from tenant name
|
|
2407
|
+
if (this.tenantName) {
|
|
2408
|
+
const slug = this.tenantName
|
|
2409
|
+
.toLowerCase()
|
|
2410
|
+
.replace(/[^a-z0-9\s-]/g, '')
|
|
2411
|
+
.replace(/\s+/g, '-')
|
|
2412
|
+
.replace(/-+/g, '-')
|
|
2413
|
+
.replace(/^-|-$/g, '');
|
|
2414
|
+
this.tenantSlug = slug;
|
|
2415
|
+
this.slugError = '';
|
|
2416
|
+
this.slugAvailable = false;
|
|
2417
|
+
}
|
|
2418
|
+
}
|
|
2419
|
+
async checkSlugAvailability() {
|
|
2420
|
+
if (!this.tenantSlug || this.tenantSlug.length < 3) {
|
|
2421
|
+
this.slugError = 'Slug must be at least 3 characters';
|
|
2422
|
+
return;
|
|
2423
|
+
}
|
|
2424
|
+
if (!/^[a-z0-9-]+$/.test(this.tenantSlug)) {
|
|
2425
|
+
this.slugError = 'Only lowercase letters, numbers, and hyphens allowed';
|
|
2426
|
+
return;
|
|
2427
|
+
}
|
|
2428
|
+
this.checkingSlug = true;
|
|
2429
|
+
this.slugError = '';
|
|
2430
|
+
try {
|
|
2431
|
+
const result = await this.auth.checkTenantSlugAvailable(this.tenantSlug);
|
|
2432
|
+
if (result.available) {
|
|
2433
|
+
this.slugAvailable = true;
|
|
2434
|
+
}
|
|
2435
|
+
else {
|
|
2436
|
+
this.slugError = 'This URL is already taken';
|
|
2437
|
+
if (result.suggestion) {
|
|
2438
|
+
this.slugError += `. Try: ${result.suggestion}`;
|
|
2439
|
+
}
|
|
2440
|
+
}
|
|
2441
|
+
}
|
|
2442
|
+
catch (err) {
|
|
2443
|
+
// Slug check failed, but don't block registration
|
|
2444
|
+
console.warn('Slug availability check failed:', err);
|
|
2445
|
+
}
|
|
2446
|
+
finally {
|
|
2447
|
+
this.checkingSlug = false;
|
|
2448
|
+
}
|
|
2449
|
+
}
|
|
2450
|
+
toggleAuthMethod(event) {
|
|
2451
|
+
event.preventDefault();
|
|
2452
|
+
this.useEmailPassword = !this.useEmailPassword;
|
|
2453
|
+
this.error = '';
|
|
2454
|
+
}
|
|
2455
|
+
isFormValid() {
|
|
2456
|
+
// Tenant information must be valid
|
|
2457
|
+
if (!this.tenantName || !this.tenantSlug) {
|
|
2458
|
+
return false;
|
|
2459
|
+
}
|
|
2460
|
+
if (this.slugError) {
|
|
2461
|
+
return false;
|
|
2462
|
+
}
|
|
2463
|
+
// If using email/password, check those fields
|
|
2464
|
+
if (this.useEmailPassword) {
|
|
2465
|
+
if (!this.displayName || !this.email || !this.password || !this.confirmPassword) {
|
|
2466
|
+
return false;
|
|
2467
|
+
}
|
|
2468
|
+
if (this.password.length < 8) {
|
|
2469
|
+
return false;
|
|
2470
|
+
}
|
|
2471
|
+
if (this.password !== this.confirmPassword) {
|
|
2472
|
+
return false;
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
return true;
|
|
2476
|
+
}
|
|
2477
|
+
async onOAuthRegister(provider) {
|
|
2478
|
+
if (!this.isFormValid()) {
|
|
2479
|
+
this.error = 'Please complete the organization information';
|
|
2480
|
+
return;
|
|
2481
|
+
}
|
|
2482
|
+
this.loading = true;
|
|
2483
|
+
this.loadingText = `Signing up with ${provider}...`;
|
|
2484
|
+
this.error = '';
|
|
2485
|
+
try {
|
|
2486
|
+
const result = await this.auth.registerTenant({
|
|
2487
|
+
tenantName: this.tenantName,
|
|
2488
|
+
tenantSlug: this.tenantSlug,
|
|
2489
|
+
provider: provider
|
|
2490
|
+
});
|
|
2491
|
+
if (result.success && result.tenant && result.user) {
|
|
2492
|
+
this.success = 'Organization created successfully!';
|
|
2493
|
+
this.tenantCreated.emit({
|
|
2494
|
+
tenant: result.tenant,
|
|
2495
|
+
user: result.user
|
|
2496
|
+
});
|
|
2497
|
+
}
|
|
2498
|
+
else {
|
|
2499
|
+
this.error = result.message || 'Registration failed';
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
catch (err) {
|
|
2503
|
+
this.error = err.message || 'An unexpected error occurred';
|
|
2504
|
+
}
|
|
2505
|
+
finally {
|
|
2506
|
+
this.loading = false;
|
|
2507
|
+
}
|
|
2508
|
+
}
|
|
2509
|
+
async onRegister() {
|
|
2510
|
+
if (!this.isFormValid()) {
|
|
2511
|
+
this.error = 'Please fill in all required fields correctly';
|
|
2512
|
+
return;
|
|
2513
|
+
}
|
|
2514
|
+
this.loading = true;
|
|
2515
|
+
this.loadingText = 'Creating your organization...';
|
|
2516
|
+
this.error = '';
|
|
2517
|
+
this.success = '';
|
|
2518
|
+
try {
|
|
2519
|
+
const result = await this.auth.registerTenant({
|
|
2520
|
+
tenantName: this.tenantName,
|
|
2521
|
+
tenantSlug: this.tenantSlug,
|
|
2522
|
+
displayName: this.displayName,
|
|
2523
|
+
email: this.email,
|
|
2524
|
+
password: this.password,
|
|
2525
|
+
provider: 'emailPassword'
|
|
2526
|
+
});
|
|
2527
|
+
if (result.success && result.tenant && result.user) {
|
|
2528
|
+
this.success = 'Organization created successfully!';
|
|
2529
|
+
this.tenantCreated.emit({
|
|
2530
|
+
tenant: result.tenant,
|
|
2531
|
+
user: result.user
|
|
2532
|
+
});
|
|
2533
|
+
}
|
|
2534
|
+
else {
|
|
2535
|
+
this.error = result.message || 'Registration failed';
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
catch (err) {
|
|
2539
|
+
this.error = err.message || 'An unexpected error occurred';
|
|
2540
|
+
}
|
|
2541
|
+
finally {
|
|
2542
|
+
this.loading = false;
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
onLoginClick(event) {
|
|
2546
|
+
event.preventDefault();
|
|
2547
|
+
this.navigateToLogin.emit();
|
|
2548
|
+
}
|
|
2549
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TenantRegisterComponent, deps: [{ token: AuthService }], target: i0.ɵɵFactoryTarget.Component });
|
|
2550
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.15", type: TenantRegisterComponent, isStandalone: true, selector: "lib-tenant-register", inputs: { title: "title", providers: "providers", requireTenantName: "requireTenantName", tenantSectionTitle: "tenantSectionTitle", tenantNameLabel: "tenantNameLabel", tenantNamePlaceholder: "tenantNamePlaceholder", tenantSlugLabel: "tenantSlugLabel", tenantSlugPlaceholder: "tenantSlugPlaceholder", urlPreviewEnabled: "urlPreviewEnabled", urlPreviewPrefix: "urlPreviewPrefix", userSectionTitle: "userSectionTitle", oauthDescription: "oauthDescription", ownershipTitle: "ownershipTitle", ownershipMessage: "ownershipMessage", submitButtonText: "submitButtonText", loginLinkText: "loginLinkText", loginLinkAction: "loginLinkAction" }, outputs: { tenantCreated: "tenantCreated", navigateToLogin: "navigateToLogin" }, ngImport: i0, template: `
|
|
2551
|
+
<div class="tenant-register-dialog">
|
|
2552
|
+
<h2 class="register-title">{{ title }}</h2>
|
|
2553
|
+
|
|
2554
|
+
<!-- Ownership Warning Message -->
|
|
2555
|
+
<div class="warning-box">
|
|
2556
|
+
<div class="warning-icon">⚠️</div>
|
|
2557
|
+
<div class="warning-content">
|
|
2558
|
+
<strong>{{ ownershipTitle }}</strong>
|
|
2559
|
+
<p>{{ ownershipMessage }}</p>
|
|
2560
|
+
</div>
|
|
2561
|
+
</div>
|
|
2562
|
+
|
|
2563
|
+
<form (ngSubmit)="onRegister()" class="register-form">
|
|
2564
|
+
<!-- Tenant Information Section -->
|
|
2565
|
+
<div class="section-header">{{ tenantSectionTitle }}</div>
|
|
2566
|
+
|
|
2567
|
+
<div class="form-group">
|
|
2568
|
+
<label for="tenantName">{{ tenantNameLabel }} *</label>
|
|
2569
|
+
<input
|
|
2570
|
+
id="tenantName"
|
|
2571
|
+
[(ngModel)]="tenantName"
|
|
2572
|
+
name="tenantName"
|
|
2573
|
+
[placeholder]="tenantNamePlaceholder"
|
|
2574
|
+
type="text"
|
|
2575
|
+
required
|
|
2576
|
+
(input)="onTenantNameChange()"
|
|
2577
|
+
class="form-control">
|
|
2578
|
+
</div>
|
|
2579
|
+
|
|
2580
|
+
<div class="form-group">
|
|
2581
|
+
<label for="tenantSlug">{{ tenantSlugLabel }} *</label>
|
|
2582
|
+
<input
|
|
2583
|
+
id="tenantSlug"
|
|
2584
|
+
[(ngModel)]="tenantSlug"
|
|
2585
|
+
name="tenantSlug"
|
|
2586
|
+
[placeholder]="tenantSlugPlaceholder"
|
|
2587
|
+
type="text"
|
|
2588
|
+
required
|
|
2589
|
+
pattern="[a-z0-9-]+"
|
|
2590
|
+
(blur)="checkSlugAvailability()"
|
|
2591
|
+
class="form-control"
|
|
2592
|
+
[class.input-error]="slugError"
|
|
2593
|
+
[class.input-success]="slugAvailable && tenantSlug">
|
|
2594
|
+
@if (tenantSlug && urlPreviewEnabled) {
|
|
2595
|
+
<small class="form-hint">
|
|
2596
|
+
{{ urlPreviewPrefix }}{{ tenantSlug }}
|
|
2597
|
+
@if (checkingSlug) {
|
|
2598
|
+
<span class="checking">Checking...</span>
|
|
2599
|
+
}
|
|
2600
|
+
@if (slugAvailable && !checkingSlug) {
|
|
2601
|
+
<span class="available">✓ Available</span>
|
|
2602
|
+
}
|
|
2603
|
+
@if (slugError) {
|
|
2604
|
+
<span class="error-text">{{ slugError }}</span>
|
|
2605
|
+
}
|
|
2606
|
+
</small>
|
|
2607
|
+
}
|
|
2608
|
+
</div>
|
|
2609
|
+
|
|
2610
|
+
<!-- User Information Section -->
|
|
2611
|
+
<div class="section-header">{{ userSectionTitle }}</div>
|
|
2612
|
+
|
|
2613
|
+
<!-- OAuth Providers (Primary Option) -->
|
|
2614
|
+
@if (oauthProviders.length > 0 && !useEmailPassword) {
|
|
2615
|
+
<div class="oauth-section">
|
|
2616
|
+
<p class="oauth-description">{{ oauthDescription }}</p>
|
|
2617
|
+
<div class="oauth-buttons">
|
|
2618
|
+
@for (provider of oauthProviders; track provider) {
|
|
2619
|
+
<button
|
|
2620
|
+
type="button"
|
|
2621
|
+
(click)="onOAuthRegister(provider)"
|
|
2622
|
+
[disabled]="loading || !isFormValid()"
|
|
2623
|
+
class="btn btn-oauth btn-{{ provider }}">
|
|
2624
|
+
@if (getProviderIcon(provider)) {
|
|
2625
|
+
<span class="oauth-icon">
|
|
2626
|
+
{{ getProviderIcon(provider) }}
|
|
2627
|
+
</span>
|
|
2628
|
+
}
|
|
2629
|
+
{{ getProviderLabel(provider) }}
|
|
2630
|
+
</button>
|
|
2631
|
+
}
|
|
2632
|
+
</div>
|
|
2633
|
+
</div>
|
|
2634
|
+
|
|
2635
|
+
<!-- Switch to Email/Password -->
|
|
2636
|
+
@if (isProviderEnabled('emailPassword')) {
|
|
2637
|
+
<div class="switch-method">
|
|
2638
|
+
<a href="#" (click)="toggleAuthMethod($event)">
|
|
2639
|
+
{{ useEmailPassword ? 'Use OAuth instead' : 'Use email/password instead' }}
|
|
2640
|
+
</a>
|
|
2641
|
+
</div>
|
|
2642
|
+
}
|
|
2643
|
+
}
|
|
2644
|
+
|
|
2645
|
+
<!-- Email/Password Form (Fallback or Manual Entry) -->
|
|
2646
|
+
@if (useEmailPassword || oauthProviders.length === 0) {
|
|
2647
|
+
<div class="form-group">
|
|
2648
|
+
<label for="displayName">Full Name *</label>
|
|
2649
|
+
<input
|
|
2650
|
+
id="displayName"
|
|
2651
|
+
[(ngModel)]="displayName"
|
|
2652
|
+
name="displayName"
|
|
2653
|
+
placeholder="Enter your full name"
|
|
2654
|
+
type="text"
|
|
2655
|
+
required
|
|
2656
|
+
class="form-control">
|
|
2657
|
+
</div>
|
|
2658
|
+
|
|
2659
|
+
<div class="form-group">
|
|
2660
|
+
<label for="email">Email *</label>
|
|
2661
|
+
<input
|
|
2662
|
+
id="email"
|
|
2663
|
+
[(ngModel)]="email"
|
|
2664
|
+
name="email"
|
|
2665
|
+
placeholder="Enter your email"
|
|
2666
|
+
type="email"
|
|
2667
|
+
required
|
|
2668
|
+
class="form-control">
|
|
2669
|
+
</div>
|
|
2670
|
+
|
|
2671
|
+
<div class="form-group">
|
|
2672
|
+
<label for="password">Password *</label>
|
|
2673
|
+
<input
|
|
2674
|
+
id="password"
|
|
2675
|
+
[(ngModel)]="password"
|
|
2676
|
+
name="password"
|
|
2677
|
+
placeholder="Create a password"
|
|
2678
|
+
type="password"
|
|
2679
|
+
required
|
|
2680
|
+
minlength="8"
|
|
2681
|
+
class="form-control">
|
|
2682
|
+
<small class="form-hint">At least 8 characters</small>
|
|
2683
|
+
</div>
|
|
2684
|
+
|
|
2685
|
+
<div class="form-group">
|
|
2686
|
+
<label for="confirmPassword">Confirm Password *</label>
|
|
2687
|
+
<input
|
|
2688
|
+
id="confirmPassword"
|
|
2689
|
+
[(ngModel)]="confirmPassword"
|
|
2690
|
+
name="confirmPassword"
|
|
2691
|
+
placeholder="Confirm your password"
|
|
2692
|
+
type="password"
|
|
2693
|
+
required
|
|
2694
|
+
class="form-control">
|
|
2695
|
+
</div>
|
|
2696
|
+
|
|
2697
|
+
<button
|
|
2698
|
+
type="submit"
|
|
2699
|
+
[disabled]="loading || !isFormValid()"
|
|
2700
|
+
class="btn btn-primary btn-block">
|
|
2701
|
+
{{ loading ? 'Creating...' : submitButtonText }}
|
|
2702
|
+
</button>
|
|
2703
|
+
|
|
2704
|
+
<!-- Switch to OAuth -->
|
|
2705
|
+
@if (oauthProviders.length > 0) {
|
|
2706
|
+
<div class="switch-method">
|
|
2707
|
+
<a href="#" (click)="toggleAuthMethod($event)">
|
|
2708
|
+
Use OAuth instead
|
|
2709
|
+
</a>
|
|
2710
|
+
</div>
|
|
2711
|
+
}
|
|
2712
|
+
}
|
|
2713
|
+
</form>
|
|
2714
|
+
|
|
2715
|
+
<!-- Error Message -->
|
|
2716
|
+
@if (error) {
|
|
2717
|
+
<div class="error-message">
|
|
2718
|
+
{{ error }}
|
|
2719
|
+
</div>
|
|
2720
|
+
}
|
|
2721
|
+
|
|
2722
|
+
<!-- Success Message -->
|
|
2723
|
+
@if (success) {
|
|
2724
|
+
<div class="success-message">
|
|
2725
|
+
{{ success }}
|
|
2726
|
+
</div>
|
|
2727
|
+
}
|
|
2728
|
+
|
|
2729
|
+
<!-- Loading Overlay -->
|
|
2730
|
+
@if (loading) {
|
|
2731
|
+
<div class="loading-overlay">
|
|
2732
|
+
<div class="spinner"></div>
|
|
2733
|
+
<p class="loading-text">{{ loadingText }}</p>
|
|
2734
|
+
</div>
|
|
2735
|
+
}
|
|
2736
|
+
|
|
2737
|
+
<!-- Login Link -->
|
|
2738
|
+
<div class="login-link">
|
|
2739
|
+
{{ loginLinkText }}
|
|
2740
|
+
<a href="#" (click)="onLoginClick($event)">{{ loginLinkAction }}</a>
|
|
2741
|
+
</div>
|
|
2742
|
+
</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"] }] });
|
|
2744
|
+
}
|
|
2745
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TenantRegisterComponent, decorators: [{
|
|
2746
|
+
type: Component,
|
|
2747
|
+
args: [{ selector: 'lib-tenant-register', standalone: true, imports: [CommonModule, FormsModule], template: `
|
|
2748
|
+
<div class="tenant-register-dialog">
|
|
2749
|
+
<h2 class="register-title">{{ title }}</h2>
|
|
2750
|
+
|
|
2751
|
+
<!-- Ownership Warning Message -->
|
|
2752
|
+
<div class="warning-box">
|
|
2753
|
+
<div class="warning-icon">⚠️</div>
|
|
2754
|
+
<div class="warning-content">
|
|
2755
|
+
<strong>{{ ownershipTitle }}</strong>
|
|
2756
|
+
<p>{{ ownershipMessage }}</p>
|
|
2757
|
+
</div>
|
|
2758
|
+
</div>
|
|
2759
|
+
|
|
2760
|
+
<form (ngSubmit)="onRegister()" class="register-form">
|
|
2761
|
+
<!-- Tenant Information Section -->
|
|
2762
|
+
<div class="section-header">{{ tenantSectionTitle }}</div>
|
|
2763
|
+
|
|
2764
|
+
<div class="form-group">
|
|
2765
|
+
<label for="tenantName">{{ tenantNameLabel }} *</label>
|
|
2766
|
+
<input
|
|
2767
|
+
id="tenantName"
|
|
2768
|
+
[(ngModel)]="tenantName"
|
|
2769
|
+
name="tenantName"
|
|
2770
|
+
[placeholder]="tenantNamePlaceholder"
|
|
2771
|
+
type="text"
|
|
2772
|
+
required
|
|
2773
|
+
(input)="onTenantNameChange()"
|
|
2774
|
+
class="form-control">
|
|
2775
|
+
</div>
|
|
2776
|
+
|
|
2777
|
+
<div class="form-group">
|
|
2778
|
+
<label for="tenantSlug">{{ tenantSlugLabel }} *</label>
|
|
2779
|
+
<input
|
|
2780
|
+
id="tenantSlug"
|
|
2781
|
+
[(ngModel)]="tenantSlug"
|
|
2782
|
+
name="tenantSlug"
|
|
2783
|
+
[placeholder]="tenantSlugPlaceholder"
|
|
2784
|
+
type="text"
|
|
2785
|
+
required
|
|
2786
|
+
pattern="[a-z0-9-]+"
|
|
2787
|
+
(blur)="checkSlugAvailability()"
|
|
2788
|
+
class="form-control"
|
|
2789
|
+
[class.input-error]="slugError"
|
|
2790
|
+
[class.input-success]="slugAvailable && tenantSlug">
|
|
2791
|
+
@if (tenantSlug && urlPreviewEnabled) {
|
|
2792
|
+
<small class="form-hint">
|
|
2793
|
+
{{ urlPreviewPrefix }}{{ tenantSlug }}
|
|
2794
|
+
@if (checkingSlug) {
|
|
2795
|
+
<span class="checking">Checking...</span>
|
|
2796
|
+
}
|
|
2797
|
+
@if (slugAvailable && !checkingSlug) {
|
|
2798
|
+
<span class="available">✓ Available</span>
|
|
2799
|
+
}
|
|
2800
|
+
@if (slugError) {
|
|
2801
|
+
<span class="error-text">{{ slugError }}</span>
|
|
2802
|
+
}
|
|
2803
|
+
</small>
|
|
2804
|
+
}
|
|
2805
|
+
</div>
|
|
2806
|
+
|
|
2807
|
+
<!-- User Information Section -->
|
|
2808
|
+
<div class="section-header">{{ userSectionTitle }}</div>
|
|
2809
|
+
|
|
2810
|
+
<!-- OAuth Providers (Primary Option) -->
|
|
2811
|
+
@if (oauthProviders.length > 0 && !useEmailPassword) {
|
|
2812
|
+
<div class="oauth-section">
|
|
2813
|
+
<p class="oauth-description">{{ oauthDescription }}</p>
|
|
2814
|
+
<div class="oauth-buttons">
|
|
2815
|
+
@for (provider of oauthProviders; track provider) {
|
|
2816
|
+
<button
|
|
2817
|
+
type="button"
|
|
2818
|
+
(click)="onOAuthRegister(provider)"
|
|
2819
|
+
[disabled]="loading || !isFormValid()"
|
|
2820
|
+
class="btn btn-oauth btn-{{ provider }}">
|
|
2821
|
+
@if (getProviderIcon(provider)) {
|
|
2822
|
+
<span class="oauth-icon">
|
|
2823
|
+
{{ getProviderIcon(provider) }}
|
|
2824
|
+
</span>
|
|
2825
|
+
}
|
|
2826
|
+
{{ getProviderLabel(provider) }}
|
|
2827
|
+
</button>
|
|
2828
|
+
}
|
|
2829
|
+
</div>
|
|
2830
|
+
</div>
|
|
2831
|
+
|
|
2832
|
+
<!-- Switch to Email/Password -->
|
|
2833
|
+
@if (isProviderEnabled('emailPassword')) {
|
|
2834
|
+
<div class="switch-method">
|
|
2835
|
+
<a href="#" (click)="toggleAuthMethod($event)">
|
|
2836
|
+
{{ useEmailPassword ? 'Use OAuth instead' : 'Use email/password instead' }}
|
|
2837
|
+
</a>
|
|
2838
|
+
</div>
|
|
2839
|
+
}
|
|
2840
|
+
}
|
|
2841
|
+
|
|
2842
|
+
<!-- Email/Password Form (Fallback or Manual Entry) -->
|
|
2843
|
+
@if (useEmailPassword || oauthProviders.length === 0) {
|
|
2844
|
+
<div class="form-group">
|
|
2845
|
+
<label for="displayName">Full Name *</label>
|
|
2846
|
+
<input
|
|
2847
|
+
id="displayName"
|
|
2848
|
+
[(ngModel)]="displayName"
|
|
2849
|
+
name="displayName"
|
|
2850
|
+
placeholder="Enter your full name"
|
|
2851
|
+
type="text"
|
|
2852
|
+
required
|
|
2853
|
+
class="form-control">
|
|
2854
|
+
</div>
|
|
2855
|
+
|
|
2856
|
+
<div class="form-group">
|
|
2857
|
+
<label for="email">Email *</label>
|
|
2858
|
+
<input
|
|
2859
|
+
id="email"
|
|
2860
|
+
[(ngModel)]="email"
|
|
2861
|
+
name="email"
|
|
2862
|
+
placeholder="Enter your email"
|
|
2863
|
+
type="email"
|
|
2864
|
+
required
|
|
2865
|
+
class="form-control">
|
|
2866
|
+
</div>
|
|
2867
|
+
|
|
2868
|
+
<div class="form-group">
|
|
2869
|
+
<label for="password">Password *</label>
|
|
2870
|
+
<input
|
|
2871
|
+
id="password"
|
|
2872
|
+
[(ngModel)]="password"
|
|
2873
|
+
name="password"
|
|
2874
|
+
placeholder="Create a password"
|
|
2875
|
+
type="password"
|
|
2876
|
+
required
|
|
2877
|
+
minlength="8"
|
|
2878
|
+
class="form-control">
|
|
2879
|
+
<small class="form-hint">At least 8 characters</small>
|
|
2880
|
+
</div>
|
|
2881
|
+
|
|
2882
|
+
<div class="form-group">
|
|
2883
|
+
<label for="confirmPassword">Confirm Password *</label>
|
|
2884
|
+
<input
|
|
2885
|
+
id="confirmPassword"
|
|
2886
|
+
[(ngModel)]="confirmPassword"
|
|
2887
|
+
name="confirmPassword"
|
|
2888
|
+
placeholder="Confirm your password"
|
|
2889
|
+
type="password"
|
|
2890
|
+
required
|
|
2891
|
+
class="form-control">
|
|
2892
|
+
</div>
|
|
2893
|
+
|
|
2894
|
+
<button
|
|
2895
|
+
type="submit"
|
|
2896
|
+
[disabled]="loading || !isFormValid()"
|
|
2897
|
+
class="btn btn-primary btn-block">
|
|
2898
|
+
{{ loading ? 'Creating...' : submitButtonText }}
|
|
2899
|
+
</button>
|
|
2900
|
+
|
|
2901
|
+
<!-- Switch to OAuth -->
|
|
2902
|
+
@if (oauthProviders.length > 0) {
|
|
2903
|
+
<div class="switch-method">
|
|
2904
|
+
<a href="#" (click)="toggleAuthMethod($event)">
|
|
2905
|
+
Use OAuth instead
|
|
2906
|
+
</a>
|
|
2907
|
+
</div>
|
|
2908
|
+
}
|
|
2909
|
+
}
|
|
2910
|
+
</form>
|
|
2911
|
+
|
|
2912
|
+
<!-- Error Message -->
|
|
2913
|
+
@if (error) {
|
|
2914
|
+
<div class="error-message">
|
|
2915
|
+
{{ error }}
|
|
2916
|
+
</div>
|
|
2917
|
+
}
|
|
2918
|
+
|
|
2919
|
+
<!-- Success Message -->
|
|
2920
|
+
@if (success) {
|
|
2921
|
+
<div class="success-message">
|
|
2922
|
+
{{ success }}
|
|
2923
|
+
</div>
|
|
2924
|
+
}
|
|
2925
|
+
|
|
2926
|
+
<!-- Loading Overlay -->
|
|
2927
|
+
@if (loading) {
|
|
2928
|
+
<div class="loading-overlay">
|
|
2929
|
+
<div class="spinner"></div>
|
|
2930
|
+
<p class="loading-text">{{ loadingText }}</p>
|
|
2931
|
+
</div>
|
|
2932
|
+
}
|
|
2933
|
+
|
|
2934
|
+
<!-- Login Link -->
|
|
2935
|
+
<div class="login-link">
|
|
2936
|
+
{{ loginLinkText }}
|
|
2937
|
+
<a href="#" (click)="onLoginClick($event)">{{ loginLinkAction }}</a>
|
|
2938
|
+
</div>
|
|
2939
|
+
</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"] }]
|
|
2941
|
+
}], ctorParameters: () => [{ type: AuthService }], propDecorators: { title: [{
|
|
2942
|
+
type: Input
|
|
2943
|
+
}], providers: [{
|
|
2944
|
+
type: Input
|
|
2945
|
+
}], requireTenantName: [{
|
|
2946
|
+
type: Input
|
|
2947
|
+
}], tenantSectionTitle: [{
|
|
2948
|
+
type: Input
|
|
2949
|
+
}], tenantNameLabel: [{
|
|
2950
|
+
type: Input
|
|
2951
|
+
}], tenantNamePlaceholder: [{
|
|
2952
|
+
type: Input
|
|
2953
|
+
}], tenantSlugLabel: [{
|
|
2954
|
+
type: Input
|
|
2955
|
+
}], tenantSlugPlaceholder: [{
|
|
2956
|
+
type: Input
|
|
2957
|
+
}], urlPreviewEnabled: [{
|
|
2958
|
+
type: Input
|
|
2959
|
+
}], urlPreviewPrefix: [{
|
|
2960
|
+
type: Input
|
|
2961
|
+
}], userSectionTitle: [{
|
|
2962
|
+
type: Input
|
|
2963
|
+
}], oauthDescription: [{
|
|
2964
|
+
type: Input
|
|
2965
|
+
}], ownershipTitle: [{
|
|
2966
|
+
type: Input
|
|
2967
|
+
}], ownershipMessage: [{
|
|
2968
|
+
type: Input
|
|
2969
|
+
}], submitButtonText: [{
|
|
2970
|
+
type: Input
|
|
2971
|
+
}], loginLinkText: [{
|
|
2972
|
+
type: Input
|
|
2973
|
+
}], loginLinkAction: [{
|
|
2974
|
+
type: Input
|
|
2975
|
+
}], tenantCreated: [{
|
|
2976
|
+
type: Output
|
|
2977
|
+
}], navigateToLogin: [{
|
|
2978
|
+
type: Output
|
|
2979
|
+
}] } });
|
|
2980
|
+
|
|
2981
|
+
/**
|
|
2982
|
+
* Dialog wrapper for TenantLoginComponent
|
|
2983
|
+
*
|
|
2984
|
+
* Usage with Angular Material Dialog:
|
|
2985
|
+
* ```typescript
|
|
2986
|
+
* const dialogRef = this.dialog.open(TenantLoginDialogComponent, {
|
|
2987
|
+
* width: '450px',
|
|
2988
|
+
* data: {
|
|
2989
|
+
* providers: ['google'],
|
|
2990
|
+
* showTenantSelector: true
|
|
2991
|
+
* }
|
|
2992
|
+
* });
|
|
2993
|
+
*
|
|
2994
|
+
* dialogRef.afterClosed().subscribe(result => {
|
|
2995
|
+
* if (result && result.tenantId) {
|
|
2996
|
+
* console.log('Logged in to tenant:', result.tenantSlug);
|
|
2997
|
+
* // Redirect to dashboard
|
|
2998
|
+
* }
|
|
2999
|
+
* });
|
|
3000
|
+
* ```
|
|
3001
|
+
*
|
|
3002
|
+
* Usage with custom dialog service:
|
|
3003
|
+
* ```typescript
|
|
3004
|
+
* const dialog = this.dialogService.open(TenantLoginDialogComponent, {
|
|
3005
|
+
* providers: ['google', 'emailPassword'],
|
|
3006
|
+
* autoSelectSingleTenant: true
|
|
3007
|
+
* });
|
|
3008
|
+
* ```
|
|
3009
|
+
*/
|
|
3010
|
+
class TenantLoginDialogComponent {
|
|
3011
|
+
data;
|
|
3012
|
+
dialogRef;
|
|
3013
|
+
constructor(injectedData, injectedDialogRef) {
|
|
3014
|
+
// Support both Angular Material Dialog and custom dialog implementations
|
|
3015
|
+
this.data = injectedData || {};
|
|
3016
|
+
this.dialogRef = injectedDialogRef;
|
|
3017
|
+
}
|
|
3018
|
+
onTenantSelected(event) {
|
|
3019
|
+
// Close dialog and return the selected tenant
|
|
3020
|
+
if (this.dialogRef && this.dialogRef.close) {
|
|
3021
|
+
this.dialogRef.close(event);
|
|
3022
|
+
}
|
|
3023
|
+
}
|
|
3024
|
+
onCreateTenant() {
|
|
3025
|
+
// Close dialog and signal that user wants to create a tenant
|
|
3026
|
+
if (this.dialogRef && this.dialogRef.close) {
|
|
3027
|
+
this.dialogRef.close({ action: 'create_tenant' });
|
|
3028
|
+
}
|
|
3029
|
+
}
|
|
3030
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TenantLoginDialogComponent, deps: [{ token: 'DIALOG_DATA', optional: true }, { token: 'DIALOG_REF', optional: true }], target: i0.ɵɵFactoryTarget.Component });
|
|
3031
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.15", type: TenantLoginDialogComponent, isStandalone: true, selector: "lib-tenant-login-dialog", ngImport: i0, template: `
|
|
3032
|
+
<div class="dialog-wrapper">
|
|
3033
|
+
<lib-tenant-login
|
|
3034
|
+
[title]="data?.title || 'Sign In'"
|
|
3035
|
+
[providers]="data?.providers || ['google']"
|
|
3036
|
+
[showTenantSelector]="data?.showTenantSelector !== false"
|
|
3037
|
+
[autoSelectSingleTenant]="data?.autoSelectSingleTenant !== false"
|
|
3038
|
+
[allowTenantCreation]="data?.allowTenantCreation !== false"
|
|
3039
|
+
[tenantSelectorTitle]="data?.tenantSelectorTitle || 'Select Organization'"
|
|
3040
|
+
[tenantSelectorDescription]="data?.tenantSelectorDescription || 'Choose which organization you want to access:'"
|
|
3041
|
+
[continueButtonText]="data?.continueButtonText || 'Continue'"
|
|
3042
|
+
[registerLinkText]="data?.registerLinkText || 'Don\\'t have an account?'"
|
|
3043
|
+
[registerLinkAction]="data?.registerLinkAction || 'Sign up'"
|
|
3044
|
+
[createTenantLinkText]="data?.createTenantLinkText || 'Don\\'t see your organization?'"
|
|
3045
|
+
[createTenantLinkAction]="data?.createTenantLinkAction || 'Create New Organization'"
|
|
3046
|
+
(tenantSelected)="onTenantSelected($event)"
|
|
3047
|
+
(createTenant)="onCreateTenant()">
|
|
3048
|
+
</lib-tenant-login>
|
|
3049
|
+
</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"] }] });
|
|
3051
|
+
}
|
|
3052
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TenantLoginDialogComponent, decorators: [{
|
|
3053
|
+
type: Component,
|
|
3054
|
+
args: [{ selector: 'lib-tenant-login-dialog', standalone: true, imports: [CommonModule, TenantLoginComponent], template: `
|
|
3055
|
+
<div class="dialog-wrapper">
|
|
3056
|
+
<lib-tenant-login
|
|
3057
|
+
[title]="data?.title || 'Sign In'"
|
|
3058
|
+
[providers]="data?.providers || ['google']"
|
|
3059
|
+
[showTenantSelector]="data?.showTenantSelector !== false"
|
|
3060
|
+
[autoSelectSingleTenant]="data?.autoSelectSingleTenant !== false"
|
|
3061
|
+
[allowTenantCreation]="data?.allowTenantCreation !== false"
|
|
3062
|
+
[tenantSelectorTitle]="data?.tenantSelectorTitle || 'Select Organization'"
|
|
3063
|
+
[tenantSelectorDescription]="data?.tenantSelectorDescription || 'Choose which organization you want to access:'"
|
|
3064
|
+
[continueButtonText]="data?.continueButtonText || 'Continue'"
|
|
3065
|
+
[registerLinkText]="data?.registerLinkText || 'Don\\'t have an account?'"
|
|
3066
|
+
[registerLinkAction]="data?.registerLinkAction || 'Sign up'"
|
|
3067
|
+
[createTenantLinkText]="data?.createTenantLinkText || 'Don\\'t see your organization?'"
|
|
3068
|
+
[createTenantLinkAction]="data?.createTenantLinkAction || 'Create New Organization'"
|
|
3069
|
+
(tenantSelected)="onTenantSelected($event)"
|
|
3070
|
+
(createTenant)="onCreateTenant()">
|
|
3071
|
+
</lib-tenant-login>
|
|
3072
|
+
</div>
|
|
3073
|
+
`, styles: [".dialog-wrapper{padding:0}\n"] }]
|
|
3074
|
+
}], ctorParameters: () => [{ type: undefined, decorators: [{
|
|
3075
|
+
type: Optional
|
|
3076
|
+
}, {
|
|
3077
|
+
type: Inject,
|
|
3078
|
+
args: ['DIALOG_DATA']
|
|
3079
|
+
}] }, { type: undefined, decorators: [{
|
|
3080
|
+
type: Optional
|
|
3081
|
+
}, {
|
|
3082
|
+
type: Inject,
|
|
3083
|
+
args: ['DIALOG_REF']
|
|
3084
|
+
}] }] });
|
|
3085
|
+
|
|
3086
|
+
/**
|
|
3087
|
+
* Dialog wrapper for TenantRegisterComponent
|
|
3088
|
+
*
|
|
3089
|
+
* Usage with Angular Material Dialog:
|
|
3090
|
+
* ```typescript
|
|
3091
|
+
* const dialogRef = this.dialog.open(TenantRegisterDialogComponent, {
|
|
3092
|
+
* width: '500px',
|
|
3093
|
+
* data: {
|
|
3094
|
+
* providers: ['google'],
|
|
3095
|
+
* tenantNameLabel: 'Store Name'
|
|
3096
|
+
* }
|
|
3097
|
+
* });
|
|
3098
|
+
*
|
|
3099
|
+
* dialogRef.afterClosed().subscribe(result => {
|
|
3100
|
+
* if (result && result.tenant) {
|
|
3101
|
+
* console.log('Tenant created:', result.tenant);
|
|
3102
|
+
* }
|
|
3103
|
+
* });
|
|
3104
|
+
* ```
|
|
3105
|
+
*
|
|
3106
|
+
* Usage with custom dialog service:
|
|
3107
|
+
* ```typescript
|
|
3108
|
+
* const dialog = this.dialogService.open(TenantRegisterDialogComponent, {
|
|
3109
|
+
* providers: ['google', 'emailPassword']
|
|
3110
|
+
* });
|
|
3111
|
+
* ```
|
|
3112
|
+
*/
|
|
3113
|
+
class TenantRegisterDialogComponent {
|
|
3114
|
+
data;
|
|
3115
|
+
dialogRef;
|
|
3116
|
+
constructor(injectedData, injectedDialogRef) {
|
|
3117
|
+
// Support both Angular Material Dialog and custom dialog implementations
|
|
3118
|
+
this.data = injectedData || {};
|
|
3119
|
+
this.dialogRef = injectedDialogRef;
|
|
3120
|
+
}
|
|
3121
|
+
onTenantCreated(event) {
|
|
3122
|
+
// Close dialog and return the created tenant
|
|
3123
|
+
if (this.dialogRef && this.dialogRef.close) {
|
|
3124
|
+
this.dialogRef.close(event);
|
|
3125
|
+
}
|
|
3126
|
+
}
|
|
3127
|
+
onNavigateToLogin() {
|
|
3128
|
+
// Close dialog without result (user wants to login instead)
|
|
3129
|
+
if (this.dialogRef && this.dialogRef.close) {
|
|
3130
|
+
this.dialogRef.close({ action: 'navigate_to_login' });
|
|
3131
|
+
}
|
|
3132
|
+
}
|
|
3133
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TenantRegisterDialogComponent, deps: [{ token: 'DIALOG_DATA', optional: true }, { token: 'DIALOG_REF', optional: true }], target: i0.ɵɵFactoryTarget.Component });
|
|
3134
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "14.0.0", version: "20.3.15", type: TenantRegisterDialogComponent, isStandalone: true, selector: "lib-tenant-register-dialog", ngImport: i0, template: `
|
|
3135
|
+
<div class="dialog-wrapper">
|
|
3136
|
+
<lib-tenant-register
|
|
3137
|
+
[title]="data?.title || 'Create New Organization'"
|
|
3138
|
+
[providers]="data?.providers || ['google']"
|
|
3139
|
+
[requireTenantName]="data?.requireTenantName !== false"
|
|
3140
|
+
[tenantSectionTitle]="data?.tenantSectionTitle || 'Organization Information'"
|
|
3141
|
+
[tenantNameLabel]="data?.tenantNameLabel || 'Organization Name'"
|
|
3142
|
+
[tenantNamePlaceholder]="data?.tenantNamePlaceholder || 'Enter your organization name'"
|
|
3143
|
+
[tenantSlugLabel]="data?.tenantSlugLabel || 'Organization URL'"
|
|
3144
|
+
[tenantSlugPlaceholder]="data?.tenantSlugPlaceholder || 'organization-name'"
|
|
3145
|
+
[urlPreviewEnabled]="data?.urlPreviewEnabled !== false"
|
|
3146
|
+
[urlPreviewPrefix]="data?.urlPreviewPrefix || 'app.example.com/'"
|
|
3147
|
+
[userSectionTitle]="data?.userSectionTitle || 'Your Information'"
|
|
3148
|
+
[oauthDescription]="data?.oauthDescription || 'Recommended: Sign up with your Google account'"
|
|
3149
|
+
[ownershipTitle]="data?.ownershipTitle || 'CREATING A NEW ORGANIZATION'"
|
|
3150
|
+
[ownershipMessage]="data?.ownershipMessage || 'You are registering as an organization owner. If you are an employee, use Login instead.'"
|
|
3151
|
+
[submitButtonText]="data?.submitButtonText || 'Create Organization'"
|
|
3152
|
+
[loginLinkText]="data?.loginLinkText || 'Already have an account?'"
|
|
3153
|
+
[loginLinkAction]="data?.loginLinkAction || 'Sign in'"
|
|
3154
|
+
(tenantCreated)="onTenantCreated($event)"
|
|
3155
|
+
(navigateToLogin)="onNavigateToLogin()">
|
|
3156
|
+
</lib-tenant-register>
|
|
3157
|
+
</div>
|
|
3158
|
+
`, isInline: true, styles: [".dialog-wrapper{padding:0}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: TenantRegisterComponent, selector: "lib-tenant-register", inputs: ["title", "providers", "requireTenantName", "tenantSectionTitle", "tenantNameLabel", "tenantNamePlaceholder", "tenantSlugLabel", "tenantSlugPlaceholder", "urlPreviewEnabled", "urlPreviewPrefix", "userSectionTitle", "oauthDescription", "ownershipTitle", "ownershipMessage", "submitButtonText", "loginLinkText", "loginLinkAction"], outputs: ["tenantCreated", "navigateToLogin"] }] });
|
|
3159
|
+
}
|
|
3160
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TenantRegisterDialogComponent, decorators: [{
|
|
3161
|
+
type: Component,
|
|
3162
|
+
args: [{ selector: 'lib-tenant-register-dialog', standalone: true, imports: [CommonModule, TenantRegisterComponent], template: `
|
|
3163
|
+
<div class="dialog-wrapper">
|
|
3164
|
+
<lib-tenant-register
|
|
3165
|
+
[title]="data?.title || 'Create New Organization'"
|
|
3166
|
+
[providers]="data?.providers || ['google']"
|
|
3167
|
+
[requireTenantName]="data?.requireTenantName !== false"
|
|
3168
|
+
[tenantSectionTitle]="data?.tenantSectionTitle || 'Organization Information'"
|
|
3169
|
+
[tenantNameLabel]="data?.tenantNameLabel || 'Organization Name'"
|
|
3170
|
+
[tenantNamePlaceholder]="data?.tenantNamePlaceholder || 'Enter your organization name'"
|
|
3171
|
+
[tenantSlugLabel]="data?.tenantSlugLabel || 'Organization URL'"
|
|
3172
|
+
[tenantSlugPlaceholder]="data?.tenantSlugPlaceholder || 'organization-name'"
|
|
3173
|
+
[urlPreviewEnabled]="data?.urlPreviewEnabled !== false"
|
|
3174
|
+
[urlPreviewPrefix]="data?.urlPreviewPrefix || 'app.example.com/'"
|
|
3175
|
+
[userSectionTitle]="data?.userSectionTitle || 'Your Information'"
|
|
3176
|
+
[oauthDescription]="data?.oauthDescription || 'Recommended: Sign up with your Google account'"
|
|
3177
|
+
[ownershipTitle]="data?.ownershipTitle || 'CREATING A NEW ORGANIZATION'"
|
|
3178
|
+
[ownershipMessage]="data?.ownershipMessage || 'You are registering as an organization owner. If you are an employee, use Login instead.'"
|
|
3179
|
+
[submitButtonText]="data?.submitButtonText || 'Create Organization'"
|
|
3180
|
+
[loginLinkText]="data?.loginLinkText || 'Already have an account?'"
|
|
3181
|
+
[loginLinkAction]="data?.loginLinkAction || 'Sign in'"
|
|
3182
|
+
(tenantCreated)="onTenantCreated($event)"
|
|
3183
|
+
(navigateToLogin)="onNavigateToLogin()">
|
|
3184
|
+
</lib-tenant-register>
|
|
3185
|
+
</div>
|
|
3186
|
+
`, styles: [".dialog-wrapper{padding:0}\n"] }]
|
|
3187
|
+
}], ctorParameters: () => [{ type: undefined, decorators: [{
|
|
3188
|
+
type: Optional
|
|
3189
|
+
}, {
|
|
3190
|
+
type: Inject,
|
|
3191
|
+
args: ['DIALOG_DATA']
|
|
3192
|
+
}] }, { type: undefined, decorators: [{
|
|
3193
|
+
type: Optional
|
|
3194
|
+
}, {
|
|
3195
|
+
type: Inject,
|
|
3196
|
+
args: ['DIALOG_REF']
|
|
3197
|
+
}] }] });
|
|
3198
|
+
|
|
1458
3199
|
/*
|
|
1459
3200
|
* Public API Surface of ngx-stonescriptphp-client
|
|
1460
3201
|
*/
|
|
@@ -1463,5 +3204,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
|
|
|
1463
3204
|
* Generated bundle index. Do not edit.
|
|
1464
3205
|
*/
|
|
1465
3206
|
|
|
1466
|
-
export { ApiConnectionService, ApiResponse, AuthService, CsrfService, DbService, LoginDialogComponent, MyEnvironmentModel, NgxStoneScriptPhpClientModule, RegisterComponent, SigninStatusService, TokenService, VerifyStatus };
|
|
3207
|
+
export { ApiConnectionService, ApiResponse, AuthService, CsrfService, DbService, LoginDialogComponent, MyEnvironmentModel, NgxStoneScriptPhpClientModule, RegisterComponent, SigninStatusService, TenantLoginComponent, TenantLoginDialogComponent, TenantRegisterComponent, TenantRegisterDialogComponent, TokenService, VerifyStatus };
|
|
1467
3208
|
//# sourceMappingURL=progalaxyelabs-ngx-stonescriptphp-client.mjs.map
|