@progalaxyelabs/ngx-stonescriptphp-client 1.3.0 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { Injectable, Inject, NgModule,
|
|
2
|
+
import { Injectable, Inject, NgModule, EventEmitter, Output, Input, Component, Optional } from '@angular/core';
|
|
3
3
|
import { BehaviorSubject } from 'rxjs';
|
|
4
|
+
import * as i2$1 from '@angular/common';
|
|
4
5
|
import { CommonModule } from '@angular/common';
|
|
5
6
|
import * as i2 from '@angular/forms';
|
|
6
7
|
import { FormsModule } from '@angular/forms';
|
|
@@ -194,6 +195,11 @@ class MyEnvironmentModel {
|
|
|
194
195
|
csrfTokenCookieName: 'csrf_token',
|
|
195
196
|
csrfHeaderName: 'X-CSRF-Token'
|
|
196
197
|
};
|
|
198
|
+
/**
|
|
199
|
+
* Branding configuration for auth components
|
|
200
|
+
* Allows platforms to customize login/register pages without creating wrappers
|
|
201
|
+
*/
|
|
202
|
+
branding;
|
|
197
203
|
}
|
|
198
204
|
|
|
199
205
|
/**
|
|
@@ -576,6 +582,7 @@ class AuthService {
|
|
|
576
582
|
tokens;
|
|
577
583
|
signinStatus;
|
|
578
584
|
environment;
|
|
585
|
+
USER_STORAGE_KEY = 'progalaxyapi_user';
|
|
579
586
|
// Observable user state
|
|
580
587
|
userSubject = new BehaviorSubject(null);
|
|
581
588
|
user$ = this.userSubject.asObservable();
|
|
@@ -583,6 +590,46 @@ class AuthService {
|
|
|
583
590
|
this.tokens = tokens;
|
|
584
591
|
this.signinStatus = signinStatus;
|
|
585
592
|
this.environment = environment;
|
|
593
|
+
// Restore user from localStorage on initialization
|
|
594
|
+
this.restoreUser();
|
|
595
|
+
}
|
|
596
|
+
/**
|
|
597
|
+
* Restore user from localStorage
|
|
598
|
+
*/
|
|
599
|
+
restoreUser() {
|
|
600
|
+
try {
|
|
601
|
+
const userJson = localStorage.getItem(this.USER_STORAGE_KEY);
|
|
602
|
+
if (userJson) {
|
|
603
|
+
const user = JSON.parse(userJson);
|
|
604
|
+
this.updateUser(user);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
catch (error) {
|
|
608
|
+
console.error('Failed to restore user from localStorage:', error);
|
|
609
|
+
}
|
|
610
|
+
}
|
|
611
|
+
/**
|
|
612
|
+
* Save user to localStorage
|
|
613
|
+
*/
|
|
614
|
+
saveUser(user) {
|
|
615
|
+
try {
|
|
616
|
+
if (user) {
|
|
617
|
+
localStorage.setItem(this.USER_STORAGE_KEY, JSON.stringify(user));
|
|
618
|
+
}
|
|
619
|
+
else {
|
|
620
|
+
localStorage.removeItem(this.USER_STORAGE_KEY);
|
|
621
|
+
}
|
|
622
|
+
}
|
|
623
|
+
catch (error) {
|
|
624
|
+
console.error('Failed to save user to localStorage:', error);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
/**
|
|
628
|
+
* Update user subject and persist to localStorage
|
|
629
|
+
*/
|
|
630
|
+
updateUser(user) {
|
|
631
|
+
this.updateUser(user);
|
|
632
|
+
this.saveUser(user);
|
|
586
633
|
}
|
|
587
634
|
/**
|
|
588
635
|
* Login with email and password
|
|
@@ -603,7 +650,7 @@ class AuthService {
|
|
|
603
650
|
if (data.success && data.access_token) {
|
|
604
651
|
this.tokens.setAccessToken(data.access_token);
|
|
605
652
|
this.signinStatus.setSigninStatus(true);
|
|
606
|
-
this.
|
|
653
|
+
this.updateUser(data.user);
|
|
607
654
|
return { success: true, user: data.user };
|
|
608
655
|
}
|
|
609
656
|
return {
|
|
@@ -688,7 +735,7 @@ class AuthService {
|
|
|
688
735
|
if (event.data.type === 'oauth_success') {
|
|
689
736
|
this.tokens.setAccessToken(event.data.access_token);
|
|
690
737
|
this.signinStatus.setSigninStatus(true);
|
|
691
|
-
this.
|
|
738
|
+
this.updateUser(event.data.user);
|
|
692
739
|
window.removeEventListener('message', messageHandler);
|
|
693
740
|
popup.close();
|
|
694
741
|
resolve({
|
|
@@ -739,7 +786,7 @@ class AuthService {
|
|
|
739
786
|
if (data.success && data.access_token) {
|
|
740
787
|
this.tokens.setAccessToken(data.access_token);
|
|
741
788
|
this.signinStatus.setSigninStatus(true);
|
|
742
|
-
this.
|
|
789
|
+
this.updateUser(data.user);
|
|
743
790
|
return {
|
|
744
791
|
success: true,
|
|
745
792
|
user: data.user,
|
|
@@ -763,10 +810,19 @@ class AuthService {
|
|
|
763
810
|
*/
|
|
764
811
|
async signout() {
|
|
765
812
|
try {
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
813
|
+
const refreshToken = this.tokens.getRefreshToken();
|
|
814
|
+
if (refreshToken) {
|
|
815
|
+
await fetch(`${this.environment.accountsUrl}/api/auth/logout`, {
|
|
816
|
+
method: 'POST',
|
|
817
|
+
headers: {
|
|
818
|
+
'Content-Type': 'application/json'
|
|
819
|
+
},
|
|
820
|
+
credentials: 'include',
|
|
821
|
+
body: JSON.stringify({
|
|
822
|
+
refresh_token: refreshToken
|
|
823
|
+
})
|
|
824
|
+
});
|
|
825
|
+
}
|
|
770
826
|
}
|
|
771
827
|
catch (error) {
|
|
772
828
|
console.error('Logout API call failed:', error);
|
|
@@ -774,7 +830,7 @@ class AuthService {
|
|
|
774
830
|
finally {
|
|
775
831
|
this.tokens.clear();
|
|
776
832
|
this.signinStatus.setSigninStatus(false);
|
|
777
|
-
this.
|
|
833
|
+
this.updateUser(null);
|
|
778
834
|
}
|
|
779
835
|
}
|
|
780
836
|
/**
|
|
@@ -798,7 +854,7 @@ class AuthService {
|
|
|
798
854
|
const data = await response.json();
|
|
799
855
|
if (data.access_token) {
|
|
800
856
|
this.tokens.setAccessToken(data.access_token);
|
|
801
|
-
this.
|
|
857
|
+
this.updateUser(data.user);
|
|
802
858
|
this.signinStatus.setSigninStatus(true);
|
|
803
859
|
return true;
|
|
804
860
|
}
|
|
@@ -852,7 +908,7 @@ class AuthService {
|
|
|
852
908
|
this.tokens.setAccessToken(result.access_token);
|
|
853
909
|
this.signinStatus.setSigninStatus(true);
|
|
854
910
|
if (result.user) {
|
|
855
|
-
this.
|
|
911
|
+
this.updateUser(result.user);
|
|
856
912
|
}
|
|
857
913
|
}
|
|
858
914
|
return result;
|
|
@@ -902,7 +958,7 @@ class AuthService {
|
|
|
902
958
|
this.signinStatus.setSigninStatus(true);
|
|
903
959
|
}
|
|
904
960
|
if (event.data.user) {
|
|
905
|
-
this.
|
|
961
|
+
this.updateUser(event.data.user);
|
|
906
962
|
}
|
|
907
963
|
window.removeEventListener('message', messageHandler);
|
|
908
964
|
popup.close();
|
|
@@ -1094,6 +1150,65 @@ class AuthService {
|
|
|
1094
1150
|
return null;
|
|
1095
1151
|
}
|
|
1096
1152
|
}
|
|
1153
|
+
/**
|
|
1154
|
+
* Check if user has completed onboarding (has a tenant)
|
|
1155
|
+
*/
|
|
1156
|
+
async checkOnboardingStatus(identityId) {
|
|
1157
|
+
try {
|
|
1158
|
+
const response = await fetch(`${this.environment.accountsUrl}/api/auth/onboarding/status?platform_code=${this.environment.platformCode}&identity_id=${identityId}`, {
|
|
1159
|
+
method: 'GET',
|
|
1160
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1161
|
+
credentials: 'include'
|
|
1162
|
+
});
|
|
1163
|
+
if (!response.ok) {
|
|
1164
|
+
throw new Error('Failed to check onboarding status');
|
|
1165
|
+
}
|
|
1166
|
+
return await response.json();
|
|
1167
|
+
}
|
|
1168
|
+
catch (error) {
|
|
1169
|
+
throw error;
|
|
1170
|
+
}
|
|
1171
|
+
}
|
|
1172
|
+
/**
|
|
1173
|
+
* Complete tenant onboarding (create tenant with country + org name)
|
|
1174
|
+
*/
|
|
1175
|
+
async completeTenantOnboarding(countryCode, tenantName) {
|
|
1176
|
+
try {
|
|
1177
|
+
const accessToken = this.tokens.getAccessToken();
|
|
1178
|
+
if (!accessToken) {
|
|
1179
|
+
throw new Error('Not authenticated');
|
|
1180
|
+
}
|
|
1181
|
+
const response = await fetch(`${this.environment.accountsUrl}/api/auth/register-tenant`, {
|
|
1182
|
+
method: 'POST',
|
|
1183
|
+
headers: {
|
|
1184
|
+
'Content-Type': 'application/json',
|
|
1185
|
+
'Authorization': `Bearer ${accessToken}`
|
|
1186
|
+
},
|
|
1187
|
+
credentials: 'include',
|
|
1188
|
+
body: JSON.stringify({
|
|
1189
|
+
platform: this.environment.platformCode,
|
|
1190
|
+
tenant_name: tenantName,
|
|
1191
|
+
country_code: countryCode,
|
|
1192
|
+
provider: 'google', // Assuming OAuth
|
|
1193
|
+
oauth_token: accessToken
|
|
1194
|
+
})
|
|
1195
|
+
});
|
|
1196
|
+
if (!response.ok) {
|
|
1197
|
+
const errorData = await response.json();
|
|
1198
|
+
throw new Error(errorData.message || 'Failed to create tenant');
|
|
1199
|
+
}
|
|
1200
|
+
const data = await response.json();
|
|
1201
|
+
// Update tokens with new tenant-scoped tokens
|
|
1202
|
+
if (data.access_token) {
|
|
1203
|
+
this.tokens.setAccessToken(data.access_token);
|
|
1204
|
+
this.signinStatus.setSigninStatus(true);
|
|
1205
|
+
}
|
|
1206
|
+
return data;
|
|
1207
|
+
}
|
|
1208
|
+
catch (error) {
|
|
1209
|
+
throw error;
|
|
1210
|
+
}
|
|
1211
|
+
}
|
|
1097
1212
|
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 });
|
|
1098
1213
|
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: AuthService, providedIn: 'root' });
|
|
1099
1214
|
}
|
|
@@ -1142,29 +1257,59 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
|
|
|
1142
1257
|
}]
|
|
1143
1258
|
}] });
|
|
1144
1259
|
|
|
1145
|
-
class
|
|
1260
|
+
class TenantLoginComponent {
|
|
1146
1261
|
auth;
|
|
1147
|
-
|
|
1148
|
-
|
|
1149
|
-
|
|
1150
|
-
|
|
1151
|
-
|
|
1262
|
+
// Component Configuration
|
|
1263
|
+
title = 'Sign In';
|
|
1264
|
+
providers = ['google'];
|
|
1265
|
+
showTenantSelector = true;
|
|
1266
|
+
autoSelectSingleTenant = true;
|
|
1267
|
+
prefillEmail; // Email to prefill (for account linking flow)
|
|
1268
|
+
allowTenantCreation = true;
|
|
1269
|
+
// Tenant Selector Labels
|
|
1270
|
+
tenantSelectorTitle = 'Select Organization';
|
|
1271
|
+
tenantSelectorDescription = 'Choose which organization you want to access:';
|
|
1272
|
+
continueButtonText = 'Continue';
|
|
1273
|
+
// Link Labels
|
|
1274
|
+
registerLinkText = "Don't have an account?";
|
|
1275
|
+
registerLinkAction = 'Sign up';
|
|
1276
|
+
createTenantLinkText = "Don't see your organization?";
|
|
1277
|
+
createTenantLinkAction = 'Create New Organization';
|
|
1278
|
+
// Outputs
|
|
1279
|
+
tenantSelected = new EventEmitter();
|
|
1280
|
+
createTenant = new EventEmitter();
|
|
1281
|
+
// Form Fields
|
|
1152
1282
|
email = '';
|
|
1153
1283
|
password = '';
|
|
1284
|
+
// State
|
|
1154
1285
|
error = '';
|
|
1155
1286
|
loading = false;
|
|
1287
|
+
showPassword = false;
|
|
1288
|
+
useOAuth = true;
|
|
1156
1289
|
oauthProviders = [];
|
|
1290
|
+
// Tenant Selection State
|
|
1291
|
+
showingTenantSelector = false;
|
|
1292
|
+
memberships = [];
|
|
1293
|
+
selectedTenantId = null;
|
|
1294
|
+
userName = '';
|
|
1157
1295
|
constructor(auth) {
|
|
1158
1296
|
this.auth = auth;
|
|
1159
1297
|
}
|
|
1160
1298
|
ngOnInit() {
|
|
1161
1299
|
if (!this.providers || this.providers.length === 0) {
|
|
1162
|
-
this.error = 'Configuration Error: No authentication providers specified.
|
|
1163
|
-
throw new Error('
|
|
1300
|
+
this.error = 'Configuration Error: No authentication providers specified.';
|
|
1301
|
+
throw new Error('TenantLoginComponent requires providers input.');
|
|
1302
|
+
}
|
|
1303
|
+
this.oauthProviders = this.providers.filter(p => p !== 'emailPassword');
|
|
1304
|
+
// If only emailPassword is available, use it by default
|
|
1305
|
+
if (this.oauthProviders.length === 0 && this.isProviderEnabled('emailPassword')) {
|
|
1306
|
+
this.useOAuth = false;
|
|
1307
|
+
}
|
|
1308
|
+
// Prefill email if provided (for account linking flow)
|
|
1309
|
+
if (this.prefillEmail) {
|
|
1310
|
+
this.email = this.prefillEmail;
|
|
1311
|
+
this.useOAuth = false; // Switch to email/password form
|
|
1164
1312
|
}
|
|
1165
|
-
// Get OAuth providers (excluding emailPassword)
|
|
1166
|
-
this.oauthProviders = this.providers
|
|
1167
|
-
.filter(p => p !== 'emailPassword');
|
|
1168
1313
|
}
|
|
1169
1314
|
isProviderEnabled(provider) {
|
|
1170
1315
|
return this.providers.includes(provider);
|
|
@@ -1181,9 +1326,13 @@ class LoginDialogComponent {
|
|
|
1181
1326
|
return labels[provider];
|
|
1182
1327
|
}
|
|
1183
1328
|
getProviderIcon(provider) {
|
|
1184
|
-
// Platforms can customize icons via CSS classes: .btn-google, .btn-linkedin, etc.
|
|
1185
1329
|
return undefined;
|
|
1186
1330
|
}
|
|
1331
|
+
toggleAuthMethod(event) {
|
|
1332
|
+
event.preventDefault();
|
|
1333
|
+
this.useOAuth = !this.useOAuth;
|
|
1334
|
+
this.error = '';
|
|
1335
|
+
}
|
|
1187
1336
|
async onEmailLogin() {
|
|
1188
1337
|
if (!this.email || !this.password) {
|
|
1189
1338
|
this.error = 'Please enter email and password';
|
|
@@ -1195,11 +1344,13 @@ class LoginDialogComponent {
|
|
|
1195
1344
|
const result = await this.auth.loginWithEmail(this.email, this.password);
|
|
1196
1345
|
if (!result.success) {
|
|
1197
1346
|
this.error = result.message || 'Login failed';
|
|
1347
|
+
return;
|
|
1198
1348
|
}
|
|
1199
|
-
//
|
|
1349
|
+
// Authentication successful, now handle tenant selection
|
|
1350
|
+
await this.handlePostAuthFlow();
|
|
1200
1351
|
}
|
|
1201
1352
|
catch (err) {
|
|
1202
|
-
this.error = 'An unexpected error occurred';
|
|
1353
|
+
this.error = err.message || 'An unexpected error occurred';
|
|
1203
1354
|
}
|
|
1204
1355
|
finally {
|
|
1205
1356
|
this.loading = false;
|
|
@@ -1212,194 +1363,499 @@ class LoginDialogComponent {
|
|
|
1212
1363
|
const result = await this.auth.loginWithProvider(provider);
|
|
1213
1364
|
if (!result.success) {
|
|
1214
1365
|
this.error = result.message || 'OAuth login failed';
|
|
1366
|
+
return;
|
|
1215
1367
|
}
|
|
1216
|
-
//
|
|
1368
|
+
// Authentication successful, now handle tenant selection
|
|
1369
|
+
await this.handlePostAuthFlow();
|
|
1217
1370
|
}
|
|
1218
1371
|
catch (err) {
|
|
1219
|
-
this.error = 'An unexpected error occurred';
|
|
1372
|
+
this.error = err.message || 'An unexpected error occurred';
|
|
1220
1373
|
}
|
|
1221
1374
|
finally {
|
|
1222
1375
|
this.loading = false;
|
|
1223
1376
|
}
|
|
1224
1377
|
}
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
class="form-control">
|
|
1247
|
-
</div>
|
|
1248
|
-
<div class="form-group">
|
|
1249
|
-
<input
|
|
1250
|
-
[(ngModel)]="password"
|
|
1251
|
-
name="password"
|
|
1252
|
-
placeholder="Password"
|
|
1253
|
-
type="password"
|
|
1254
|
-
required
|
|
1255
|
-
class="form-control">
|
|
1256
|
-
</div>
|
|
1257
|
-
<button
|
|
1258
|
-
type="submit"
|
|
1259
|
-
[disabled]="loading"
|
|
1260
|
-
class="btn btn-primary btn-block">
|
|
1261
|
-
{{ loading ? 'Signing in...' : getProviderLabel('emailPassword') }}
|
|
1262
|
-
</button>
|
|
1263
|
-
</form>
|
|
1264
|
-
}
|
|
1265
|
-
|
|
1266
|
-
<!-- Divider if both email and OAuth are present -->
|
|
1267
|
-
@if (isProviderEnabled('emailPassword') && oauthProviders.length > 0) {
|
|
1268
|
-
<div class="divider">
|
|
1269
|
-
<span>OR</span>
|
|
1270
|
-
</div>
|
|
1378
|
+
async handlePostAuthFlow() {
|
|
1379
|
+
if (!this.showTenantSelector) {
|
|
1380
|
+
// Tenant selection is disabled, emit event immediately
|
|
1381
|
+
this.tenantSelected.emit({
|
|
1382
|
+
tenantId: '',
|
|
1383
|
+
tenantSlug: '',
|
|
1384
|
+
role: ''
|
|
1385
|
+
});
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
// Fetch user's tenant memberships
|
|
1389
|
+
this.loading = true;
|
|
1390
|
+
try {
|
|
1391
|
+
const result = await this.auth.getTenantMemberships();
|
|
1392
|
+
if (!result.memberships || result.memberships.length === 0) {
|
|
1393
|
+
// User has no tenants, prompt to create one
|
|
1394
|
+
this.error = 'You are not a member of any organization. Please create one.';
|
|
1395
|
+
if (this.allowTenantCreation) {
|
|
1396
|
+
setTimeout(() => this.createTenant.emit(), 2000);
|
|
1397
|
+
}
|
|
1398
|
+
return;
|
|
1271
1399
|
}
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
<button
|
|
1278
|
-
(click)="onOAuthLogin(provider)"
|
|
1279
|
-
[disabled]="loading"
|
|
1280
|
-
class="btn btn-oauth btn-{{ provider }}">
|
|
1281
|
-
@if (getProviderIcon(provider)) {
|
|
1282
|
-
<span class="oauth-icon">
|
|
1283
|
-
{{ getProviderIcon(provider) }}
|
|
1284
|
-
</span>
|
|
1285
|
-
}
|
|
1286
|
-
{{ getProviderLabel(provider) }}
|
|
1287
|
-
</button>
|
|
1288
|
-
}
|
|
1289
|
-
</div>
|
|
1400
|
+
this.memberships = result.memberships;
|
|
1401
|
+
// Get user name if available
|
|
1402
|
+
const currentUser = this.auth.getCurrentUser();
|
|
1403
|
+
if (currentUser) {
|
|
1404
|
+
this.userName = currentUser.display_name || currentUser.email;
|
|
1290
1405
|
}
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
<div class="error-message">
|
|
1295
|
-
{{ error }}
|
|
1296
|
-
</div>
|
|
1406
|
+
// Auto-select if user has only one tenant
|
|
1407
|
+
if (this.memberships.length === 1 && this.autoSelectSingleTenant) {
|
|
1408
|
+
await this.selectAndContinue(this.memberships[0]);
|
|
1297
1409
|
}
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
<div class="loading-overlay">
|
|
1302
|
-
<div class="spinner"></div>
|
|
1303
|
-
</div>
|
|
1410
|
+
else {
|
|
1411
|
+
// Show tenant selector
|
|
1412
|
+
this.showingTenantSelector = true;
|
|
1304
1413
|
}
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1414
|
+
}
|
|
1415
|
+
catch (err) {
|
|
1416
|
+
this.error = err.message || 'Failed to load organizations';
|
|
1417
|
+
}
|
|
1418
|
+
finally {
|
|
1419
|
+
this.loading = false;
|
|
1420
|
+
}
|
|
1421
|
+
}
|
|
1422
|
+
selectTenantItem(tenantId) {
|
|
1423
|
+
this.selectedTenantId = tenantId;
|
|
1424
|
+
}
|
|
1425
|
+
async onContinueWithTenant() {
|
|
1426
|
+
if (!this.selectedTenantId) {
|
|
1427
|
+
this.error = 'Please select an organization';
|
|
1428
|
+
return;
|
|
1429
|
+
}
|
|
1430
|
+
const membership = this.memberships.find(m => m.tenant_id === this.selectedTenantId);
|
|
1431
|
+
if (!membership) {
|
|
1432
|
+
this.error = 'Selected organization not found';
|
|
1433
|
+
return;
|
|
1434
|
+
}
|
|
1435
|
+
await this.selectAndContinue(membership);
|
|
1436
|
+
}
|
|
1437
|
+
async selectAndContinue(membership) {
|
|
1438
|
+
this.loading = true;
|
|
1439
|
+
this.error = '';
|
|
1440
|
+
try {
|
|
1441
|
+
const result = await this.auth.selectTenant(membership.tenant_id);
|
|
1442
|
+
if (!result.success) {
|
|
1443
|
+
this.error = result.message || 'Failed to select organization';
|
|
1444
|
+
return;
|
|
1445
|
+
}
|
|
1446
|
+
// Emit tenant selected event
|
|
1447
|
+
this.tenantSelected.emit({
|
|
1448
|
+
tenantId: membership.tenant_id,
|
|
1449
|
+
tenantSlug: membership.slug,
|
|
1450
|
+
role: membership.role
|
|
1451
|
+
});
|
|
1452
|
+
}
|
|
1453
|
+
catch (err) {
|
|
1454
|
+
this.error = err.message || 'An unexpected error occurred';
|
|
1455
|
+
}
|
|
1456
|
+
finally {
|
|
1457
|
+
this.loading = false;
|
|
1458
|
+
}
|
|
1459
|
+
}
|
|
1460
|
+
formatRole(role) {
|
|
1461
|
+
return role.charAt(0).toUpperCase() + role.slice(1);
|
|
1462
|
+
}
|
|
1463
|
+
formatLastAccessed(dateStr) {
|
|
1464
|
+
try {
|
|
1465
|
+
const date = new Date(dateStr);
|
|
1466
|
+
const now = new Date();
|
|
1467
|
+
const diffMs = now.getTime() - date.getTime();
|
|
1468
|
+
const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
|
|
1469
|
+
if (diffDays === 0)
|
|
1470
|
+
return 'today';
|
|
1471
|
+
if (diffDays === 1)
|
|
1472
|
+
return 'yesterday';
|
|
1473
|
+
if (diffDays < 7)
|
|
1474
|
+
return `${diffDays} days ago`;
|
|
1475
|
+
if (diffDays < 30)
|
|
1476
|
+
return `${Math.floor(diffDays / 7)} weeks ago`;
|
|
1477
|
+
return `${Math.floor(diffDays / 30)} months ago`;
|
|
1478
|
+
}
|
|
1479
|
+
catch {
|
|
1480
|
+
return dateStr;
|
|
1481
|
+
}
|
|
1482
|
+
}
|
|
1483
|
+
onCreateTenantClick(event) {
|
|
1484
|
+
event.preventDefault();
|
|
1485
|
+
this.createTenant.emit();
|
|
1486
|
+
}
|
|
1487
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TenantLoginComponent, deps: [{ token: AuthService }], target: i0.ɵɵFactoryTarget.Component });
|
|
1488
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.15", type: TenantLoginComponent, isStandalone: true, selector: "lib-tenant-login", inputs: { title: "title", providers: "providers", showTenantSelector: "showTenantSelector", autoSelectSingleTenant: "autoSelectSingleTenant", prefillEmail: "prefillEmail", allowTenantCreation: "allowTenantCreation", tenantSelectorTitle: "tenantSelectorTitle", tenantSelectorDescription: "tenantSelectorDescription", continueButtonText: "continueButtonText", registerLinkText: "registerLinkText", registerLinkAction: "registerLinkAction", createTenantLinkText: "createTenantLinkText", createTenantLinkAction: "createTenantLinkAction" }, outputs: { tenantSelected: "tenantSelected", createTenant: "createTenant" }, ngImport: i0, template: `
|
|
1489
|
+
<div class="tenant-login-dialog">
|
|
1490
|
+
@if (!showingTenantSelector) {
|
|
1491
|
+
<!-- Step 1: Authentication -->
|
|
1492
|
+
<h2 class="login-title">{{ title }}</h2>
|
|
1493
|
+
|
|
1494
|
+
<!-- Email/Password Form (if enabled) -->
|
|
1495
|
+
@if (isProviderEnabled('emailPassword') && !useOAuth) {
|
|
1496
|
+
<form (ngSubmit)="onEmailLogin()" class="email-form">
|
|
1497
|
+
<div class="form-group">
|
|
1498
|
+
<input
|
|
1499
|
+
[(ngModel)]="email"
|
|
1500
|
+
name="email"
|
|
1501
|
+
placeholder="Email"
|
|
1502
|
+
type="email"
|
|
1503
|
+
required
|
|
1504
|
+
class="form-control">
|
|
1505
|
+
</div>
|
|
1506
|
+
<div class="form-group password-group">
|
|
1507
|
+
<input
|
|
1508
|
+
[(ngModel)]="password"
|
|
1509
|
+
name="password"
|
|
1510
|
+
placeholder="Password"
|
|
1511
|
+
[type]="showPassword ? 'text' : 'password'"
|
|
1512
|
+
required
|
|
1513
|
+
class="form-control password-input">
|
|
1514
|
+
<button
|
|
1515
|
+
type="button"
|
|
1516
|
+
class="password-toggle"
|
|
1517
|
+
(click)="showPassword = !showPassword"
|
|
1518
|
+
[attr.aria-label]="showPassword ? 'Hide password' : 'Show password'">
|
|
1519
|
+
{{ showPassword ? '👁️' : '👁️🗨️' }}
|
|
1520
|
+
</button>
|
|
1521
|
+
</div>
|
|
1522
|
+
<button
|
|
1523
|
+
type="submit"
|
|
1524
|
+
[disabled]="loading"
|
|
1525
|
+
class="btn btn-primary btn-block">
|
|
1526
|
+
{{ loading ? 'Signing in...' : 'Sign in with Email' }}
|
|
1527
|
+
</button>
|
|
1528
|
+
</form>
|
|
1529
|
+
|
|
1530
|
+
<!-- Divider -->
|
|
1531
|
+
@if (oauthProviders.length > 0) {
|
|
1532
|
+
<div class="divider">
|
|
1533
|
+
<span>OR</span>
|
|
1534
|
+
</div>
|
|
1535
|
+
}
|
|
1536
|
+
}
|
|
1537
|
+
|
|
1538
|
+
<!-- OAuth Providers -->
|
|
1539
|
+
@if (oauthProviders.length > 0 && (useOAuth || !isProviderEnabled('emailPassword'))) {
|
|
1540
|
+
<div class="oauth-buttons">
|
|
1541
|
+
@for (provider of oauthProviders; track provider) {
|
|
1542
|
+
<button
|
|
1543
|
+
type="button"
|
|
1544
|
+
(click)="onOAuthLogin(provider)"
|
|
1545
|
+
[disabled]="loading"
|
|
1546
|
+
class="btn btn-oauth btn-{{ provider }}">
|
|
1547
|
+
@if (getProviderIcon(provider)) {
|
|
1548
|
+
<span class="oauth-icon">
|
|
1549
|
+
{{ getProviderIcon(provider) }}
|
|
1550
|
+
</span>
|
|
1551
|
+
}
|
|
1552
|
+
{{ getProviderLabel(provider) }}
|
|
1553
|
+
</button>
|
|
1554
|
+
}
|
|
1331
1555
|
</div>
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1556
|
+
|
|
1557
|
+
<!-- Switch to Email/Password -->
|
|
1558
|
+
@if (isProviderEnabled('emailPassword') && oauthProviders.length > 0) {
|
|
1559
|
+
<div class="switch-method">
|
|
1560
|
+
<a href="#" (click)="toggleAuthMethod($event)">
|
|
1561
|
+
{{ useOAuth ? 'Use email/password instead' : 'Use OAuth instead' }}
|
|
1562
|
+
</a>
|
|
1563
|
+
</div>
|
|
1564
|
+
}
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
<!-- Error Message -->
|
|
1568
|
+
@if (error) {
|
|
1569
|
+
<div class="error-message">
|
|
1570
|
+
{{ error }}
|
|
1340
1571
|
</div>
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1572
|
+
}
|
|
1573
|
+
|
|
1574
|
+
<!-- Register Link -->
|
|
1575
|
+
@if (allowTenantCreation) {
|
|
1576
|
+
<div class="register-link">
|
|
1577
|
+
{{ registerLinkText }}
|
|
1578
|
+
<a href="#" (click)="onCreateTenantClick($event)">{{ registerLinkAction }}</a>
|
|
1579
|
+
</div>
|
|
1580
|
+
}
|
|
1581
|
+
} @else {
|
|
1582
|
+
<!-- Step 2: Tenant Selection -->
|
|
1583
|
+
<h2 class="login-title">{{ tenantSelectorTitle }}</h2>
|
|
1584
|
+
|
|
1585
|
+
@if (userName) {
|
|
1586
|
+
<div class="welcome-message">
|
|
1587
|
+
Welcome back, <strong>{{ userName }}</strong>!
|
|
1588
|
+
</div>
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
<p class="selector-description">{{ tenantSelectorDescription }}</p>
|
|
1592
|
+
|
|
1593
|
+
<div class="tenant-list">
|
|
1594
|
+
@for (membership of memberships; track membership.tenant_id) {
|
|
1595
|
+
<div
|
|
1596
|
+
class="tenant-item"
|
|
1597
|
+
[class.selected]="selectedTenantId === membership.tenant_id"
|
|
1598
|
+
(click)="selectTenantItem(membership.tenant_id)">
|
|
1599
|
+
<div class="tenant-radio">
|
|
1600
|
+
<input
|
|
1601
|
+
type="radio"
|
|
1602
|
+
[checked]="selectedTenantId === membership.tenant_id"
|
|
1603
|
+
[name]="'tenant-' + membership.tenant_id"
|
|
1604
|
+
[id]="'tenant-' + membership.tenant_id">
|
|
1605
|
+
</div>
|
|
1606
|
+
<div class="tenant-info">
|
|
1607
|
+
<div class="tenant-name">{{ membership.name }}</div>
|
|
1608
|
+
<div class="tenant-meta">
|
|
1609
|
+
<span class="tenant-role">{{ formatRole(membership.role) }}</span>
|
|
1610
|
+
@if (membership.last_accessed) {
|
|
1611
|
+
<span class="tenant-separator">·</span>
|
|
1612
|
+
<span class="tenant-last-accessed">
|
|
1613
|
+
Last accessed {{ formatLastAccessed(membership.last_accessed) }}
|
|
1614
|
+
</span>
|
|
1615
|
+
}
|
|
1616
|
+
</div>
|
|
1617
|
+
</div>
|
|
1618
|
+
</div>
|
|
1619
|
+
}
|
|
1620
|
+
</div>
|
|
1621
|
+
|
|
1622
|
+
<button
|
|
1623
|
+
type="button"
|
|
1624
|
+
(click)="onContinueWithTenant()"
|
|
1625
|
+
[disabled]="!selectedTenantId || loading"
|
|
1626
|
+
class="btn btn-primary btn-block">
|
|
1627
|
+
{{ loading ? 'Loading...' : continueButtonText }}
|
|
1628
|
+
</button>
|
|
1629
|
+
|
|
1630
|
+
<!-- Error Message -->
|
|
1631
|
+
@if (error) {
|
|
1632
|
+
<div class="error-message">
|
|
1633
|
+
{{ error }}
|
|
1634
|
+
</div>
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1637
|
+
<!-- Create New Tenant Link -->
|
|
1638
|
+
@if (allowTenantCreation) {
|
|
1639
|
+
<div class="create-tenant-link">
|
|
1640
|
+
{{ createTenantLinkText }}
|
|
1641
|
+
<a href="#" (click)="onCreateTenantClick($event)">{{ createTenantLinkAction }}</a>
|
|
1642
|
+
</div>
|
|
1643
|
+
}
|
|
1348
1644
|
}
|
|
1349
1645
|
|
|
1350
|
-
<!--
|
|
1351
|
-
@if (
|
|
1352
|
-
<div class="
|
|
1353
|
-
<
|
|
1646
|
+
<!-- Loading Overlay -->
|
|
1647
|
+
@if (loading) {
|
|
1648
|
+
<div class="loading-overlay">
|
|
1649
|
+
<div class="spinner"></div>
|
|
1354
1650
|
</div>
|
|
1355
1651
|
}
|
|
1652
|
+
</div>
|
|
1653
|
+
`, isInline: true, styles: [".tenant-login-dialog{padding:24px;max-width:450px;position:relative}.login-title{margin:0 0 24px;font-size:24px;font-weight:500;text-align:center}.welcome-message{margin-bottom:16px;padding:12px;background:#e8f5e9;border-radius:4px;text-align:center;font-size:14px;color:#2e7d32}.selector-description{margin-bottom:20px;font-size:14px;color:#666;text-align:center}.email-form,.form-group{margin-bottom:16px}.password-group{position:relative}.form-control{width:100%;padding:12px;border:1px solid #ddd;border-radius:4px;font-size:14px;box-sizing:border-box}.password-input{padding-right:45px}.password-toggle{position:absolute;right:8px;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;font-size:18px;padding:8px;line-height:1;opacity:.6;transition:opacity .2s}.password-toggle:hover{opacity:1}.password-toggle:focus{outline:2px solid #4285f4;outline-offset:2px;border-radius:4px}.form-control:focus{outline:none;border-color:#4285f4}.btn{padding:12px 24px;border:none;border-radius:4px;font-size:14px;font-weight:500;cursor:pointer;transition:background-color .2s}.btn:disabled{opacity:.6;cursor:not-allowed}.btn-block{width:100%}.btn-primary{background-color:#4285f4;color:#fff}.btn-primary:hover:not(:disabled){background-color:#357ae8}.divider{margin:16px 0;text-align:center;position:relative}.divider:before{content:\"\";position:absolute;top:50%;left:0;right:0;height:1px;background:#ddd}.divider span{background:#fff;padding:0 12px;position:relative;color:#666;font-size:12px}.oauth-buttons{display:flex;flex-direction:column;gap:12px}.btn-oauth{width:100%;background:#fff;color:#333;border:1px solid #ddd;display:flex;align-items:center;justify-content:center;gap:8px}.btn-oauth:hover:not(:disabled){background:#f8f8f8}.btn-google{border-color:#4285f4}.btn-linkedin{border-color:#0077b5}.btn-apple{border-color:#000}.btn-microsoft{border-color:#00a4ef}.btn-github{border-color:#333}.oauth-icon{font-size:18px}.switch-method{margin-top:12px;text-align:center;font-size:14px}.switch-method a{color:#4285f4;text-decoration:none}.switch-method a:hover{text-decoration:underline}.tenant-list{margin-bottom:20px;display:flex;flex-direction:column;gap:12px}.tenant-item{display:flex;align-items:flex-start;gap:12px;padding:16px;border:2px solid #e0e0e0;border-radius:6px;cursor:pointer;transition:all .2s}.tenant-item:hover{border-color:#4285f4;background:#f8f9ff}.tenant-item.selected{border-color:#4285f4;background:#e8f0fe}.tenant-radio{flex-shrink:0;padding-top:2px}.tenant-radio input[type=radio]{width:18px;height:18px;cursor:pointer}.tenant-info{flex:1}.tenant-name{font-size:16px;font-weight:500;color:#333;margin-bottom:4px}.tenant-meta{font-size:13px;color:#666}.tenant-role{font-weight:500;color:#4285f4}.tenant-separator{margin:0 6px}.error-message{margin-top:16px;padding:12px;background:#fee;color:#c33;border-radius:4px;font-size:14px}.loading-overlay{position:absolute;inset:0;background:#fffc;display:flex;align-items:center;justify-content:center}.spinner{width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #4285f4;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.register-link{margin-top:16px;text-align:center;font-size:14px;color:#666}.register-link a{color:#4285f4;text-decoration:none}.register-link a:hover{text-decoration:underline}.create-tenant-link{margin-top:16px;padding-top:16px;border-top:1px solid #e0e0e0;text-align:center;font-size:14px;color:#666}.create-tenant-link a{color:#4285f4;text-decoration:none;font-weight:500}.create-tenant-link a:hover{text-decoration:underline}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i2.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }] });
|
|
1654
|
+
}
|
|
1655
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TenantLoginComponent, decorators: [{
|
|
1656
|
+
type: Component,
|
|
1657
|
+
args: [{ selector: 'lib-tenant-login', standalone: true, imports: [CommonModule, FormsModule], template: `
|
|
1658
|
+
<div class="tenant-login-dialog">
|
|
1659
|
+
@if (!showingTenantSelector) {
|
|
1660
|
+
<!-- Step 1: Authentication -->
|
|
1661
|
+
<h2 class="login-title">{{ title }}</h2>
|
|
1356
1662
|
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1663
|
+
<!-- Email/Password Form (if enabled) -->
|
|
1664
|
+
@if (isProviderEnabled('emailPassword') && !useOAuth) {
|
|
1665
|
+
<form (ngSubmit)="onEmailLogin()" class="email-form">
|
|
1666
|
+
<div class="form-group">
|
|
1667
|
+
<input
|
|
1668
|
+
[(ngModel)]="email"
|
|
1669
|
+
name="email"
|
|
1670
|
+
placeholder="Email"
|
|
1671
|
+
type="email"
|
|
1672
|
+
required
|
|
1673
|
+
class="form-control">
|
|
1674
|
+
</div>
|
|
1675
|
+
<div class="form-group password-group">
|
|
1676
|
+
<input
|
|
1677
|
+
[(ngModel)]="password"
|
|
1678
|
+
name="password"
|
|
1679
|
+
placeholder="Password"
|
|
1680
|
+
[type]="showPassword ? 'text' : 'password'"
|
|
1681
|
+
required
|
|
1682
|
+
class="form-control password-input">
|
|
1683
|
+
<button
|
|
1684
|
+
type="button"
|
|
1685
|
+
class="password-toggle"
|
|
1686
|
+
(click)="showPassword = !showPassword"
|
|
1687
|
+
[attr.aria-label]="showPassword ? 'Hide password' : 'Show password'">
|
|
1688
|
+
{{ showPassword ? '👁️' : '👁️🗨️' }}
|
|
1689
|
+
</button>
|
|
1690
|
+
</div>
|
|
1361
1691
|
<button
|
|
1362
|
-
|
|
1692
|
+
type="submit"
|
|
1363
1693
|
[disabled]="loading"
|
|
1364
|
-
class="btn btn-
|
|
1365
|
-
|
|
1366
|
-
<span class="oauth-icon">
|
|
1367
|
-
{{ getProviderIcon(provider) }}
|
|
1368
|
-
</span>
|
|
1369
|
-
}
|
|
1370
|
-
{{ getProviderLabel(provider) }}
|
|
1694
|
+
class="btn btn-primary btn-block">
|
|
1695
|
+
{{ loading ? 'Signing in...' : 'Sign in with Email' }}
|
|
1371
1696
|
</button>
|
|
1697
|
+
</form>
|
|
1698
|
+
|
|
1699
|
+
<!-- Divider -->
|
|
1700
|
+
@if (oauthProviders.length > 0) {
|
|
1701
|
+
<div class="divider">
|
|
1702
|
+
<span>OR</span>
|
|
1703
|
+
</div>
|
|
1372
1704
|
}
|
|
1373
|
-
|
|
1374
|
-
}
|
|
1705
|
+
}
|
|
1375
1706
|
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1707
|
+
<!-- OAuth Providers -->
|
|
1708
|
+
@if (oauthProviders.length > 0 && (useOAuth || !isProviderEnabled('emailPassword'))) {
|
|
1709
|
+
<div class="oauth-buttons">
|
|
1710
|
+
@for (provider of oauthProviders; track provider) {
|
|
1711
|
+
<button
|
|
1712
|
+
type="button"
|
|
1713
|
+
(click)="onOAuthLogin(provider)"
|
|
1714
|
+
[disabled]="loading"
|
|
1715
|
+
class="btn btn-oauth btn-{{ provider }}">
|
|
1716
|
+
@if (getProviderIcon(provider)) {
|
|
1717
|
+
<span class="oauth-icon">
|
|
1718
|
+
{{ getProviderIcon(provider) }}
|
|
1719
|
+
</span>
|
|
1720
|
+
}
|
|
1721
|
+
{{ getProviderLabel(provider) }}
|
|
1722
|
+
</button>
|
|
1723
|
+
}
|
|
1724
|
+
</div>
|
|
1725
|
+
|
|
1726
|
+
<!-- Switch to Email/Password -->
|
|
1727
|
+
@if (isProviderEnabled('emailPassword') && oauthProviders.length > 0) {
|
|
1728
|
+
<div class="switch-method">
|
|
1729
|
+
<a href="#" (click)="toggleAuthMethod($event)">
|
|
1730
|
+
{{ useOAuth ? 'Use email/password instead' : 'Use OAuth instead' }}
|
|
1731
|
+
</a>
|
|
1732
|
+
</div>
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
|
|
1736
|
+
<!-- Error Message -->
|
|
1737
|
+
@if (error) {
|
|
1738
|
+
<div class="error-message">
|
|
1739
|
+
{{ error }}
|
|
1740
|
+
</div>
|
|
1741
|
+
}
|
|
1742
|
+
|
|
1743
|
+
<!-- Register Link -->
|
|
1744
|
+
@if (allowTenantCreation) {
|
|
1745
|
+
<div class="register-link">
|
|
1746
|
+
{{ registerLinkText }}
|
|
1747
|
+
<a href="#" (click)="onCreateTenantClick($event)">{{ registerLinkAction }}</a>
|
|
1748
|
+
</div>
|
|
1749
|
+
}
|
|
1750
|
+
} @else {
|
|
1751
|
+
<!-- Step 2: Tenant Selection -->
|
|
1752
|
+
<h2 class="login-title">{{ tenantSelectorTitle }}</h2>
|
|
1753
|
+
|
|
1754
|
+
@if (userName) {
|
|
1755
|
+
<div class="welcome-message">
|
|
1756
|
+
Welcome back, <strong>{{ userName }}</strong>!
|
|
1757
|
+
</div>
|
|
1758
|
+
}
|
|
1759
|
+
|
|
1760
|
+
<p class="selector-description">{{ tenantSelectorDescription }}</p>
|
|
1761
|
+
|
|
1762
|
+
<div class="tenant-list">
|
|
1763
|
+
@for (membership of memberships; track membership.tenant_id) {
|
|
1764
|
+
<div
|
|
1765
|
+
class="tenant-item"
|
|
1766
|
+
[class.selected]="selectedTenantId === membership.tenant_id"
|
|
1767
|
+
(click)="selectTenantItem(membership.tenant_id)">
|
|
1768
|
+
<div class="tenant-radio">
|
|
1769
|
+
<input
|
|
1770
|
+
type="radio"
|
|
1771
|
+
[checked]="selectedTenantId === membership.tenant_id"
|
|
1772
|
+
[name]="'tenant-' + membership.tenant_id"
|
|
1773
|
+
[id]="'tenant-' + membership.tenant_id">
|
|
1774
|
+
</div>
|
|
1775
|
+
<div class="tenant-info">
|
|
1776
|
+
<div class="tenant-name">{{ membership.name }}</div>
|
|
1777
|
+
<div class="tenant-meta">
|
|
1778
|
+
<span class="tenant-role">{{ formatRole(membership.role) }}</span>
|
|
1779
|
+
@if (membership.last_accessed) {
|
|
1780
|
+
<span class="tenant-separator">·</span>
|
|
1781
|
+
<span class="tenant-last-accessed">
|
|
1782
|
+
Last accessed {{ formatLastAccessed(membership.last_accessed) }}
|
|
1783
|
+
</span>
|
|
1784
|
+
}
|
|
1785
|
+
</div>
|
|
1786
|
+
</div>
|
|
1787
|
+
</div>
|
|
1788
|
+
}
|
|
1380
1789
|
</div>
|
|
1790
|
+
|
|
1791
|
+
<button
|
|
1792
|
+
type="button"
|
|
1793
|
+
(click)="onContinueWithTenant()"
|
|
1794
|
+
[disabled]="!selectedTenantId || loading"
|
|
1795
|
+
class="btn btn-primary btn-block">
|
|
1796
|
+
{{ loading ? 'Loading...' : continueButtonText }}
|
|
1797
|
+
</button>
|
|
1798
|
+
|
|
1799
|
+
<!-- Error Message -->
|
|
1800
|
+
@if (error) {
|
|
1801
|
+
<div class="error-message">
|
|
1802
|
+
{{ error }}
|
|
1803
|
+
</div>
|
|
1804
|
+
}
|
|
1805
|
+
|
|
1806
|
+
<!-- Create New Tenant Link -->
|
|
1807
|
+
@if (allowTenantCreation) {
|
|
1808
|
+
<div class="create-tenant-link">
|
|
1809
|
+
{{ createTenantLinkText }}
|
|
1810
|
+
<a href="#" (click)="onCreateTenantClick($event)">{{ createTenantLinkAction }}</a>
|
|
1811
|
+
</div>
|
|
1812
|
+
}
|
|
1381
1813
|
}
|
|
1382
1814
|
|
|
1383
|
-
<!-- Loading
|
|
1815
|
+
<!-- Loading Overlay -->
|
|
1384
1816
|
@if (loading) {
|
|
1385
1817
|
<div class="loading-overlay">
|
|
1386
1818
|
<div class="spinner"></div>
|
|
1387
1819
|
</div>
|
|
1388
1820
|
}
|
|
1389
|
-
|
|
1390
|
-
<!-- Register Link -->
|
|
1391
|
-
<div class="register-link">
|
|
1392
|
-
Don't have an account?
|
|
1393
|
-
<a href="#" (click)="onRegisterClick($event)">Sign up</a>
|
|
1394
|
-
</div>
|
|
1395
1821
|
</div>
|
|
1396
|
-
`, styles: [".login-dialog{padding:24px;max-width:
|
|
1397
|
-
}], ctorParameters: () => [{ type: AuthService }], propDecorators: {
|
|
1822
|
+
`, styles: [".tenant-login-dialog{padding:24px;max-width:450px;position:relative}.login-title{margin:0 0 24px;font-size:24px;font-weight:500;text-align:center}.welcome-message{margin-bottom:16px;padding:12px;background:#e8f5e9;border-radius:4px;text-align:center;font-size:14px;color:#2e7d32}.selector-description{margin-bottom:20px;font-size:14px;color:#666;text-align:center}.email-form,.form-group{margin-bottom:16px}.password-group{position:relative}.form-control{width:100%;padding:12px;border:1px solid #ddd;border-radius:4px;font-size:14px;box-sizing:border-box}.password-input{padding-right:45px}.password-toggle{position:absolute;right:8px;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;font-size:18px;padding:8px;line-height:1;opacity:.6;transition:opacity .2s}.password-toggle:hover{opacity:1}.password-toggle:focus{outline:2px solid #4285f4;outline-offset:2px;border-radius:4px}.form-control:focus{outline:none;border-color:#4285f4}.btn{padding:12px 24px;border:none;border-radius:4px;font-size:14px;font-weight:500;cursor:pointer;transition:background-color .2s}.btn:disabled{opacity:.6;cursor:not-allowed}.btn-block{width:100%}.btn-primary{background-color:#4285f4;color:#fff}.btn-primary:hover:not(:disabled){background-color:#357ae8}.divider{margin:16px 0;text-align:center;position:relative}.divider:before{content:\"\";position:absolute;top:50%;left:0;right:0;height:1px;background:#ddd}.divider span{background:#fff;padding:0 12px;position:relative;color:#666;font-size:12px}.oauth-buttons{display:flex;flex-direction:column;gap:12px}.btn-oauth{width:100%;background:#fff;color:#333;border:1px solid #ddd;display:flex;align-items:center;justify-content:center;gap:8px}.btn-oauth:hover:not(:disabled){background:#f8f8f8}.btn-google{border-color:#4285f4}.btn-linkedin{border-color:#0077b5}.btn-apple{border-color:#000}.btn-microsoft{border-color:#00a4ef}.btn-github{border-color:#333}.oauth-icon{font-size:18px}.switch-method{margin-top:12px;text-align:center;font-size:14px}.switch-method a{color:#4285f4;text-decoration:none}.switch-method a:hover{text-decoration:underline}.tenant-list{margin-bottom:20px;display:flex;flex-direction:column;gap:12px}.tenant-item{display:flex;align-items:flex-start;gap:12px;padding:16px;border:2px solid #e0e0e0;border-radius:6px;cursor:pointer;transition:all .2s}.tenant-item:hover{border-color:#4285f4;background:#f8f9ff}.tenant-item.selected{border-color:#4285f4;background:#e8f0fe}.tenant-radio{flex-shrink:0;padding-top:2px}.tenant-radio input[type=radio]{width:18px;height:18px;cursor:pointer}.tenant-info{flex:1}.tenant-name{font-size:16px;font-weight:500;color:#333;margin-bottom:4px}.tenant-meta{font-size:13px;color:#666}.tenant-role{font-weight:500;color:#4285f4}.tenant-separator{margin:0 6px}.error-message{margin-top:16px;padding:12px;background:#fee;color:#c33;border-radius:4px;font-size:14px}.loading-overlay{position:absolute;inset:0;background:#fffc;display:flex;align-items:center;justify-content:center}.spinner{width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #4285f4;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.register-link{margin-top:16px;text-align:center;font-size:14px;color:#666}.register-link a{color:#4285f4;text-decoration:none}.register-link a:hover{text-decoration:underline}.create-tenant-link{margin-top:16px;padding-top:16px;border-top:1px solid #e0e0e0;text-align:center;font-size:14px;color:#666}.create-tenant-link a{color:#4285f4;text-decoration:none;font-weight:500}.create-tenant-link a:hover{text-decoration:underline}\n"] }]
|
|
1823
|
+
}], ctorParameters: () => [{ type: AuthService }], propDecorators: { title: [{
|
|
1824
|
+
type: Input
|
|
1825
|
+
}], providers: [{
|
|
1826
|
+
type: Input
|
|
1827
|
+
}], showTenantSelector: [{
|
|
1828
|
+
type: Input
|
|
1829
|
+
}], autoSelectSingleTenant: [{
|
|
1830
|
+
type: Input
|
|
1831
|
+
}], prefillEmail: [{
|
|
1832
|
+
type: Input
|
|
1833
|
+
}], allowTenantCreation: [{
|
|
1834
|
+
type: Input
|
|
1835
|
+
}], tenantSelectorTitle: [{
|
|
1836
|
+
type: Input
|
|
1837
|
+
}], tenantSelectorDescription: [{
|
|
1838
|
+
type: Input
|
|
1839
|
+
}], continueButtonText: [{
|
|
1840
|
+
type: Input
|
|
1841
|
+
}], registerLinkText: [{
|
|
1842
|
+
type: Input
|
|
1843
|
+
}], registerLinkAction: [{
|
|
1844
|
+
type: Input
|
|
1845
|
+
}], createTenantLinkText: [{
|
|
1846
|
+
type: Input
|
|
1847
|
+
}], createTenantLinkAction: [{
|
|
1398
1848
|
type: Input
|
|
1849
|
+
}], tenantSelected: [{
|
|
1850
|
+
type: Output
|
|
1851
|
+
}], createTenant: [{
|
|
1852
|
+
type: Output
|
|
1399
1853
|
}] } });
|
|
1400
1854
|
|
|
1401
1855
|
class RegisterComponent {
|
|
1402
1856
|
auth;
|
|
1857
|
+
environment;
|
|
1858
|
+
navigateToLogin = new EventEmitter();
|
|
1403
1859
|
displayName = '';
|
|
1404
1860
|
email = '';
|
|
1405
1861
|
password = '';
|
|
@@ -1407,8 +1863,13 @@ class RegisterComponent {
|
|
|
1407
1863
|
error = '';
|
|
1408
1864
|
success = '';
|
|
1409
1865
|
loading = false;
|
|
1410
|
-
|
|
1866
|
+
showAccountLinkPrompt = false;
|
|
1867
|
+
existingEmail = '';
|
|
1868
|
+
showPassword = false;
|
|
1869
|
+
showConfirmPassword = false;
|
|
1870
|
+
constructor(auth, environment) {
|
|
1411
1871
|
this.auth = auth;
|
|
1872
|
+
this.environment = environment;
|
|
1412
1873
|
}
|
|
1413
1874
|
async onRegister() {
|
|
1414
1875
|
// Reset messages
|
|
@@ -1435,14 +1896,40 @@ class RegisterComponent {
|
|
|
1435
1896
|
}
|
|
1436
1897
|
this.loading = true;
|
|
1437
1898
|
try {
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1899
|
+
// Direct API call to check for email already registered
|
|
1900
|
+
const response = await fetch(`${this.environment.accountsUrl}/api/auth/register`, {
|
|
1901
|
+
method: 'POST',
|
|
1902
|
+
headers: { 'Content-Type': 'application/json' },
|
|
1903
|
+
credentials: 'include',
|
|
1904
|
+
body: JSON.stringify({
|
|
1905
|
+
email: this.email,
|
|
1906
|
+
password: this.password,
|
|
1907
|
+
display_name: this.displayName,
|
|
1908
|
+
platform: this.environment.platformCode
|
|
1909
|
+
})
|
|
1910
|
+
});
|
|
1911
|
+
const data = await response.json();
|
|
1912
|
+
if (response.ok && data.identity_id) {
|
|
1913
|
+
// Registration successful - now login
|
|
1914
|
+
const loginResult = await this.auth.loginWithEmail(this.email, this.password);
|
|
1915
|
+
if (loginResult.success) {
|
|
1916
|
+
this.success = 'Account created successfully!';
|
|
1917
|
+
}
|
|
1918
|
+
else {
|
|
1919
|
+
this.success = 'Account created! Please sign in.';
|
|
1920
|
+
}
|
|
1443
1921
|
}
|
|
1444
1922
|
else {
|
|
1445
|
-
|
|
1923
|
+
// Check if email already registered
|
|
1924
|
+
if (data.error === 'Email already registered' || data.details?.includes('Email already registered')) {
|
|
1925
|
+
this.existingEmail = this.email;
|
|
1926
|
+
this.showAccountLinkPrompt = true;
|
|
1927
|
+
this.error = '';
|
|
1928
|
+
}
|
|
1929
|
+
else {
|
|
1930
|
+
// Other errors
|
|
1931
|
+
this.error = data.error || data.details || 'Registration failed';
|
|
1932
|
+
}
|
|
1446
1933
|
}
|
|
1447
1934
|
}
|
|
1448
1935
|
catch (err) {
|
|
@@ -1454,16 +1941,50 @@ class RegisterComponent {
|
|
|
1454
1941
|
}
|
|
1455
1942
|
onLoginClick(event) {
|
|
1456
1943
|
event.preventDefault();
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1944
|
+
this.navigateToLogin.emit('');
|
|
1945
|
+
}
|
|
1946
|
+
linkExistingAccount() {
|
|
1947
|
+
// User confirmed they want to link their existing account
|
|
1948
|
+
this.navigateToLogin.emit(this.existingEmail);
|
|
1949
|
+
}
|
|
1950
|
+
cancelLinking() {
|
|
1951
|
+
// User decided not to link - reset form
|
|
1952
|
+
this.showAccountLinkPrompt = false;
|
|
1953
|
+
this.existingEmail = '';
|
|
1954
|
+
this.email = '';
|
|
1955
|
+
this.password = '';
|
|
1956
|
+
this.confirmPassword = '';
|
|
1957
|
+
this.displayName = '';
|
|
1958
|
+
}
|
|
1959
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: RegisterComponent, deps: [{ token: AuthService }, { token: MyEnvironmentModel }], target: i0.ɵɵFactoryTarget.Component });
|
|
1960
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.15", type: RegisterComponent, isStandalone: true, selector: "lib-register", outputs: { navigateToLogin: "navigateToLogin" }, ngImport: i0, template: `
|
|
1463
1961
|
<div class="register-dialog">
|
|
1464
1962
|
<h2 class="register-title">Create Account</h2>
|
|
1465
1963
|
|
|
1466
|
-
|
|
1964
|
+
<!-- Account Link Prompt -->
|
|
1965
|
+
@if (showAccountLinkPrompt) {
|
|
1966
|
+
<div class="account-link-prompt">
|
|
1967
|
+
<div class="prompt-icon">🔗</div>
|
|
1968
|
+
<h3>Account Already Exists</h3>
|
|
1969
|
+
<p>
|
|
1970
|
+
You already have an account with <strong>{{ existingEmail }}</strong>,
|
|
1971
|
+
used on another ProGalaxy E-Labs platform.
|
|
1972
|
+
</p>
|
|
1973
|
+
<p>
|
|
1974
|
+
Would you like to use the same account to access this platform?
|
|
1975
|
+
</p>
|
|
1976
|
+
<div class="prompt-actions">
|
|
1977
|
+
<button type="button" class="btn btn-primary btn-block" (click)="linkExistingAccount()">
|
|
1978
|
+
Yes, Use My Existing Account
|
|
1979
|
+
</button>
|
|
1980
|
+
<button type="button" class="btn btn-secondary btn-block" (click)="cancelLinking()">
|
|
1981
|
+
No, Use Different Email
|
|
1982
|
+
</button>
|
|
1983
|
+
</div>
|
|
1984
|
+
</div>
|
|
1985
|
+
}
|
|
1986
|
+
|
|
1987
|
+
<form *ngIf="!showAccountLinkPrompt" (ngSubmit)="onRegister()" class="register-form">
|
|
1467
1988
|
<div class="form-group">
|
|
1468
1989
|
<label for="displayName">Full Name</label>
|
|
1469
1990
|
<input
|
|
@@ -1488,30 +2009,44 @@ class RegisterComponent {
|
|
|
1488
2009
|
class="form-control">
|
|
1489
2010
|
</div>
|
|
1490
2011
|
|
|
1491
|
-
<div class="form-group">
|
|
2012
|
+
<div class="form-group password-group">
|
|
1492
2013
|
<label for="password">Password</label>
|
|
1493
2014
|
<input
|
|
1494
2015
|
id="password"
|
|
1495
2016
|
[(ngModel)]="password"
|
|
1496
2017
|
name="password"
|
|
1497
2018
|
placeholder="Create a password"
|
|
1498
|
-
type="password"
|
|
2019
|
+
[type]="showPassword ? 'text' : 'password'"
|
|
1499
2020
|
required
|
|
1500
2021
|
minlength="8"
|
|
1501
|
-
class="form-control">
|
|
2022
|
+
class="form-control password-input">
|
|
2023
|
+
<button
|
|
2024
|
+
type="button"
|
|
2025
|
+
class="password-toggle"
|
|
2026
|
+
(click)="showPassword = !showPassword"
|
|
2027
|
+
[attr.aria-label]="showPassword ? 'Hide password' : 'Show password'">
|
|
2028
|
+
{{ showPassword ? '👁️' : '👁️🗨️' }}
|
|
2029
|
+
</button>
|
|
1502
2030
|
<small class="form-hint">At least 8 characters</small>
|
|
1503
2031
|
</div>
|
|
1504
2032
|
|
|
1505
|
-
<div class="form-group">
|
|
2033
|
+
<div class="form-group password-group">
|
|
1506
2034
|
<label for="confirmPassword">Confirm Password</label>
|
|
1507
2035
|
<input
|
|
1508
2036
|
id="confirmPassword"
|
|
1509
2037
|
[(ngModel)]="confirmPassword"
|
|
1510
2038
|
name="confirmPassword"
|
|
1511
2039
|
placeholder="Confirm your password"
|
|
1512
|
-
type="password"
|
|
2040
|
+
[type]="showConfirmPassword ? 'text' : 'password'"
|
|
1513
2041
|
required
|
|
1514
|
-
class="form-control">
|
|
2042
|
+
class="form-control password-input">
|
|
2043
|
+
<button
|
|
2044
|
+
type="button"
|
|
2045
|
+
class="password-toggle"
|
|
2046
|
+
(click)="showConfirmPassword = !showConfirmPassword"
|
|
2047
|
+
[attr.aria-label]="showConfirmPassword ? 'Hide password' : 'Show password'">
|
|
2048
|
+
{{ showConfirmPassword ? '👁️' : '👁️🗨️' }}
|
|
2049
|
+
</button>
|
|
1515
2050
|
</div>
|
|
1516
2051
|
|
|
1517
2052
|
<button
|
|
@@ -1523,7 +2058,7 @@ class RegisterComponent {
|
|
|
1523
2058
|
</form>
|
|
1524
2059
|
|
|
1525
2060
|
<!-- Error Message -->
|
|
1526
|
-
@if (error) {
|
|
2061
|
+
@if (error && !showAccountLinkPrompt) {
|
|
1527
2062
|
<div class="error-message">
|
|
1528
2063
|
{{ error }}
|
|
1529
2064
|
</div>
|
|
@@ -1544,12 +2079,12 @@ class RegisterComponent {
|
|
|
1544
2079
|
}
|
|
1545
2080
|
|
|
1546
2081
|
<!-- Login Link -->
|
|
1547
|
-
<div class="login-link">
|
|
2082
|
+
<div *ngIf="!showAccountLinkPrompt" class="login-link">
|
|
1548
2083
|
Already have an account?
|
|
1549
2084
|
<a href="#" (click)="onLoginClick($event)">Sign in</a>
|
|
1550
2085
|
</div>
|
|
1551
2086
|
</div>
|
|
1552
|
-
`, isInline: true, styles: [".register-dialog{padding:24px;max-width:400px;position:relative}.register-title{margin:0 0 24px;font-size:24px;font-weight:500;text-align:center}.register-form,.form-group{margin-bottom:16px}.form-group label{display:block;margin-bottom:6px;font-size:14px;font-weight:500;color:#333}.form-control{width:100%;padding:12px;border:1px solid #ddd;border-radius:4px;font-size:14px;box-sizing:border-box}.form-control:focus{outline:none;border-color:#4285f4}.form-hint{display:block;margin-top:4px;font-size:12px;color:#666}.btn{padding:12px 24px;border:none;border-radius:4px;font-size:14px;font-weight:500;cursor:pointer;transition:background-color .2s}.btn:disabled{opacity:.6;cursor:not-allowed}.btn-block{width:100%}.btn-primary{background-color:#4285f4;color:#fff}.btn-primary:hover:not(:disabled){background-color:#357ae8}.error-message{margin-top:16px;padding:12px;background:#fee;color:#c33;border-radius:4px;font-size:14px}.success-message{margin-top:16px;padding:12px;background:#efe;color:#3a3;border-radius:4px;font-size:14px}.loading-overlay{position:absolute;inset:0;background:#fffc;display:flex;align-items:center;justify-content:center}.spinner{width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #4285f4;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.login-link{margin-top:16px;text-align:center;font-size:14px;color:#666}.login-link a{color:#4285f4;text-decoration:none}.login-link a:hover{text-decoration:underline}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i2.MinLengthValidator, selector: "[minlength][formControlName],[minlength][formControl],[minlength][ngModel]", inputs: ["minlength"] }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i2.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }] });
|
|
2087
|
+
`, isInline: true, styles: [".register-dialog{padding:24px;max-width:400px;position:relative}.register-title{margin:0 0 24px;font-size:24px;font-weight:500;text-align:center}.register-form,.form-group{margin-bottom:16px}.password-group{position:relative}.form-group label{display:block;margin-bottom:6px;font-size:14px;font-weight:500;color:#333}.form-control{width:100%;padding:12px;border:1px solid #ddd;border-radius:4px;font-size:14px;box-sizing:border-box}.password-input{padding-right:45px}.password-toggle{position:absolute;right:8px;top:38px;background:none;border:none;cursor:pointer;font-size:18px;padding:8px;line-height:1;opacity:.6;transition:opacity .2s}.password-toggle:hover{opacity:1}.password-toggle:focus{outline:2px solid #4285f4;outline-offset:2px;border-radius:4px}.form-control:focus{outline:none;border-color:#4285f4}.form-hint{display:block;margin-top:4px;font-size:12px;color:#666}.btn{padding:12px 24px;border:none;border-radius:4px;font-size:14px;font-weight:500;cursor:pointer;transition:background-color .2s}.btn:disabled{opacity:.6;cursor:not-allowed}.btn-block{width:100%}.btn-primary{background-color:#4285f4;color:#fff}.btn-primary:hover:not(:disabled){background-color:#357ae8}.error-message{margin-top:16px;padding:12px;background:#fee;color:#c33;border-radius:4px;font-size:14px}.success-message{margin-top:16px;padding:12px;background:#efe;color:#3a3;border-radius:4px;font-size:14px}.loading-overlay{position:absolute;inset:0;background:#fffc;display:flex;align-items:center;justify-content:center}.spinner{width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #4285f4;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.login-link{margin-top:16px;text-align:center;font-size:14px;color:#666}.login-link a{color:#4285f4;text-decoration:none}.login-link a:hover{text-decoration:underline}.account-link-prompt{background:#f8f9fa;border:2px solid #4285f4;border-radius:8px;padding:24px;margin-bottom:16px;text-align:center}.prompt-icon{font-size:48px;margin-bottom:12px}.account-link-prompt h3{margin:0 0 12px;color:#333;font-size:20px;font-weight:500}.account-link-prompt p{margin:8px 0;color:#555;font-size:14px;line-height:1.6}.prompt-actions{margin-top:20px;display:flex;flex-direction:column;gap:10px}.btn-secondary{background:#fff;color:#333;border:1px solid #ddd}.btn-secondary:hover:not(:disabled){background:#f8f9fa;border-color:#ccc}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i2$1.NgIf, selector: "[ngIf]", inputs: ["ngIf", "ngIfThen", "ngIfElse"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i2.MinLengthValidator, selector: "[minlength][formControlName],[minlength][formControl],[minlength][ngModel]", inputs: ["minlength"] }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i2.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }] });
|
|
1553
2088
|
}
|
|
1554
2089
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: RegisterComponent, decorators: [{
|
|
1555
2090
|
type: Component,
|
|
@@ -1557,7 +2092,30 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
|
|
|
1557
2092
|
<div class="register-dialog">
|
|
1558
2093
|
<h2 class="register-title">Create Account</h2>
|
|
1559
2094
|
|
|
1560
|
-
|
|
2095
|
+
<!-- Account Link Prompt -->
|
|
2096
|
+
@if (showAccountLinkPrompt) {
|
|
2097
|
+
<div class="account-link-prompt">
|
|
2098
|
+
<div class="prompt-icon">🔗</div>
|
|
2099
|
+
<h3>Account Already Exists</h3>
|
|
2100
|
+
<p>
|
|
2101
|
+
You already have an account with <strong>{{ existingEmail }}</strong>,
|
|
2102
|
+
used on another ProGalaxy E-Labs platform.
|
|
2103
|
+
</p>
|
|
2104
|
+
<p>
|
|
2105
|
+
Would you like to use the same account to access this platform?
|
|
2106
|
+
</p>
|
|
2107
|
+
<div class="prompt-actions">
|
|
2108
|
+
<button type="button" class="btn btn-primary btn-block" (click)="linkExistingAccount()">
|
|
2109
|
+
Yes, Use My Existing Account
|
|
2110
|
+
</button>
|
|
2111
|
+
<button type="button" class="btn btn-secondary btn-block" (click)="cancelLinking()">
|
|
2112
|
+
No, Use Different Email
|
|
2113
|
+
</button>
|
|
2114
|
+
</div>
|
|
2115
|
+
</div>
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
<form *ngIf="!showAccountLinkPrompt" (ngSubmit)="onRegister()" class="register-form">
|
|
1561
2119
|
<div class="form-group">
|
|
1562
2120
|
<label for="displayName">Full Name</label>
|
|
1563
2121
|
<input
|
|
@@ -1582,30 +2140,44 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
|
|
|
1582
2140
|
class="form-control">
|
|
1583
2141
|
</div>
|
|
1584
2142
|
|
|
1585
|
-
<div class="form-group">
|
|
2143
|
+
<div class="form-group password-group">
|
|
1586
2144
|
<label for="password">Password</label>
|
|
1587
2145
|
<input
|
|
1588
2146
|
id="password"
|
|
1589
2147
|
[(ngModel)]="password"
|
|
1590
2148
|
name="password"
|
|
1591
2149
|
placeholder="Create a password"
|
|
1592
|
-
type="password"
|
|
2150
|
+
[type]="showPassword ? 'text' : 'password'"
|
|
1593
2151
|
required
|
|
1594
2152
|
minlength="8"
|
|
1595
|
-
class="form-control">
|
|
2153
|
+
class="form-control password-input">
|
|
2154
|
+
<button
|
|
2155
|
+
type="button"
|
|
2156
|
+
class="password-toggle"
|
|
2157
|
+
(click)="showPassword = !showPassword"
|
|
2158
|
+
[attr.aria-label]="showPassword ? 'Hide password' : 'Show password'">
|
|
2159
|
+
{{ showPassword ? '👁️' : '👁️🗨️' }}
|
|
2160
|
+
</button>
|
|
1596
2161
|
<small class="form-hint">At least 8 characters</small>
|
|
1597
2162
|
</div>
|
|
1598
2163
|
|
|
1599
|
-
<div class="form-group">
|
|
2164
|
+
<div class="form-group password-group">
|
|
1600
2165
|
<label for="confirmPassword">Confirm Password</label>
|
|
1601
2166
|
<input
|
|
1602
2167
|
id="confirmPassword"
|
|
1603
2168
|
[(ngModel)]="confirmPassword"
|
|
1604
2169
|
name="confirmPassword"
|
|
1605
2170
|
placeholder="Confirm your password"
|
|
1606
|
-
type="password"
|
|
2171
|
+
[type]="showConfirmPassword ? 'text' : 'password'"
|
|
1607
2172
|
required
|
|
1608
|
-
class="form-control">
|
|
2173
|
+
class="form-control password-input">
|
|
2174
|
+
<button
|
|
2175
|
+
type="button"
|
|
2176
|
+
class="password-toggle"
|
|
2177
|
+
(click)="showConfirmPassword = !showConfirmPassword"
|
|
2178
|
+
[attr.aria-label]="showConfirmPassword ? 'Hide password' : 'Show password'">
|
|
2179
|
+
{{ showConfirmPassword ? '👁️' : '👁️🗨️' }}
|
|
2180
|
+
</button>
|
|
1609
2181
|
</div>
|
|
1610
2182
|
|
|
1611
2183
|
<button
|
|
@@ -1617,7 +2189,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
|
|
|
1617
2189
|
</form>
|
|
1618
2190
|
|
|
1619
2191
|
<!-- Error Message -->
|
|
1620
|
-
@if (error) {
|
|
2192
|
+
@if (error && !showAccountLinkPrompt) {
|
|
1621
2193
|
<div class="error-message">
|
|
1622
2194
|
{{ error }}
|
|
1623
2195
|
</div>
|
|
@@ -1638,60 +2210,157 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
|
|
|
1638
2210
|
}
|
|
1639
2211
|
|
|
1640
2212
|
<!-- Login Link -->
|
|
1641
|
-
<div class="login-link">
|
|
2213
|
+
<div *ngIf="!showAccountLinkPrompt" class="login-link">
|
|
1642
2214
|
Already have an account?
|
|
1643
2215
|
<a href="#" (click)="onLoginClick($event)">Sign in</a>
|
|
1644
2216
|
</div>
|
|
1645
2217
|
</div>
|
|
1646
|
-
`, 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"] }]
|
|
1647
|
-
}], ctorParameters: () => [{ type: AuthService }
|
|
2218
|
+
`, styles: [".register-dialog{padding:24px;max-width:400px;position:relative}.register-title{margin:0 0 24px;font-size:24px;font-weight:500;text-align:center}.register-form,.form-group{margin-bottom:16px}.password-group{position:relative}.form-group label{display:block;margin-bottom:6px;font-size:14px;font-weight:500;color:#333}.form-control{width:100%;padding:12px;border:1px solid #ddd;border-radius:4px;font-size:14px;box-sizing:border-box}.password-input{padding-right:45px}.password-toggle{position:absolute;right:8px;top:38px;background:none;border:none;cursor:pointer;font-size:18px;padding:8px;line-height:1;opacity:.6;transition:opacity .2s}.password-toggle:hover{opacity:1}.password-toggle:focus{outline:2px solid #4285f4;outline-offset:2px;border-radius:4px}.form-control:focus{outline:none;border-color:#4285f4}.form-hint{display:block;margin-top:4px;font-size:12px;color:#666}.btn{padding:12px 24px;border:none;border-radius:4px;font-size:14px;font-weight:500;cursor:pointer;transition:background-color .2s}.btn:disabled{opacity:.6;cursor:not-allowed}.btn-block{width:100%}.btn-primary{background-color:#4285f4;color:#fff}.btn-primary:hover:not(:disabled){background-color:#357ae8}.error-message{margin-top:16px;padding:12px;background:#fee;color:#c33;border-radius:4px;font-size:14px}.success-message{margin-top:16px;padding:12px;background:#efe;color:#3a3;border-radius:4px;font-size:14px}.loading-overlay{position:absolute;inset:0;background:#fffc;display:flex;align-items:center;justify-content:center}.spinner{width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #4285f4;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.login-link{margin-top:16px;text-align:center;font-size:14px;color:#666}.login-link a{color:#4285f4;text-decoration:none}.login-link a:hover{text-decoration:underline}.account-link-prompt{background:#f8f9fa;border:2px solid #4285f4;border-radius:8px;padding:24px;margin-bottom:16px;text-align:center}.prompt-icon{font-size:48px;margin-bottom:12px}.account-link-prompt h3{margin:0 0 12px;color:#333;font-size:20px;font-weight:500}.account-link-prompt p{margin:8px 0;color:#555;font-size:14px;line-height:1.6}.prompt-actions{margin-top:20px;display:flex;flex-direction:column;gap:10px}.btn-secondary{background:#fff;color:#333;border:1px solid #ddd}.btn-secondary:hover:not(:disabled){background:#f8f9fa;border-color:#ccc}\n"] }]
|
|
2219
|
+
}], ctorParameters: () => [{ type: AuthService }, { type: MyEnvironmentModel, decorators: [{
|
|
2220
|
+
type: Inject,
|
|
2221
|
+
args: [MyEnvironmentModel]
|
|
2222
|
+
}] }], propDecorators: { navigateToLogin: [{
|
|
2223
|
+
type: Output
|
|
2224
|
+
}] } });
|
|
1648
2225
|
|
|
1649
|
-
class
|
|
2226
|
+
class AuthPageComponent {
|
|
2227
|
+
environment;
|
|
2228
|
+
providers = ['google', 'emailPassword'];
|
|
2229
|
+
authenticated = new EventEmitter();
|
|
2230
|
+
mode = 'login';
|
|
2231
|
+
appName = '';
|
|
2232
|
+
logo;
|
|
2233
|
+
subtitle;
|
|
2234
|
+
gradientStyle = 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)';
|
|
2235
|
+
constructor(environment) {
|
|
2236
|
+
this.environment = environment;
|
|
2237
|
+
}
|
|
2238
|
+
ngOnInit() {
|
|
2239
|
+
const branding = this.environment.branding;
|
|
2240
|
+
if (branding) {
|
|
2241
|
+
this.appName = branding.appName || 'Sign In';
|
|
2242
|
+
this.logo = branding.logo;
|
|
2243
|
+
this.subtitle = branding.subtitle;
|
|
2244
|
+
if (branding.gradientStart && branding.gradientEnd) {
|
|
2245
|
+
this.gradientStyle = `linear-gradient(135deg, ${branding.gradientStart} 0%, ${branding.gradientEnd} 100%)`;
|
|
2246
|
+
}
|
|
2247
|
+
else if (branding.primaryColor) {
|
|
2248
|
+
const color = branding.primaryColor;
|
|
2249
|
+
this.gradientStyle = `linear-gradient(135deg, ${color} 0%, ${this.adjustColor(color, -20)} 100%)`;
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
else {
|
|
2253
|
+
this.appName = 'Sign In';
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
onAuthenticated(event) {
|
|
2257
|
+
this.authenticated.emit(event);
|
|
2258
|
+
}
|
|
2259
|
+
/**
|
|
2260
|
+
* Adjust color brightness (simple implementation)
|
|
2261
|
+
* @param color Hex color (e.g., '#667eea')
|
|
2262
|
+
* @param percent Percentage to darken (negative) or lighten (positive)
|
|
2263
|
+
*/
|
|
2264
|
+
adjustColor(color, percent) {
|
|
2265
|
+
const num = parseInt(color.replace('#', ''), 16);
|
|
2266
|
+
const amt = Math.round(2.55 * percent);
|
|
2267
|
+
const R = (num >> 16) + amt;
|
|
2268
|
+
const G = (num >> 8 & 0x00FF) + amt;
|
|
2269
|
+
const B = (num & 0x0000FF) + amt;
|
|
2270
|
+
return '#' + (0x1000000 + (R < 255 ? R < 1 ? 0 : R : 255) * 0x10000 +
|
|
2271
|
+
(G < 255 ? G < 1 ? 0 : G : 255) * 0x100 +
|
|
2272
|
+
(B < 255 ? B < 1 ? 0 : B : 255))
|
|
2273
|
+
.toString(16).slice(1);
|
|
2274
|
+
}
|
|
2275
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: AuthPageComponent, deps: [{ token: MyEnvironmentModel }], target: i0.ɵɵFactoryTarget.Component });
|
|
2276
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.15", type: AuthPageComponent, isStandalone: true, selector: "lib-auth-page", inputs: { providers: "providers" }, outputs: { authenticated: "authenticated" }, ngImport: i0, template: `
|
|
2277
|
+
<div class="auth-container" [style.background]="gradientStyle">
|
|
2278
|
+
<div class="auth-card">
|
|
2279
|
+
@if (logo) {
|
|
2280
|
+
<img [src]="logo" [alt]="appName + ' logo'" class="logo">
|
|
2281
|
+
}
|
|
2282
|
+
<h1 class="app-name">{{ appName }}</h1>
|
|
2283
|
+
@if (subtitle) {
|
|
2284
|
+
<p class="subtitle">{{ subtitle }}</p>
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
@if (mode === 'login') {
|
|
2288
|
+
<lib-tenant-login
|
|
2289
|
+
[providers]="providers"
|
|
2290
|
+
[allowTenantCreation]="false"
|
|
2291
|
+
(tenantSelected)="onAuthenticated($event)"
|
|
2292
|
+
(createTenant)="mode = 'register'">
|
|
2293
|
+
</lib-tenant-login>
|
|
2294
|
+
} @else {
|
|
2295
|
+
<lib-register
|
|
2296
|
+
(navigateToLogin)="mode = 'login'">
|
|
2297
|
+
</lib-register>
|
|
2298
|
+
}
|
|
2299
|
+
</div>
|
|
2300
|
+
</div>
|
|
2301
|
+
`, isInline: true, styles: [".auth-container{min-height:100vh;display:flex;align-items:center;justify-content:center;padding:20px;background:linear-gradient(135deg,#667eea,#764ba2)}.auth-card{background:#fff;border-radius:12px;box-shadow:0 10px 40px #0000001a;padding:40px;width:100%;max-width:480px}.logo{display:block;max-width:200px;max-height:80px;margin:0 auto 24px}.app-name{margin:0 0 12px;font-size:28px;font-weight:600;text-align:center;color:#1a202c}.subtitle{margin:0 0 32px;font-size:16px;text-align:center;color:#718096}:host ::ng-deep .tenant-login-dialog,:host ::ng-deep .register-dialog{padding:0;max-width:none}:host ::ng-deep .login-title,:host ::ng-deep .register-title{display:none}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: TenantLoginComponent, selector: "lib-tenant-login", inputs: ["title", "providers", "showTenantSelector", "autoSelectSingleTenant", "prefillEmail", "allowTenantCreation", "tenantSelectorTitle", "tenantSelectorDescription", "continueButtonText", "registerLinkText", "registerLinkAction", "createTenantLinkText", "createTenantLinkAction"], outputs: ["tenantSelected", "createTenant"] }, { kind: "component", type: RegisterComponent, selector: "lib-register", outputs: ["navigateToLogin"] }] });
|
|
2302
|
+
}
|
|
2303
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: AuthPageComponent, decorators: [{
|
|
2304
|
+
type: Component,
|
|
2305
|
+
args: [{ selector: 'lib-auth-page', standalone: true, imports: [CommonModule, TenantLoginComponent, RegisterComponent], template: `
|
|
2306
|
+
<div class="auth-container" [style.background]="gradientStyle">
|
|
2307
|
+
<div class="auth-card">
|
|
2308
|
+
@if (logo) {
|
|
2309
|
+
<img [src]="logo" [alt]="appName + ' logo'" class="logo">
|
|
2310
|
+
}
|
|
2311
|
+
<h1 class="app-name">{{ appName }}</h1>
|
|
2312
|
+
@if (subtitle) {
|
|
2313
|
+
<p class="subtitle">{{ subtitle }}</p>
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
@if (mode === 'login') {
|
|
2317
|
+
<lib-tenant-login
|
|
2318
|
+
[providers]="providers"
|
|
2319
|
+
[allowTenantCreation]="false"
|
|
2320
|
+
(tenantSelected)="onAuthenticated($event)"
|
|
2321
|
+
(createTenant)="mode = 'register'">
|
|
2322
|
+
</lib-tenant-login>
|
|
2323
|
+
} @else {
|
|
2324
|
+
<lib-register
|
|
2325
|
+
(navigateToLogin)="mode = 'login'">
|
|
2326
|
+
</lib-register>
|
|
2327
|
+
}
|
|
2328
|
+
</div>
|
|
2329
|
+
</div>
|
|
2330
|
+
`, styles: [".auth-container{min-height:100vh;display:flex;align-items:center;justify-content:center;padding:20px;background:linear-gradient(135deg,#667eea,#764ba2)}.auth-card{background:#fff;border-radius:12px;box-shadow:0 10px 40px #0000001a;padding:40px;width:100%;max-width:480px}.logo{display:block;max-width:200px;max-height:80px;margin:0 auto 24px}.app-name{margin:0 0 12px;font-size:28px;font-weight:600;text-align:center;color:#1a202c}.subtitle{margin:0 0 32px;font-size:16px;text-align:center;color:#718096}:host ::ng-deep .tenant-login-dialog,:host ::ng-deep .register-dialog{padding:0;max-width:none}:host ::ng-deep .login-title,:host ::ng-deep .register-title{display:none}\n"] }]
|
|
2331
|
+
}], ctorParameters: () => [{ type: MyEnvironmentModel, decorators: [{
|
|
2332
|
+
type: Inject,
|
|
2333
|
+
args: [MyEnvironmentModel]
|
|
2334
|
+
}] }], propDecorators: { providers: [{
|
|
2335
|
+
type: Input
|
|
2336
|
+
}], authenticated: [{
|
|
2337
|
+
type: Output
|
|
2338
|
+
}] } });
|
|
2339
|
+
|
|
2340
|
+
class LoginDialogComponent {
|
|
1650
2341
|
auth;
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
allowTenantCreation = true;
|
|
1657
|
-
// Tenant Selector Labels
|
|
1658
|
-
tenantSelectorTitle = 'Select Organization';
|
|
1659
|
-
tenantSelectorDescription = 'Choose which organization you want to access:';
|
|
1660
|
-
continueButtonText = 'Continue';
|
|
1661
|
-
// Link Labels
|
|
1662
|
-
registerLinkText = "Don't have an account?";
|
|
1663
|
-
registerLinkAction = 'Sign up';
|
|
1664
|
-
createTenantLinkText = "Don't see your organization?";
|
|
1665
|
-
createTenantLinkAction = 'Create New Organization';
|
|
1666
|
-
// Outputs
|
|
1667
|
-
tenantSelected = new EventEmitter();
|
|
1668
|
-
createTenant = new EventEmitter();
|
|
1669
|
-
// Form Fields
|
|
2342
|
+
/**
|
|
2343
|
+
* REQUIRED: Which authentication providers to show in this dialog
|
|
2344
|
+
* @example ['google', 'linkedin', 'emailPassword']
|
|
2345
|
+
*/
|
|
2346
|
+
providers = [];
|
|
1670
2347
|
email = '';
|
|
1671
2348
|
password = '';
|
|
1672
|
-
// State
|
|
1673
2349
|
error = '';
|
|
1674
2350
|
loading = false;
|
|
1675
|
-
|
|
2351
|
+
showPassword = false;
|
|
1676
2352
|
oauthProviders = [];
|
|
1677
|
-
// Tenant Selection State
|
|
1678
|
-
showingTenantSelector = false;
|
|
1679
|
-
memberships = [];
|
|
1680
|
-
selectedTenantId = null;
|
|
1681
|
-
userName = '';
|
|
1682
2353
|
constructor(auth) {
|
|
1683
2354
|
this.auth = auth;
|
|
1684
2355
|
}
|
|
1685
2356
|
ngOnInit() {
|
|
1686
2357
|
if (!this.providers || this.providers.length === 0) {
|
|
1687
|
-
this.error = 'Configuration Error: No authentication providers specified.';
|
|
1688
|
-
throw new Error('
|
|
1689
|
-
}
|
|
1690
|
-
this.oauthProviders = this.providers.filter(p => p !== 'emailPassword');
|
|
1691
|
-
// If only emailPassword is available, use it by default
|
|
1692
|
-
if (this.oauthProviders.length === 0 && this.isProviderEnabled('emailPassword')) {
|
|
1693
|
-
this.useOAuth = false;
|
|
2358
|
+
this.error = 'Configuration Error: No authentication providers specified. Please pass providers to LoginDialogComponent.';
|
|
2359
|
+
throw new Error('LoginDialogComponent requires providers input. Example: dialogRef.componentInstance.providers = [\'google\', \'emailPassword\']');
|
|
1694
2360
|
}
|
|
2361
|
+
// Get OAuth providers (excluding emailPassword)
|
|
2362
|
+
this.oauthProviders = this.providers
|
|
2363
|
+
.filter(p => p !== 'emailPassword');
|
|
1695
2364
|
}
|
|
1696
2365
|
isProviderEnabled(provider) {
|
|
1697
2366
|
return this.providers.includes(provider);
|
|
@@ -1708,514 +2377,235 @@ class TenantLoginComponent {
|
|
|
1708
2377
|
return labels[provider];
|
|
1709
2378
|
}
|
|
1710
2379
|
getProviderIcon(provider) {
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
this.
|
|
1716
|
-
|
|
1717
|
-
}
|
|
1718
|
-
async onEmailLogin() {
|
|
1719
|
-
if (!this.email || !this.password) {
|
|
1720
|
-
this.error = 'Please enter email and password';
|
|
1721
|
-
return;
|
|
1722
|
-
}
|
|
1723
|
-
this.loading = true;
|
|
1724
|
-
this.error = '';
|
|
1725
|
-
try {
|
|
1726
|
-
const result = await this.auth.loginWithEmail(this.email, this.password);
|
|
1727
|
-
if (!result.success) {
|
|
1728
|
-
this.error = result.message || 'Login failed';
|
|
1729
|
-
return;
|
|
1730
|
-
}
|
|
1731
|
-
// Authentication successful, now handle tenant selection
|
|
1732
|
-
await this.handlePostAuthFlow();
|
|
1733
|
-
}
|
|
1734
|
-
catch (err) {
|
|
1735
|
-
this.error = err.message || 'An unexpected error occurred';
|
|
1736
|
-
}
|
|
1737
|
-
finally {
|
|
1738
|
-
this.loading = false;
|
|
1739
|
-
}
|
|
1740
|
-
}
|
|
1741
|
-
async onOAuthLogin(provider) {
|
|
1742
|
-
this.loading = true;
|
|
1743
|
-
this.error = '';
|
|
1744
|
-
try {
|
|
1745
|
-
const result = await this.auth.loginWithProvider(provider);
|
|
1746
|
-
if (!result.success) {
|
|
1747
|
-
this.error = result.message || 'OAuth login failed';
|
|
1748
|
-
return;
|
|
1749
|
-
}
|
|
1750
|
-
// Authentication successful, now handle tenant selection
|
|
1751
|
-
await this.handlePostAuthFlow();
|
|
1752
|
-
}
|
|
1753
|
-
catch (err) {
|
|
1754
|
-
this.error = err.message || 'An unexpected error occurred';
|
|
1755
|
-
}
|
|
1756
|
-
finally {
|
|
1757
|
-
this.loading = false;
|
|
1758
|
-
}
|
|
1759
|
-
}
|
|
1760
|
-
async handlePostAuthFlow() {
|
|
1761
|
-
if (!this.showTenantSelector) {
|
|
1762
|
-
// Tenant selection is disabled, emit event immediately
|
|
1763
|
-
this.tenantSelected.emit({
|
|
1764
|
-
tenantId: '',
|
|
1765
|
-
tenantSlug: '',
|
|
1766
|
-
role: ''
|
|
1767
|
-
});
|
|
1768
|
-
return;
|
|
1769
|
-
}
|
|
1770
|
-
// Fetch user's tenant memberships
|
|
1771
|
-
this.loading = true;
|
|
1772
|
-
try {
|
|
1773
|
-
const result = await this.auth.getTenantMemberships();
|
|
1774
|
-
if (!result.memberships || result.memberships.length === 0) {
|
|
1775
|
-
// User has no tenants, prompt to create one
|
|
1776
|
-
this.error = 'You are not a member of any organization. Please create one.';
|
|
1777
|
-
if (this.allowTenantCreation) {
|
|
1778
|
-
setTimeout(() => this.createTenant.emit(), 2000);
|
|
1779
|
-
}
|
|
1780
|
-
return;
|
|
1781
|
-
}
|
|
1782
|
-
this.memberships = result.memberships;
|
|
1783
|
-
// Get user name if available
|
|
1784
|
-
const currentUser = this.auth.getCurrentUser();
|
|
1785
|
-
if (currentUser) {
|
|
1786
|
-
this.userName = currentUser.display_name || currentUser.email;
|
|
1787
|
-
}
|
|
1788
|
-
// Auto-select if user has only one tenant
|
|
1789
|
-
if (this.memberships.length === 1 && this.autoSelectSingleTenant) {
|
|
1790
|
-
await this.selectAndContinue(this.memberships[0]);
|
|
1791
|
-
}
|
|
1792
|
-
else {
|
|
1793
|
-
// Show tenant selector
|
|
1794
|
-
this.showingTenantSelector = true;
|
|
1795
|
-
}
|
|
1796
|
-
}
|
|
1797
|
-
catch (err) {
|
|
1798
|
-
this.error = err.message || 'Failed to load organizations';
|
|
1799
|
-
}
|
|
1800
|
-
finally {
|
|
1801
|
-
this.loading = false;
|
|
1802
|
-
}
|
|
1803
|
-
}
|
|
1804
|
-
selectTenantItem(tenantId) {
|
|
1805
|
-
this.selectedTenantId = tenantId;
|
|
1806
|
-
}
|
|
1807
|
-
async onContinueWithTenant() {
|
|
1808
|
-
if (!this.selectedTenantId) {
|
|
1809
|
-
this.error = 'Please select an organization';
|
|
1810
|
-
return;
|
|
1811
|
-
}
|
|
1812
|
-
const membership = this.memberships.find(m => m.tenant_id === this.selectedTenantId);
|
|
1813
|
-
if (!membership) {
|
|
1814
|
-
this.error = 'Selected organization not found';
|
|
2380
|
+
// Platforms can customize icons via CSS classes: .btn-google, .btn-linkedin, etc.
|
|
2381
|
+
return undefined;
|
|
2382
|
+
}
|
|
2383
|
+
async onEmailLogin() {
|
|
2384
|
+
if (!this.email || !this.password) {
|
|
2385
|
+
this.error = 'Please enter email and password';
|
|
1815
2386
|
return;
|
|
1816
2387
|
}
|
|
1817
|
-
await this.selectAndContinue(membership);
|
|
1818
|
-
}
|
|
1819
|
-
async selectAndContinue(membership) {
|
|
1820
2388
|
this.loading = true;
|
|
1821
2389
|
this.error = '';
|
|
1822
2390
|
try {
|
|
1823
|
-
const result = await this.auth.
|
|
2391
|
+
const result = await this.auth.loginWithEmail(this.email, this.password);
|
|
1824
2392
|
if (!result.success) {
|
|
1825
|
-
this.error = result.message || '
|
|
1826
|
-
return;
|
|
2393
|
+
this.error = result.message || 'Login failed';
|
|
1827
2394
|
}
|
|
1828
|
-
//
|
|
1829
|
-
this.tenantSelected.emit({
|
|
1830
|
-
tenantId: membership.tenant_id,
|
|
1831
|
-
tenantSlug: membership.slug,
|
|
1832
|
-
role: membership.role
|
|
1833
|
-
});
|
|
2395
|
+
// On success, parent component/dialog should close automatically via user$ subscription
|
|
1834
2396
|
}
|
|
1835
2397
|
catch (err) {
|
|
1836
|
-
this.error =
|
|
2398
|
+
this.error = 'An unexpected error occurred';
|
|
1837
2399
|
}
|
|
1838
2400
|
finally {
|
|
1839
2401
|
this.loading = false;
|
|
1840
2402
|
}
|
|
1841
2403
|
}
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
1845
|
-
formatLastAccessed(dateStr) {
|
|
2404
|
+
async onOAuthLogin(provider) {
|
|
2405
|
+
this.loading = true;
|
|
2406
|
+
this.error = '';
|
|
1846
2407
|
try {
|
|
1847
|
-
const
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
return 'today';
|
|
1853
|
-
if (diffDays === 1)
|
|
1854
|
-
return 'yesterday';
|
|
1855
|
-
if (diffDays < 7)
|
|
1856
|
-
return `${diffDays} days ago`;
|
|
1857
|
-
if (diffDays < 30)
|
|
1858
|
-
return `${Math.floor(diffDays / 7)} weeks ago`;
|
|
1859
|
-
return `${Math.floor(diffDays / 30)} months ago`;
|
|
2408
|
+
const result = await this.auth.loginWithProvider(provider);
|
|
2409
|
+
if (!result.success) {
|
|
2410
|
+
this.error = result.message || 'OAuth login failed';
|
|
2411
|
+
}
|
|
2412
|
+
// On success, parent component/dialog should close automatically via user$ subscription
|
|
1860
2413
|
}
|
|
1861
|
-
catch {
|
|
1862
|
-
|
|
2414
|
+
catch (err) {
|
|
2415
|
+
this.error = 'An unexpected error occurred';
|
|
2416
|
+
}
|
|
2417
|
+
finally {
|
|
2418
|
+
this.loading = false;
|
|
1863
2419
|
}
|
|
1864
2420
|
}
|
|
1865
|
-
|
|
2421
|
+
onRegisterClick(event) {
|
|
1866
2422
|
event.preventDefault();
|
|
1867
|
-
this
|
|
2423
|
+
// Platforms can override this or listen for a custom event
|
|
2424
|
+
// For now, just emit a console message
|
|
2425
|
+
console.log('Register clicked - platform should handle navigation');
|
|
1868
2426
|
}
|
|
1869
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type:
|
|
1870
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.15", type:
|
|
1871
|
-
<div class="
|
|
1872
|
-
|
|
1873
|
-
<!-- Step 1: Authentication -->
|
|
1874
|
-
<h2 class="login-title">{{ title }}</h2>
|
|
2427
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: LoginDialogComponent, deps: [{ token: AuthService }], target: i0.ɵɵFactoryTarget.Component });
|
|
2428
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.15", type: LoginDialogComponent, isStandalone: true, selector: "lib-login-dialog", inputs: { providers: "providers" }, ngImport: i0, template: `
|
|
2429
|
+
<div class="login-dialog">
|
|
2430
|
+
<h2 class="login-title">Sign In</h2>
|
|
1875
2431
|
|
|
1876
|
-
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
|
|
1881
|
-
|
|
1882
|
-
|
|
1883
|
-
|
|
1884
|
-
|
|
1885
|
-
|
|
1886
|
-
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
</div>
|
|
2432
|
+
<!-- Email/Password Form (if enabled) -->
|
|
2433
|
+
@if (isProviderEnabled('emailPassword')) {
|
|
2434
|
+
<form (ngSubmit)="onEmailLogin()" class="email-form">
|
|
2435
|
+
<div class="form-group">
|
|
2436
|
+
<input
|
|
2437
|
+
[(ngModel)]="email"
|
|
2438
|
+
name="email"
|
|
2439
|
+
placeholder="Email"
|
|
2440
|
+
type="email"
|
|
2441
|
+
required
|
|
2442
|
+
class="form-control">
|
|
2443
|
+
</div>
|
|
2444
|
+
<div class="form-group password-group">
|
|
2445
|
+
<input
|
|
2446
|
+
[(ngModel)]="password"
|
|
2447
|
+
name="password"
|
|
2448
|
+
placeholder="Password"
|
|
2449
|
+
[type]="showPassword ? 'text' : 'password'"
|
|
2450
|
+
required
|
|
2451
|
+
class="form-control password-input">
|
|
1897
2452
|
<button
|
|
1898
|
-
type="
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
2453
|
+
type="button"
|
|
2454
|
+
class="password-toggle"
|
|
2455
|
+
(click)="showPassword = !showPassword"
|
|
2456
|
+
[attr.aria-label]="showPassword ? 'Hide password' : 'Show password'">
|
|
2457
|
+
{{ showPassword ? '👁️' : '👁️🗨️' }}
|
|
1902
2458
|
</button>
|
|
1903
|
-
</form>
|
|
1904
|
-
|
|
1905
|
-
<!-- Divider -->
|
|
1906
|
-
@if (oauthProviders.length > 0) {
|
|
1907
|
-
<div class="divider">
|
|
1908
|
-
<span>OR</span>
|
|
1909
|
-
</div>
|
|
1910
|
-
}
|
|
1911
|
-
}
|
|
1912
|
-
|
|
1913
|
-
<!-- OAuth Providers -->
|
|
1914
|
-
@if (oauthProviders.length > 0 && (useOAuth || !isProviderEnabled('emailPassword'))) {
|
|
1915
|
-
<div class="oauth-buttons">
|
|
1916
|
-
@for (provider of oauthProviders; track provider) {
|
|
1917
|
-
<button
|
|
1918
|
-
type="button"
|
|
1919
|
-
(click)="onOAuthLogin(provider)"
|
|
1920
|
-
[disabled]="loading"
|
|
1921
|
-
class="btn btn-oauth btn-{{ provider }}">
|
|
1922
|
-
@if (getProviderIcon(provider)) {
|
|
1923
|
-
<span class="oauth-icon">
|
|
1924
|
-
{{ getProviderIcon(provider) }}
|
|
1925
|
-
</span>
|
|
1926
|
-
}
|
|
1927
|
-
{{ getProviderLabel(provider) }}
|
|
1928
|
-
</button>
|
|
1929
|
-
}
|
|
1930
|
-
</div>
|
|
1931
|
-
|
|
1932
|
-
<!-- Switch to Email/Password -->
|
|
1933
|
-
@if (isProviderEnabled('emailPassword') && oauthProviders.length > 0) {
|
|
1934
|
-
<div class="switch-method">
|
|
1935
|
-
<a href="#" (click)="toggleAuthMethod($event)">
|
|
1936
|
-
{{ useOAuth ? 'Use email/password instead' : 'Use OAuth instead' }}
|
|
1937
|
-
</a>
|
|
1938
|
-
</div>
|
|
1939
|
-
}
|
|
1940
|
-
}
|
|
1941
|
-
|
|
1942
|
-
<!-- Error Message -->
|
|
1943
|
-
@if (error) {
|
|
1944
|
-
<div class="error-message">
|
|
1945
|
-
{{ error }}
|
|
1946
|
-
</div>
|
|
1947
|
-
}
|
|
1948
|
-
|
|
1949
|
-
<!-- Register Link -->
|
|
1950
|
-
@if (allowTenantCreation) {
|
|
1951
|
-
<div class="register-link">
|
|
1952
|
-
{{ registerLinkText }}
|
|
1953
|
-
<a href="#" (click)="onCreateTenantClick($event)">{{ registerLinkAction }}</a>
|
|
1954
|
-
</div>
|
|
1955
|
-
}
|
|
1956
|
-
} @else {
|
|
1957
|
-
<!-- Step 2: Tenant Selection -->
|
|
1958
|
-
<h2 class="login-title">{{ tenantSelectorTitle }}</h2>
|
|
1959
|
-
|
|
1960
|
-
@if (userName) {
|
|
1961
|
-
<div class="welcome-message">
|
|
1962
|
-
Welcome back, <strong>{{ userName }}</strong>!
|
|
1963
2459
|
</div>
|
|
1964
|
-
|
|
2460
|
+
<button
|
|
2461
|
+
type="submit"
|
|
2462
|
+
[disabled]="loading"
|
|
2463
|
+
class="btn btn-primary btn-block">
|
|
2464
|
+
{{ loading ? 'Signing in...' : getProviderLabel('emailPassword') }}
|
|
2465
|
+
</button>
|
|
2466
|
+
</form>
|
|
2467
|
+
}
|
|
1965
2468
|
|
|
1966
|
-
|
|
2469
|
+
<!-- Divider if both email and OAuth are present -->
|
|
2470
|
+
@if (isProviderEnabled('emailPassword') && oauthProviders.length > 0) {
|
|
2471
|
+
<div class="divider">
|
|
2472
|
+
<span>OR</span>
|
|
2473
|
+
</div>
|
|
2474
|
+
}
|
|
1967
2475
|
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
(click)="
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
1977
|
-
|
|
1978
|
-
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
<div class="tenant-meta">
|
|
1984
|
-
<span class="tenant-role">{{ formatRole(membership.role) }}</span>
|
|
1985
|
-
@if (membership.last_accessed) {
|
|
1986
|
-
<span class="tenant-separator">·</span>
|
|
1987
|
-
<span class="tenant-last-accessed">
|
|
1988
|
-
Last accessed {{ formatLastAccessed(membership.last_accessed) }}
|
|
1989
|
-
</span>
|
|
1990
|
-
}
|
|
1991
|
-
</div>
|
|
1992
|
-
</div>
|
|
1993
|
-
</div>
|
|
2476
|
+
<!-- OAuth Providers -->
|
|
2477
|
+
@if (oauthProviders.length > 0) {
|
|
2478
|
+
<div class="oauth-buttons">
|
|
2479
|
+
@for (provider of oauthProviders; track provider) {
|
|
2480
|
+
<button
|
|
2481
|
+
(click)="onOAuthLogin(provider)"
|
|
2482
|
+
[disabled]="loading"
|
|
2483
|
+
class="btn btn-oauth btn-{{ provider }}">
|
|
2484
|
+
@if (getProviderIcon(provider)) {
|
|
2485
|
+
<span class="oauth-icon">
|
|
2486
|
+
{{ getProviderIcon(provider) }}
|
|
2487
|
+
</span>
|
|
2488
|
+
}
|
|
2489
|
+
{{ getProviderLabel(provider) }}
|
|
2490
|
+
</button>
|
|
1994
2491
|
}
|
|
1995
2492
|
</div>
|
|
2493
|
+
}
|
|
1996
2494
|
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
{{ loading ? 'Loading...' : continueButtonText }}
|
|
2003
|
-
</button>
|
|
2004
|
-
|
|
2005
|
-
<!-- Error Message -->
|
|
2006
|
-
@if (error) {
|
|
2007
|
-
<div class="error-message">
|
|
2008
|
-
{{ error }}
|
|
2009
|
-
</div>
|
|
2010
|
-
}
|
|
2011
|
-
|
|
2012
|
-
<!-- Create New Tenant Link -->
|
|
2013
|
-
@if (allowTenantCreation) {
|
|
2014
|
-
<div class="create-tenant-link">
|
|
2015
|
-
{{ createTenantLinkText }}
|
|
2016
|
-
<a href="#" (click)="onCreateTenantClick($event)">{{ createTenantLinkAction }}</a>
|
|
2017
|
-
</div>
|
|
2018
|
-
}
|
|
2495
|
+
<!-- Error Message -->
|
|
2496
|
+
@if (error) {
|
|
2497
|
+
<div class="error-message">
|
|
2498
|
+
{{ error }}
|
|
2499
|
+
</div>
|
|
2019
2500
|
}
|
|
2020
2501
|
|
|
2021
|
-
<!-- Loading
|
|
2502
|
+
<!-- Loading State -->
|
|
2022
2503
|
@if (loading) {
|
|
2023
2504
|
<div class="loading-overlay">
|
|
2024
2505
|
<div class="spinner"></div>
|
|
2025
2506
|
</div>
|
|
2026
2507
|
}
|
|
2508
|
+
|
|
2509
|
+
<!-- Register Link -->
|
|
2510
|
+
<div class="register-link">
|
|
2511
|
+
Don't have an account?
|
|
2512
|
+
<a href="#" (click)="onRegisterClick($event)">Sign up</a>
|
|
2513
|
+
</div>
|
|
2027
2514
|
</div>
|
|
2028
|
-
`, isInline: true, styles: [".
|
|
2515
|
+
`, isInline: true, styles: [".login-dialog{padding:24px;max-width:400px;position:relative}.login-title{margin:0 0 24px;font-size:24px;font-weight:500;text-align:center}.email-form,.form-group{margin-bottom:16px}.password-group{position:relative}.form-control{width:100%;padding:12px;border:1px solid #ddd;border-radius:4px;font-size:14px;box-sizing:border-box}.password-input{padding-right:45px}.password-toggle{position:absolute;right:8px;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;font-size:18px;padding:8px;line-height:1;opacity:.6;transition:opacity .2s}.password-toggle:hover{opacity:1}.password-toggle:focus{outline:2px solid #4285f4;outline-offset:2px;border-radius:4px}.form-control:focus{outline:none;border-color:#4285f4}.btn{padding:12px 24px;border:none;border-radius:4px;font-size:14px;font-weight:500;cursor:pointer;transition:background-color .2s}.btn:disabled{opacity:.6;cursor:not-allowed}.btn-block{width:100%}.btn-primary{background-color:#4285f4;color:#fff}.btn-primary:hover:not(:disabled){background-color:#357ae8}.divider{margin:16px 0;text-align:center;position:relative}.divider:before{content:\"\";position:absolute;top:50%;left:0;right:0;height:1px;background:#ddd}.divider span{background:#fff;padding:0 12px;position:relative;color:#666;font-size:12px}.oauth-buttons{display:flex;flex-direction:column;gap:12px}.btn-oauth{width:100%;background:#fff;color:#333;border:1px solid #ddd;display:flex;align-items:center;justify-content:center;gap:8px}.btn-oauth:hover:not(:disabled){background:#f8f8f8}.btn-google{border-color:#4285f4}.btn-linkedin{border-color:#0077b5}.btn-apple{border-color:#000}.btn-microsoft{border-color:#00a4ef}.btn-github{border-color:#333}.oauth-icon{font-size:18px}.error-message{margin-top:16px;padding:12px;background:#fee;color:#c33;border-radius:4px;font-size:14px}.loading-overlay{position:absolute;inset:0;background:#fffc;display:flex;align-items:center;justify-content:center}.spinner{width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #4285f4;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.register-link{margin-top:16px;text-align:center;font-size:14px;color:#666}.register-link a{color:#4285f4;text-decoration:none}.register-link a:hover{text-decoration:underline}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i2.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }] });
|
|
2029
2516
|
}
|
|
2030
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type:
|
|
2517
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: LoginDialogComponent, decorators: [{
|
|
2031
2518
|
type: Component,
|
|
2032
|
-
args: [{ selector: 'lib-
|
|
2033
|
-
<div class="
|
|
2034
|
-
|
|
2035
|
-
<!-- Step 1: Authentication -->
|
|
2036
|
-
<h2 class="login-title">{{ title }}</h2>
|
|
2519
|
+
args: [{ selector: 'lib-login-dialog', standalone: true, imports: [CommonModule, FormsModule], template: `
|
|
2520
|
+
<div class="login-dialog">
|
|
2521
|
+
<h2 class="login-title">Sign In</h2>
|
|
2037
2522
|
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2047
|
-
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
</div>
|
|
2523
|
+
<!-- Email/Password Form (if enabled) -->
|
|
2524
|
+
@if (isProviderEnabled('emailPassword')) {
|
|
2525
|
+
<form (ngSubmit)="onEmailLogin()" class="email-form">
|
|
2526
|
+
<div class="form-group">
|
|
2527
|
+
<input
|
|
2528
|
+
[(ngModel)]="email"
|
|
2529
|
+
name="email"
|
|
2530
|
+
placeholder="Email"
|
|
2531
|
+
type="email"
|
|
2532
|
+
required
|
|
2533
|
+
class="form-control">
|
|
2534
|
+
</div>
|
|
2535
|
+
<div class="form-group password-group">
|
|
2536
|
+
<input
|
|
2537
|
+
[(ngModel)]="password"
|
|
2538
|
+
name="password"
|
|
2539
|
+
placeholder="Password"
|
|
2540
|
+
[type]="showPassword ? 'text' : 'password'"
|
|
2541
|
+
required
|
|
2542
|
+
class="form-control password-input">
|
|
2059
2543
|
<button
|
|
2060
|
-
type="
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2544
|
+
type="button"
|
|
2545
|
+
class="password-toggle"
|
|
2546
|
+
(click)="showPassword = !showPassword"
|
|
2547
|
+
[attr.aria-label]="showPassword ? 'Hide password' : 'Show password'">
|
|
2548
|
+
{{ showPassword ? '👁️' : '👁️🗨️' }}
|
|
2064
2549
|
</button>
|
|
2065
|
-
</form>
|
|
2066
|
-
|
|
2067
|
-
<!-- Divider -->
|
|
2068
|
-
@if (oauthProviders.length > 0) {
|
|
2069
|
-
<div class="divider">
|
|
2070
|
-
<span>OR</span>
|
|
2071
|
-
</div>
|
|
2072
|
-
}
|
|
2073
|
-
}
|
|
2074
|
-
|
|
2075
|
-
<!-- OAuth Providers -->
|
|
2076
|
-
@if (oauthProviders.length > 0 && (useOAuth || !isProviderEnabled('emailPassword'))) {
|
|
2077
|
-
<div class="oauth-buttons">
|
|
2078
|
-
@for (provider of oauthProviders; track provider) {
|
|
2079
|
-
<button
|
|
2080
|
-
type="button"
|
|
2081
|
-
(click)="onOAuthLogin(provider)"
|
|
2082
|
-
[disabled]="loading"
|
|
2083
|
-
class="btn btn-oauth btn-{{ provider }}">
|
|
2084
|
-
@if (getProviderIcon(provider)) {
|
|
2085
|
-
<span class="oauth-icon">
|
|
2086
|
-
{{ getProviderIcon(provider) }}
|
|
2087
|
-
</span>
|
|
2088
|
-
}
|
|
2089
|
-
{{ getProviderLabel(provider) }}
|
|
2090
|
-
</button>
|
|
2091
|
-
}
|
|
2092
|
-
</div>
|
|
2093
|
-
|
|
2094
|
-
<!-- Switch to Email/Password -->
|
|
2095
|
-
@if (isProviderEnabled('emailPassword') && oauthProviders.length > 0) {
|
|
2096
|
-
<div class="switch-method">
|
|
2097
|
-
<a href="#" (click)="toggleAuthMethod($event)">
|
|
2098
|
-
{{ useOAuth ? 'Use email/password instead' : 'Use OAuth instead' }}
|
|
2099
|
-
</a>
|
|
2100
|
-
</div>
|
|
2101
|
-
}
|
|
2102
|
-
}
|
|
2103
|
-
|
|
2104
|
-
<!-- Error Message -->
|
|
2105
|
-
@if (error) {
|
|
2106
|
-
<div class="error-message">
|
|
2107
|
-
{{ error }}
|
|
2108
|
-
</div>
|
|
2109
|
-
}
|
|
2110
|
-
|
|
2111
|
-
<!-- Register Link -->
|
|
2112
|
-
@if (allowTenantCreation) {
|
|
2113
|
-
<div class="register-link">
|
|
2114
|
-
{{ registerLinkText }}
|
|
2115
|
-
<a href="#" (click)="onCreateTenantClick($event)">{{ registerLinkAction }}</a>
|
|
2116
|
-
</div>
|
|
2117
|
-
}
|
|
2118
|
-
} @else {
|
|
2119
|
-
<!-- Step 2: Tenant Selection -->
|
|
2120
|
-
<h2 class="login-title">{{ tenantSelectorTitle }}</h2>
|
|
2121
|
-
|
|
2122
|
-
@if (userName) {
|
|
2123
|
-
<div class="welcome-message">
|
|
2124
|
-
Welcome back, <strong>{{ userName }}</strong>!
|
|
2125
2550
|
</div>
|
|
2126
|
-
|
|
2551
|
+
<button
|
|
2552
|
+
type="submit"
|
|
2553
|
+
[disabled]="loading"
|
|
2554
|
+
class="btn btn-primary btn-block">
|
|
2555
|
+
{{ loading ? 'Signing in...' : getProviderLabel('emailPassword') }}
|
|
2556
|
+
</button>
|
|
2557
|
+
</form>
|
|
2558
|
+
}
|
|
2127
2559
|
|
|
2128
|
-
|
|
2560
|
+
<!-- Divider if both email and OAuth are present -->
|
|
2561
|
+
@if (isProviderEnabled('emailPassword') && oauthProviders.length > 0) {
|
|
2562
|
+
<div class="divider">
|
|
2563
|
+
<span>OR</span>
|
|
2564
|
+
</div>
|
|
2565
|
+
}
|
|
2129
2566
|
|
|
2130
|
-
|
|
2131
|
-
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
|
|
2135
|
-
(click)="
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
2143
|
-
|
|
2144
|
-
|
|
2145
|
-
<div class="tenant-meta">
|
|
2146
|
-
<span class="tenant-role">{{ formatRole(membership.role) }}</span>
|
|
2147
|
-
@if (membership.last_accessed) {
|
|
2148
|
-
<span class="tenant-separator">·</span>
|
|
2149
|
-
<span class="tenant-last-accessed">
|
|
2150
|
-
Last accessed {{ formatLastAccessed(membership.last_accessed) }}
|
|
2151
|
-
</span>
|
|
2152
|
-
}
|
|
2153
|
-
</div>
|
|
2154
|
-
</div>
|
|
2155
|
-
</div>
|
|
2567
|
+
<!-- OAuth Providers -->
|
|
2568
|
+
@if (oauthProviders.length > 0) {
|
|
2569
|
+
<div class="oauth-buttons">
|
|
2570
|
+
@for (provider of oauthProviders; track provider) {
|
|
2571
|
+
<button
|
|
2572
|
+
(click)="onOAuthLogin(provider)"
|
|
2573
|
+
[disabled]="loading"
|
|
2574
|
+
class="btn btn-oauth btn-{{ provider }}">
|
|
2575
|
+
@if (getProviderIcon(provider)) {
|
|
2576
|
+
<span class="oauth-icon">
|
|
2577
|
+
{{ getProviderIcon(provider) }}
|
|
2578
|
+
</span>
|
|
2579
|
+
}
|
|
2580
|
+
{{ getProviderLabel(provider) }}
|
|
2581
|
+
</button>
|
|
2156
2582
|
}
|
|
2157
2583
|
</div>
|
|
2584
|
+
}
|
|
2158
2585
|
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
{{ loading ? 'Loading...' : continueButtonText }}
|
|
2165
|
-
</button>
|
|
2166
|
-
|
|
2167
|
-
<!-- Error Message -->
|
|
2168
|
-
@if (error) {
|
|
2169
|
-
<div class="error-message">
|
|
2170
|
-
{{ error }}
|
|
2171
|
-
</div>
|
|
2172
|
-
}
|
|
2173
|
-
|
|
2174
|
-
<!-- Create New Tenant Link -->
|
|
2175
|
-
@if (allowTenantCreation) {
|
|
2176
|
-
<div class="create-tenant-link">
|
|
2177
|
-
{{ createTenantLinkText }}
|
|
2178
|
-
<a href="#" (click)="onCreateTenantClick($event)">{{ createTenantLinkAction }}</a>
|
|
2179
|
-
</div>
|
|
2180
|
-
}
|
|
2586
|
+
<!-- Error Message -->
|
|
2587
|
+
@if (error) {
|
|
2588
|
+
<div class="error-message">
|
|
2589
|
+
{{ error }}
|
|
2590
|
+
</div>
|
|
2181
2591
|
}
|
|
2182
2592
|
|
|
2183
|
-
<!-- Loading
|
|
2593
|
+
<!-- Loading State -->
|
|
2184
2594
|
@if (loading) {
|
|
2185
2595
|
<div class="loading-overlay">
|
|
2186
2596
|
<div class="spinner"></div>
|
|
2187
2597
|
</div>
|
|
2188
2598
|
}
|
|
2599
|
+
|
|
2600
|
+
<!-- Register Link -->
|
|
2601
|
+
<div class="register-link">
|
|
2602
|
+
Don't have an account?
|
|
2603
|
+
<a href="#" (click)="onRegisterClick($event)">Sign up</a>
|
|
2604
|
+
</div>
|
|
2189
2605
|
</div>
|
|
2190
|
-
`, styles: [".
|
|
2191
|
-
}], ctorParameters: () => [{ type: AuthService }], propDecorators: {
|
|
2192
|
-
type: Input
|
|
2193
|
-
}], providers: [{
|
|
2194
|
-
type: Input
|
|
2195
|
-
}], showTenantSelector: [{
|
|
2196
|
-
type: Input
|
|
2197
|
-
}], autoSelectSingleTenant: [{
|
|
2198
|
-
type: Input
|
|
2199
|
-
}], allowTenantCreation: [{
|
|
2200
|
-
type: Input
|
|
2201
|
-
}], tenantSelectorTitle: [{
|
|
2202
|
-
type: Input
|
|
2203
|
-
}], tenantSelectorDescription: [{
|
|
2204
|
-
type: Input
|
|
2205
|
-
}], continueButtonText: [{
|
|
2206
|
-
type: Input
|
|
2207
|
-
}], registerLinkText: [{
|
|
2208
|
-
type: Input
|
|
2209
|
-
}], registerLinkAction: [{
|
|
2210
|
-
type: Input
|
|
2211
|
-
}], createTenantLinkText: [{
|
|
2212
|
-
type: Input
|
|
2213
|
-
}], createTenantLinkAction: [{
|
|
2606
|
+
`, styles: [".login-dialog{padding:24px;max-width:400px;position:relative}.login-title{margin:0 0 24px;font-size:24px;font-weight:500;text-align:center}.email-form,.form-group{margin-bottom:16px}.password-group{position:relative}.form-control{width:100%;padding:12px;border:1px solid #ddd;border-radius:4px;font-size:14px;box-sizing:border-box}.password-input{padding-right:45px}.password-toggle{position:absolute;right:8px;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;font-size:18px;padding:8px;line-height:1;opacity:.6;transition:opacity .2s}.password-toggle:hover{opacity:1}.password-toggle:focus{outline:2px solid #4285f4;outline-offset:2px;border-radius:4px}.form-control:focus{outline:none;border-color:#4285f4}.btn{padding:12px 24px;border:none;border-radius:4px;font-size:14px;font-weight:500;cursor:pointer;transition:background-color .2s}.btn:disabled{opacity:.6;cursor:not-allowed}.btn-block{width:100%}.btn-primary{background-color:#4285f4;color:#fff}.btn-primary:hover:not(:disabled){background-color:#357ae8}.divider{margin:16px 0;text-align:center;position:relative}.divider:before{content:\"\";position:absolute;top:50%;left:0;right:0;height:1px;background:#ddd}.divider span{background:#fff;padding:0 12px;position:relative;color:#666;font-size:12px}.oauth-buttons{display:flex;flex-direction:column;gap:12px}.btn-oauth{width:100%;background:#fff;color:#333;border:1px solid #ddd;display:flex;align-items:center;justify-content:center;gap:8px}.btn-oauth:hover:not(:disabled){background:#f8f8f8}.btn-google{border-color:#4285f4}.btn-linkedin{border-color:#0077b5}.btn-apple{border-color:#000}.btn-microsoft{border-color:#00a4ef}.btn-github{border-color:#333}.oauth-icon{font-size:18px}.error-message{margin-top:16px;padding:12px;background:#fee;color:#c33;border-radius:4px;font-size:14px}.loading-overlay{position:absolute;inset:0;background:#fffc;display:flex;align-items:center;justify-content:center}.spinner{width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #4285f4;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.register-link{margin-top:16px;text-align:center;font-size:14px;color:#666}.register-link a{color:#4285f4;text-decoration:none}.register-link a:hover{text-decoration:underline}\n"] }]
|
|
2607
|
+
}], ctorParameters: () => [{ type: AuthService }], propDecorators: { providers: [{
|
|
2214
2608
|
type: Input
|
|
2215
|
-
}], tenantSelected: [{
|
|
2216
|
-
type: Output
|
|
2217
|
-
}], createTenant: [{
|
|
2218
|
-
type: Output
|
|
2219
2609
|
}] } });
|
|
2220
2610
|
|
|
2221
2611
|
class TenantRegisterComponent {
|
|
@@ -2262,6 +2652,8 @@ class TenantRegisterComponent {
|
|
|
2262
2652
|
slugError = '';
|
|
2263
2653
|
useEmailPassword = false;
|
|
2264
2654
|
oauthProviders = [];
|
|
2655
|
+
showPassword = false;
|
|
2656
|
+
showConfirmPassword = false;
|
|
2265
2657
|
constructor(auth) {
|
|
2266
2658
|
this.auth = auth;
|
|
2267
2659
|
}
|
|
@@ -2559,30 +2951,44 @@ class TenantRegisterComponent {
|
|
|
2559
2951
|
class="form-control">
|
|
2560
2952
|
</div>
|
|
2561
2953
|
|
|
2562
|
-
<div class="form-group">
|
|
2954
|
+
<div class="form-group password-group">
|
|
2563
2955
|
<label for="password">Password *</label>
|
|
2564
2956
|
<input
|
|
2565
2957
|
id="password"
|
|
2566
2958
|
[(ngModel)]="password"
|
|
2567
2959
|
name="password"
|
|
2568
2960
|
placeholder="Create a password"
|
|
2569
|
-
type="password"
|
|
2961
|
+
[type]="showPassword ? 'text' : 'password'"
|
|
2570
2962
|
required
|
|
2571
2963
|
minlength="8"
|
|
2572
|
-
class="form-control">
|
|
2964
|
+
class="form-control password-input">
|
|
2965
|
+
<button
|
|
2966
|
+
type="button"
|
|
2967
|
+
class="password-toggle"
|
|
2968
|
+
(click)="showPassword = !showPassword"
|
|
2969
|
+
[attr.aria-label]="showPassword ? 'Hide password' : 'Show password'">
|
|
2970
|
+
{{ showPassword ? '👁️' : '👁️🗨️' }}
|
|
2971
|
+
</button>
|
|
2573
2972
|
<small class="form-hint">At least 8 characters</small>
|
|
2574
2973
|
</div>
|
|
2575
2974
|
|
|
2576
|
-
<div class="form-group">
|
|
2975
|
+
<div class="form-group password-group">
|
|
2577
2976
|
<label for="confirmPassword">Confirm Password *</label>
|
|
2578
2977
|
<input
|
|
2579
2978
|
id="confirmPassword"
|
|
2580
2979
|
[(ngModel)]="confirmPassword"
|
|
2581
2980
|
name="confirmPassword"
|
|
2582
2981
|
placeholder="Confirm your password"
|
|
2583
|
-
type="password"
|
|
2982
|
+
[type]="showConfirmPassword ? 'text' : 'password'"
|
|
2584
2983
|
required
|
|
2585
|
-
class="form-control">
|
|
2984
|
+
class="form-control password-input">
|
|
2985
|
+
<button
|
|
2986
|
+
type="button"
|
|
2987
|
+
class="password-toggle"
|
|
2988
|
+
(click)="showConfirmPassword = !showConfirmPassword"
|
|
2989
|
+
[attr.aria-label]="showConfirmPassword ? 'Hide password' : 'Show password'">
|
|
2990
|
+
{{ showConfirmPassword ? '👁️' : '👁️🗨️' }}
|
|
2991
|
+
</button>
|
|
2586
2992
|
</div>
|
|
2587
2993
|
|
|
2588
2994
|
<button
|
|
@@ -2631,7 +3037,7 @@ class TenantRegisterComponent {
|
|
|
2631
3037
|
<a href="#" (click)="onLoginClick($event)">{{ loginLinkAction }}</a>
|
|
2632
3038
|
</div>
|
|
2633
3039
|
</div>
|
|
2634
|
-
`, 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"] }] });
|
|
3040
|
+
`, isInline: true, styles: [".tenant-register-dialog{padding:24px;max-width:500px;position:relative}.register-title{margin:0 0 20px;font-size:24px;font-weight:500;text-align:center}.warning-box{display:flex;gap:12px;padding:16px;background:#fff3cd;border:1px solid #ffc107;border-radius:6px;margin-bottom:24px}.warning-icon{font-size:24px;line-height:1}.warning-content{flex:1}.warning-content strong{display:block;margin-bottom:4px;color:#856404;font-size:14px}.warning-content p{margin:0;color:#856404;font-size:13px;line-height:1.5}.section-header{font-size:16px;font-weight:600;margin:20px 0 12px;padding-bottom:8px;border-bottom:2px solid #e0e0e0;color:#333}.register-form,.form-group{margin-bottom:16px}.password-group{position:relative}.form-group label{display:block;margin-bottom:6px;font-size:14px;font-weight:500;color:#333}.form-control{width:100%;padding:12px;border:1px solid #ddd;border-radius:4px;font-size:14px;box-sizing:border-box;transition:border-color .2s}.password-input{padding-right:45px}.password-toggle{position:absolute;right:8px;top:38px;background:none;border:none;cursor:pointer;font-size:18px;padding:8px;line-height:1;opacity:.6;transition:opacity .2s}.password-toggle:hover{opacity:1}.password-toggle:focus{outline:2px solid #4285f4;outline-offset:2px;border-radius:4px}.form-control:focus{outline:none;border-color:#4285f4}.form-control.input-error{border-color:#dc3545}.form-control.input-success{border-color:#28a745}.form-hint{display:block;margin-top:4px;font-size:12px;color:#666}.form-hint .checking{color:#666}.form-hint .available{color:#28a745;font-weight:500}.form-hint .error-text{color:#dc3545}.oauth-section{margin:16px 0}.oauth-description{margin-bottom:12px;font-size:14px;color:#666;text-align:center}.oauth-buttons{display:flex;flex-direction:column;gap:12px}.btn{padding:12px 24px;border:none;border-radius:4px;font-size:14px;font-weight:500;cursor:pointer;transition:background-color .2s,opacity .2s}.btn:disabled{opacity:.6;cursor:not-allowed}.btn-block{width:100%}.btn-primary{background-color:#4285f4;color:#fff}.btn-primary:hover:not(:disabled){background-color:#357ae8}.btn-oauth{width:100%;background:#fff;color:#333;border:1px solid #ddd;display:flex;align-items:center;justify-content:center;gap:8px}.btn-oauth:hover:not(:disabled){background:#f8f8f8}.btn-google{border-color:#4285f4}.btn-linkedin{border-color:#0077b5}.btn-apple{border-color:#000}.btn-microsoft{border-color:#00a4ef}.btn-github{border-color:#333}.oauth-icon{font-size:18px}.switch-method{margin-top:12px;text-align:center;font-size:14px}.switch-method a{color:#4285f4;text-decoration:none}.switch-method a:hover{text-decoration:underline}.error-message{margin-top:16px;padding:12px;background:#fee;color:#c33;border-radius:4px;font-size:14px}.success-message{margin-top:16px;padding:12px;background:#efe;color:#3a3;border-radius:4px;font-size:14px}.loading-overlay{position:absolute;inset:0;background:#fffffff2;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:16px}.spinner{width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #4285f4;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.loading-text{margin:0;font-size:14px;color:#666}.login-link{margin-top:16px;text-align:center;font-size:14px;color:#666}.login-link a{color:#4285f4;text-decoration:none}.login-link a:hover{text-decoration:underline}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i2.MinLengthValidator, selector: "[minlength][formControlName],[minlength][formControl],[minlength][ngModel]", inputs: ["minlength"] }, { kind: "directive", type: i2.PatternValidator, selector: "[pattern][formControlName],[pattern][formControl],[pattern][ngModel]", inputs: ["pattern"] }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i2.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }] });
|
|
2635
3041
|
}
|
|
2636
3042
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TenantRegisterComponent, decorators: [{
|
|
2637
3043
|
type: Component,
|
|
@@ -2756,30 +3162,44 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
|
|
|
2756
3162
|
class="form-control">
|
|
2757
3163
|
</div>
|
|
2758
3164
|
|
|
2759
|
-
<div class="form-group">
|
|
3165
|
+
<div class="form-group password-group">
|
|
2760
3166
|
<label for="password">Password *</label>
|
|
2761
3167
|
<input
|
|
2762
3168
|
id="password"
|
|
2763
3169
|
[(ngModel)]="password"
|
|
2764
3170
|
name="password"
|
|
2765
3171
|
placeholder="Create a password"
|
|
2766
|
-
type="password"
|
|
3172
|
+
[type]="showPassword ? 'text' : 'password'"
|
|
2767
3173
|
required
|
|
2768
3174
|
minlength="8"
|
|
2769
|
-
class="form-control">
|
|
3175
|
+
class="form-control password-input">
|
|
3176
|
+
<button
|
|
3177
|
+
type="button"
|
|
3178
|
+
class="password-toggle"
|
|
3179
|
+
(click)="showPassword = !showPassword"
|
|
3180
|
+
[attr.aria-label]="showPassword ? 'Hide password' : 'Show password'">
|
|
3181
|
+
{{ showPassword ? '👁️' : '👁️🗨️' }}
|
|
3182
|
+
</button>
|
|
2770
3183
|
<small class="form-hint">At least 8 characters</small>
|
|
2771
3184
|
</div>
|
|
2772
3185
|
|
|
2773
|
-
<div class="form-group">
|
|
3186
|
+
<div class="form-group password-group">
|
|
2774
3187
|
<label for="confirmPassword">Confirm Password *</label>
|
|
2775
3188
|
<input
|
|
2776
3189
|
id="confirmPassword"
|
|
2777
3190
|
[(ngModel)]="confirmPassword"
|
|
2778
3191
|
name="confirmPassword"
|
|
2779
3192
|
placeholder="Confirm your password"
|
|
2780
|
-
type="password"
|
|
3193
|
+
[type]="showConfirmPassword ? 'text' : 'password'"
|
|
2781
3194
|
required
|
|
2782
|
-
class="form-control">
|
|
3195
|
+
class="form-control password-input">
|
|
3196
|
+
<button
|
|
3197
|
+
type="button"
|
|
3198
|
+
class="password-toggle"
|
|
3199
|
+
(click)="showConfirmPassword = !showConfirmPassword"
|
|
3200
|
+
[attr.aria-label]="showConfirmPassword ? 'Hide password' : 'Show password'">
|
|
3201
|
+
{{ showConfirmPassword ? '👁️' : '👁️🗨️' }}
|
|
3202
|
+
</button>
|
|
2783
3203
|
</div>
|
|
2784
3204
|
|
|
2785
3205
|
<button
|
|
@@ -2828,7 +3248,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
|
|
|
2828
3248
|
<a href="#" (click)="onLoginClick($event)">{{ loginLinkAction }}</a>
|
|
2829
3249
|
</div>
|
|
2830
3250
|
</div>
|
|
2831
|
-
`, 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"] }]
|
|
3251
|
+
`, styles: [".tenant-register-dialog{padding:24px;max-width:500px;position:relative}.register-title{margin:0 0 20px;font-size:24px;font-weight:500;text-align:center}.warning-box{display:flex;gap:12px;padding:16px;background:#fff3cd;border:1px solid #ffc107;border-radius:6px;margin-bottom:24px}.warning-icon{font-size:24px;line-height:1}.warning-content{flex:1}.warning-content strong{display:block;margin-bottom:4px;color:#856404;font-size:14px}.warning-content p{margin:0;color:#856404;font-size:13px;line-height:1.5}.section-header{font-size:16px;font-weight:600;margin:20px 0 12px;padding-bottom:8px;border-bottom:2px solid #e0e0e0;color:#333}.register-form,.form-group{margin-bottom:16px}.password-group{position:relative}.form-group label{display:block;margin-bottom:6px;font-size:14px;font-weight:500;color:#333}.form-control{width:100%;padding:12px;border:1px solid #ddd;border-radius:4px;font-size:14px;box-sizing:border-box;transition:border-color .2s}.password-input{padding-right:45px}.password-toggle{position:absolute;right:8px;top:38px;background:none;border:none;cursor:pointer;font-size:18px;padding:8px;line-height:1;opacity:.6;transition:opacity .2s}.password-toggle:hover{opacity:1}.password-toggle:focus{outline:2px solid #4285f4;outline-offset:2px;border-radius:4px}.form-control:focus{outline:none;border-color:#4285f4}.form-control.input-error{border-color:#dc3545}.form-control.input-success{border-color:#28a745}.form-hint{display:block;margin-top:4px;font-size:12px;color:#666}.form-hint .checking{color:#666}.form-hint .available{color:#28a745;font-weight:500}.form-hint .error-text{color:#dc3545}.oauth-section{margin:16px 0}.oauth-description{margin-bottom:12px;font-size:14px;color:#666;text-align:center}.oauth-buttons{display:flex;flex-direction:column;gap:12px}.btn{padding:12px 24px;border:none;border-radius:4px;font-size:14px;font-weight:500;cursor:pointer;transition:background-color .2s,opacity .2s}.btn:disabled{opacity:.6;cursor:not-allowed}.btn-block{width:100%}.btn-primary{background-color:#4285f4;color:#fff}.btn-primary:hover:not(:disabled){background-color:#357ae8}.btn-oauth{width:100%;background:#fff;color:#333;border:1px solid #ddd;display:flex;align-items:center;justify-content:center;gap:8px}.btn-oauth:hover:not(:disabled){background:#f8f8f8}.btn-google{border-color:#4285f4}.btn-linkedin{border-color:#0077b5}.btn-apple{border-color:#000}.btn-microsoft{border-color:#00a4ef}.btn-github{border-color:#333}.oauth-icon{font-size:18px}.switch-method{margin-top:12px;text-align:center;font-size:14px}.switch-method a{color:#4285f4;text-decoration:none}.switch-method a:hover{text-decoration:underline}.error-message{margin-top:16px;padding:12px;background:#fee;color:#c33;border-radius:4px;font-size:14px}.success-message{margin-top:16px;padding:12px;background:#efe;color:#3a3;border-radius:4px;font-size:14px}.loading-overlay{position:absolute;inset:0;background:#fffffff2;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:16px}.spinner{width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #4285f4;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.loading-text{margin:0;font-size:14px;color:#666}.login-link{margin-top:16px;text-align:center;font-size:14px;color:#666}.login-link a{color:#4285f4;text-decoration:none}.login-link a:hover{text-decoration:underline}\n"] }]
|
|
2832
3252
|
}], ctorParameters: () => [{ type: AuthService }], propDecorators: { title: [{
|
|
2833
3253
|
type: Input
|
|
2834
3254
|
}], providers: [{
|
|
@@ -2938,7 +3358,7 @@ class TenantLoginDialogComponent {
|
|
|
2938
3358
|
(createTenant)="onCreateTenant()">
|
|
2939
3359
|
</lib-tenant-login>
|
|
2940
3360
|
</div>
|
|
2941
|
-
`, 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"] }] });
|
|
3361
|
+
`, isInline: true, styles: [".dialog-wrapper{padding:0}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: TenantLoginComponent, selector: "lib-tenant-login", inputs: ["title", "providers", "showTenantSelector", "autoSelectSingleTenant", "prefillEmail", "allowTenantCreation", "tenantSelectorTitle", "tenantSelectorDescription", "continueButtonText", "registerLinkText", "registerLinkAction", "createTenantLinkText", "createTenantLinkAction"], outputs: ["tenantSelected", "createTenant"] }] });
|
|
2942
3362
|
}
|
|
2943
3363
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TenantLoginDialogComponent, decorators: [{
|
|
2944
3364
|
type: Component,
|
|
@@ -3095,5 +3515,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
|
|
|
3095
3515
|
* Generated bundle index. Do not edit.
|
|
3096
3516
|
*/
|
|
3097
3517
|
|
|
3098
|
-
export { ApiConnectionService, ApiResponse, AuthService, CsrfService, DbService, LoginDialogComponent, MyEnvironmentModel, NgxStoneScriptPhpClientModule, RegisterComponent, SigninStatusService, TenantLoginComponent, TenantLoginDialogComponent, TenantRegisterComponent, TenantRegisterDialogComponent, TokenService, VerifyStatus };
|
|
3518
|
+
export { ApiConnectionService, ApiResponse, AuthPageComponent, AuthService, CsrfService, DbService, LoginDialogComponent, MyEnvironmentModel, NgxStoneScriptPhpClientModule, RegisterComponent, SigninStatusService, TenantLoginComponent, TenantLoginDialogComponent, TenantRegisterComponent, TenantRegisterDialogComponent, TokenService, VerifyStatus };
|
|
3099
3519
|
//# sourceMappingURL=progalaxyelabs-ngx-stonescriptphp-client.mjs.map
|