@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, Input, Component, EventEmitter, Output, Optional } from '@angular/core';
2
+ import { Injectable, Inject, NgModule, EventEmitter, Output, Input, Component, Optional } from '@angular/core';
3
3
  import { BehaviorSubject } from 'rxjs';
4
+ import * as i2$1 from '@angular/common';
4
5
  import { CommonModule } from '@angular/common';
5
6
  import * as i2 from '@angular/forms';
6
7
  import { FormsModule } from '@angular/forms';
@@ -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.userSubject.next(data.user);
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.userSubject.next(event.data.user);
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.userSubject.next(data.user);
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
- await fetch(`${this.environment.accountsUrl}/api/auth/logout`, {
767
- method: 'POST',
768
- credentials: 'include'
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.userSubject.next(null);
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.userSubject.next(data.user);
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.userSubject.next(result.user);
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.userSubject.next(event.data.user);
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 LoginDialogComponent {
1260
+ class TenantLoginComponent {
1146
1261
  auth;
1147
- /**
1148
- * REQUIRED: Which authentication providers to show in this dialog
1149
- * @example ['google', 'linkedin', 'emailPassword']
1150
- */
1151
- providers = [];
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. Please pass providers to LoginDialogComponent.';
1163
- throw new Error('LoginDialogComponent requires providers input. Example: dialogRef.componentInstance.providers = [\'google\', \'emailPassword\']');
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
- // On success, parent component/dialog should close automatically via user$ subscription
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
- // On success, parent component/dialog should close automatically via user$ subscription
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
- onRegisterClick(event) {
1226
- event.preventDefault();
1227
- // Platforms can override this or listen for a custom event
1228
- // For now, just emit a console message
1229
- console.log('Register clicked - platform should handle navigation');
1230
- }
1231
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: LoginDialogComponent, deps: [{ token: AuthService }], target: i0.ɵɵFactoryTarget.Component });
1232
- 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: `
1233
- <div class="login-dialog">
1234
- <h2 class="login-title">Sign In</h2>
1235
-
1236
- <!-- Email/Password Form (if enabled) -->
1237
- @if (isProviderEnabled('emailPassword')) {
1238
- <form (ngSubmit)="onEmailLogin()" class="email-form">
1239
- <div class="form-group">
1240
- <input
1241
- [(ngModel)]="email"
1242
- name="email"
1243
- placeholder="Email"
1244
- type="email"
1245
- required
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
- <!-- OAuth Providers -->
1274
- @if (oauthProviders.length > 0) {
1275
- <div class="oauth-buttons">
1276
- @for (provider of oauthProviders; track provider) {
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
- <!-- Error Message -->
1293
- @if (error) {
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
- <!-- Loading State -->
1300
- @if (loading) {
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
- <!-- Register Link -->
1307
- <div class="register-link">
1308
- Don't have an account?
1309
- <a href="#" (click)="onRegisterClick($event)">Sign up</a>
1310
- </div>
1311
- </div>
1312
- `, isInline: true, styles: [".login-dialog{padding:24px;max-width:400px;position:relative}.login-title{margin:0 0 24px;font-size:24px;font-weight:500;text-align:center}.email-form,.form-group{margin-bottom:16px}.form-control{width:100%;padding:12px;border:1px solid #ddd;border-radius:4px;font-size:14px;box-sizing:border-box}.form-control:focus{outline:none;border-color:#4285f4}.btn{padding:12px 24px;border:none;border-radius:4px;font-size:14px;font-weight:500;cursor:pointer;transition:background-color .2s}.btn:disabled{opacity:.6;cursor:not-allowed}.btn-block{width:100%}.btn-primary{background-color:#4285f4;color:#fff}.btn-primary:hover:not(:disabled){background-color:#357ae8}.divider{margin:16px 0;text-align:center;position:relative}.divider:before{content:\"\";position:absolute;top:50%;left:0;right:0;height:1px;background:#ddd}.divider span{background:#fff;padding:0 12px;position:relative;color:#666;font-size:12px}.oauth-buttons{display:flex;flex-direction:column;gap:12px}.btn-oauth{width:100%;background:#fff;color:#333;border:1px solid #ddd;display:flex;align-items:center;justify-content:center;gap:8px}.btn-oauth:hover:not(:disabled){background:#f8f8f8}.btn-google{border-color:#4285f4}.btn-linkedin{border-color:#0077b5}.btn-apple{border-color:#000}.btn-microsoft{border-color:#00a4ef}.btn-github{border-color:#333}.oauth-icon{font-size:18px}.error-message{margin-top:16px;padding:12px;background:#fee;color:#c33;border-radius:4px;font-size:14px}.loading-overlay{position:absolute;inset:0;background:#fffc;display:flex;align-items:center;justify-content:center}.spinner{width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #4285f4;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.register-link{margin-top:16px;text-align:center;font-size:14px;color:#666}.register-link a{color:#4285f4;text-decoration:none}.register-link a:hover{text-decoration:underline}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i2.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }] });
1313
- }
1314
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: LoginDialogComponent, decorators: [{
1315
- type: Component,
1316
- args: [{ selector: 'lib-login-dialog', standalone: true, imports: [CommonModule, FormsModule], template: `
1317
- <div class="login-dialog">
1318
- <h2 class="login-title">Sign In</h2>
1319
-
1320
- <!-- Email/Password Form (if enabled) -->
1321
- @if (isProviderEnabled('emailPassword')) {
1322
- <form (ngSubmit)="onEmailLogin()" class="email-form">
1323
- <div class="form-group">
1324
- <input
1325
- [(ngModel)]="email"
1326
- name="email"
1327
- placeholder="Email"
1328
- type="email"
1329
- required
1330
- class="form-control">
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
- <div class="form-group">
1333
- <input
1334
- [(ngModel)]="password"
1335
- name="password"
1336
- placeholder="Password"
1337
- type="password"
1338
- required
1339
- class="form-control">
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
- <button
1342
- type="submit"
1343
- [disabled]="loading"
1344
- class="btn btn-primary btn-block">
1345
- {{ loading ? 'Signing in...' : getProviderLabel('emailPassword') }}
1346
- </button>
1347
- </form>
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
- <!-- Divider if both email and OAuth are present -->
1351
- @if (isProviderEnabled('emailPassword') && oauthProviders.length > 0) {
1352
- <div class="divider">
1353
- <span>OR</span>
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
- <!-- OAuth Providers -->
1358
- @if (oauthProviders.length > 0) {
1359
- <div class="oauth-buttons">
1360
- @for (provider of oauthProviders; track provider) {
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
- (click)="onOAuthLogin(provider)"
1692
+ type="submit"
1363
1693
  [disabled]="loading"
1364
- class="btn btn-oauth btn-{{ provider }}">
1365
- @if (getProviderIcon(provider)) {
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
- </div>
1374
- }
1705
+ }
1375
1706
 
1376
- <!-- Error Message -->
1377
- @if (error) {
1378
- <div class="error-message">
1379
- {{ error }}
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 State -->
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:400px;position:relative}.login-title{margin:0 0 24px;font-size:24px;font-weight:500;text-align:center}.email-form,.form-group{margin-bottom:16px}.form-control{width:100%;padding:12px;border:1px solid #ddd;border-radius:4px;font-size:14px;box-sizing:border-box}.form-control:focus{outline:none;border-color:#4285f4}.btn{padding:12px 24px;border:none;border-radius:4px;font-size:14px;font-weight:500;cursor:pointer;transition:background-color .2s}.btn:disabled{opacity:.6;cursor:not-allowed}.btn-block{width:100%}.btn-primary{background-color:#4285f4;color:#fff}.btn-primary:hover:not(:disabled){background-color:#357ae8}.divider{margin:16px 0;text-align:center;position:relative}.divider:before{content:\"\";position:absolute;top:50%;left:0;right:0;height:1px;background:#ddd}.divider span{background:#fff;padding:0 12px;position:relative;color:#666;font-size:12px}.oauth-buttons{display:flex;flex-direction:column;gap:12px}.btn-oauth{width:100%;background:#fff;color:#333;border:1px solid #ddd;display:flex;align-items:center;justify-content:center;gap:8px}.btn-oauth:hover:not(:disabled){background:#f8f8f8}.btn-google{border-color:#4285f4}.btn-linkedin{border-color:#0077b5}.btn-apple{border-color:#000}.btn-microsoft{border-color:#00a4ef}.btn-github{border-color:#333}.oauth-icon{font-size:18px}.error-message{margin-top:16px;padding:12px;background:#fee;color:#c33;border-radius:4px;font-size:14px}.loading-overlay{position:absolute;inset:0;background:#fffc;display:flex;align-items:center;justify-content:center}.spinner{width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #4285f4;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.register-link{margin-top:16px;text-align:center;font-size:14px;color:#666}.register-link a{color:#4285f4;text-decoration:none}.register-link a:hover{text-decoration:underline}\n"] }]
1397
- }], ctorParameters: () => [{ type: AuthService }], propDecorators: { providers: [{
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
- constructor(auth) {
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
- const result = await this.auth.register(this.email, this.password, this.displayName);
1439
- if (result.success) {
1440
- this.success = result.message || 'Account created successfully!';
1441
- // On success, parent component/dialog should close automatically via user$ subscription
1442
- // or navigate to email verification page
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
- this.error = result.message || 'Registration failed';
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
- // Platforms can override this or listen for a custom event
1458
- // For now, just emit a console message
1459
- console.log('Login clicked - platform should handle navigation');
1460
- }
1461
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: RegisterComponent, deps: [{ token: AuthService }], target: i0.ɵɵFactoryTarget.Component });
1462
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.15", type: RegisterComponent, isStandalone: true, selector: "lib-register", ngImport: i0, template: `
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
- <form (ngSubmit)="onRegister()" class="register-form">
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
- <form (ngSubmit)="onRegister()" class="register-form">
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 TenantLoginComponent {
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
- // Component Configuration
1652
- title = 'Sign In';
1653
- providers = ['google'];
1654
- showTenantSelector = true;
1655
- autoSelectSingleTenant = true;
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
- useOAuth = true;
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('TenantLoginComponent requires providers input.');
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
- return undefined;
1712
- }
1713
- toggleAuthMethod(event) {
1714
- event.preventDefault();
1715
- this.useOAuth = !this.useOAuth;
1716
- this.error = '';
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.selectTenant(membership.tenant_id);
2391
+ const result = await this.auth.loginWithEmail(this.email, this.password);
1824
2392
  if (!result.success) {
1825
- this.error = result.message || 'Failed to select organization';
1826
- return;
2393
+ this.error = result.message || 'Login failed';
1827
2394
  }
1828
- // Emit tenant selected event
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 = err.message || 'An unexpected error occurred';
2398
+ this.error = 'An unexpected error occurred';
1837
2399
  }
1838
2400
  finally {
1839
2401
  this.loading = false;
1840
2402
  }
1841
2403
  }
1842
- formatRole(role) {
1843
- return role.charAt(0).toUpperCase() + role.slice(1);
1844
- }
1845
- formatLastAccessed(dateStr) {
2404
+ async onOAuthLogin(provider) {
2405
+ this.loading = true;
2406
+ this.error = '';
1846
2407
  try {
1847
- const date = new Date(dateStr);
1848
- const now = new Date();
1849
- const diffMs = now.getTime() - date.getTime();
1850
- const diffDays = Math.floor(diffMs / (1000 * 60 * 60 * 24));
1851
- if (diffDays === 0)
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
- return dateStr;
2414
+ catch (err) {
2415
+ this.error = 'An unexpected error occurred';
2416
+ }
2417
+ finally {
2418
+ this.loading = false;
1863
2419
  }
1864
2420
  }
1865
- onCreateTenantClick(event) {
2421
+ onRegisterClick(event) {
1866
2422
  event.preventDefault();
1867
- this.createTenant.emit();
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: TenantLoginComponent, deps: [{ token: AuthService }], target: i0.ɵɵFactoryTarget.Component });
1870
- static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.15", type: TenantLoginComponent, isStandalone: true, selector: "lib-tenant-login", inputs: { title: "title", providers: "providers", showTenantSelector: "showTenantSelector", autoSelectSingleTenant: "autoSelectSingleTenant", allowTenantCreation: "allowTenantCreation", tenantSelectorTitle: "tenantSelectorTitle", tenantSelectorDescription: "tenantSelectorDescription", continueButtonText: "continueButtonText", registerLinkText: "registerLinkText", registerLinkAction: "registerLinkAction", createTenantLinkText: "createTenantLinkText", createTenantLinkAction: "createTenantLinkAction" }, outputs: { tenantSelected: "tenantSelected", createTenant: "createTenant" }, ngImport: i0, template: `
1871
- <div class="tenant-login-dialog">
1872
- @if (!showingTenantSelector) {
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
- <!-- Email/Password Form (if enabled) -->
1877
- @if (isProviderEnabled('emailPassword') && !useOAuth) {
1878
- <form (ngSubmit)="onEmailLogin()" class="email-form">
1879
- <div class="form-group">
1880
- <input
1881
- [(ngModel)]="email"
1882
- name="email"
1883
- placeholder="Email"
1884
- type="email"
1885
- required
1886
- class="form-control">
1887
- </div>
1888
- <div class="form-group">
1889
- <input
1890
- [(ngModel)]="password"
1891
- name="password"
1892
- placeholder="Password"
1893
- type="password"
1894
- required
1895
- class="form-control">
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="submit"
1899
- [disabled]="loading"
1900
- class="btn btn-primary btn-block">
1901
- {{ loading ? 'Signing in...' : 'Sign in with Email' }}
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
- <p class="selector-description">{{ tenantSelectorDescription }}</p>
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
- <div class="tenant-list">
1969
- @for (membership of memberships; track membership.tenant_id) {
1970
- <div
1971
- class="tenant-item"
1972
- [class.selected]="selectedTenantId === membership.tenant_id"
1973
- (click)="selectTenantItem(membership.tenant_id)">
1974
- <div class="tenant-radio">
1975
- <input
1976
- type="radio"
1977
- [checked]="selectedTenantId === membership.tenant_id"
1978
- [name]="'tenant-' + membership.tenant_id"
1979
- [id]="'tenant-' + membership.tenant_id">
1980
- </div>
1981
- <div class="tenant-info">
1982
- <div class="tenant-name">{{ membership.name }}</div>
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
- <button
1998
- type="button"
1999
- (click)="onContinueWithTenant()"
2000
- [disabled]="!selectedTenantId || loading"
2001
- class="btn btn-primary btn-block">
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 Overlay -->
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: [".tenant-login-dialog{padding:24px;max-width:450px;position:relative}.login-title{margin:0 0 24px;font-size:24px;font-weight:500;text-align:center}.welcome-message{margin-bottom:16px;padding:12px;background:#e8f5e9;border-radius:4px;text-align:center;font-size:14px;color:#2e7d32}.selector-description{margin-bottom:20px;font-size:14px;color:#666;text-align:center}.email-form,.form-group{margin-bottom:16px}.form-control{width:100%;padding:12px;border:1px solid #ddd;border-radius:4px;font-size:14px;box-sizing:border-box}.form-control:focus{outline:none;border-color:#4285f4}.btn{padding:12px 24px;border:none;border-radius:4px;font-size:14px;font-weight:500;cursor:pointer;transition:background-color .2s}.btn:disabled{opacity:.6;cursor:not-allowed}.btn-block{width:100%}.btn-primary{background-color:#4285f4;color:#fff}.btn-primary:hover:not(:disabled){background-color:#357ae8}.divider{margin:16px 0;text-align:center;position:relative}.divider:before{content:\"\";position:absolute;top:50%;left:0;right:0;height:1px;background:#ddd}.divider span{background:#fff;padding:0 12px;position:relative;color:#666;font-size:12px}.oauth-buttons{display:flex;flex-direction:column;gap:12px}.btn-oauth{width:100%;background:#fff;color:#333;border:1px solid #ddd;display:flex;align-items:center;justify-content:center;gap:8px}.btn-oauth:hover:not(:disabled){background:#f8f8f8}.btn-google{border-color:#4285f4}.btn-linkedin{border-color:#0077b5}.btn-apple{border-color:#000}.btn-microsoft{border-color:#00a4ef}.btn-github{border-color:#333}.oauth-icon{font-size:18px}.switch-method{margin-top:12px;text-align:center;font-size:14px}.switch-method a{color:#4285f4;text-decoration:none}.switch-method a:hover{text-decoration:underline}.tenant-list{margin-bottom:20px;display:flex;flex-direction:column;gap:12px}.tenant-item{display:flex;align-items:flex-start;gap:12px;padding:16px;border:2px solid #e0e0e0;border-radius:6px;cursor:pointer;transition:all .2s}.tenant-item:hover{border-color:#4285f4;background:#f8f9ff}.tenant-item.selected{border-color:#4285f4;background:#e8f0fe}.tenant-radio{flex-shrink:0;padding-top:2px}.tenant-radio input[type=radio]{width:18px;height:18px;cursor:pointer}.tenant-info{flex:1}.tenant-name{font-size:16px;font-weight:500;color:#333;margin-bottom:4px}.tenant-meta{font-size:13px;color:#666}.tenant-role{font-weight:500;color:#4285f4}.tenant-separator{margin:0 6px}.error-message{margin-top:16px;padding:12px;background:#fee;color:#c33;border-radius:4px;font-size:14px}.loading-overlay{position:absolute;inset:0;background:#fffc;display:flex;align-items:center;justify-content:center}.spinner{width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #4285f4;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.register-link{margin-top:16px;text-align:center;font-size:14px;color:#666}.register-link a{color:#4285f4;text-decoration:none}.register-link a:hover{text-decoration:underline}.create-tenant-link{margin-top:16px;padding-top:16px;border-top:1px solid #e0e0e0;text-align:center;font-size:14px;color:#666}.create-tenant-link a{color:#4285f4;text-decoration:none;font-weight:500}.create-tenant-link a:hover{text-decoration:underline}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i2.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }] });
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: TenantLoginComponent, decorators: [{
2517
+ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: LoginDialogComponent, decorators: [{
2031
2518
  type: Component,
2032
- args: [{ selector: 'lib-tenant-login', standalone: true, imports: [CommonModule, FormsModule], template: `
2033
- <div class="tenant-login-dialog">
2034
- @if (!showingTenantSelector) {
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
- <!-- Email/Password Form (if enabled) -->
2039
- @if (isProviderEnabled('emailPassword') && !useOAuth) {
2040
- <form (ngSubmit)="onEmailLogin()" class="email-form">
2041
- <div class="form-group">
2042
- <input
2043
- [(ngModel)]="email"
2044
- name="email"
2045
- placeholder="Email"
2046
- type="email"
2047
- required
2048
- class="form-control">
2049
- </div>
2050
- <div class="form-group">
2051
- <input
2052
- [(ngModel)]="password"
2053
- name="password"
2054
- placeholder="Password"
2055
- type="password"
2056
- required
2057
- class="form-control">
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="submit"
2061
- [disabled]="loading"
2062
- class="btn btn-primary btn-block">
2063
- {{ loading ? 'Signing in...' : 'Sign in with Email' }}
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
- <p class="selector-description">{{ tenantSelectorDescription }}</p>
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
- <div class="tenant-list">
2131
- @for (membership of memberships; track membership.tenant_id) {
2132
- <div
2133
- class="tenant-item"
2134
- [class.selected]="selectedTenantId === membership.tenant_id"
2135
- (click)="selectTenantItem(membership.tenant_id)">
2136
- <div class="tenant-radio">
2137
- <input
2138
- type="radio"
2139
- [checked]="selectedTenantId === membership.tenant_id"
2140
- [name]="'tenant-' + membership.tenant_id"
2141
- [id]="'tenant-' + membership.tenant_id">
2142
- </div>
2143
- <div class="tenant-info">
2144
- <div class="tenant-name">{{ membership.name }}</div>
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
- <button
2160
- type="button"
2161
- (click)="onContinueWithTenant()"
2162
- [disabled]="!selectedTenantId || loading"
2163
- class="btn btn-primary btn-block">
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 Overlay -->
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: [".tenant-login-dialog{padding:24px;max-width:450px;position:relative}.login-title{margin:0 0 24px;font-size:24px;font-weight:500;text-align:center}.welcome-message{margin-bottom:16px;padding:12px;background:#e8f5e9;border-radius:4px;text-align:center;font-size:14px;color:#2e7d32}.selector-description{margin-bottom:20px;font-size:14px;color:#666;text-align:center}.email-form,.form-group{margin-bottom:16px}.form-control{width:100%;padding:12px;border:1px solid #ddd;border-radius:4px;font-size:14px;box-sizing:border-box}.form-control:focus{outline:none;border-color:#4285f4}.btn{padding:12px 24px;border:none;border-radius:4px;font-size:14px;font-weight:500;cursor:pointer;transition:background-color .2s}.btn:disabled{opacity:.6;cursor:not-allowed}.btn-block{width:100%}.btn-primary{background-color:#4285f4;color:#fff}.btn-primary:hover:not(:disabled){background-color:#357ae8}.divider{margin:16px 0;text-align:center;position:relative}.divider:before{content:\"\";position:absolute;top:50%;left:0;right:0;height:1px;background:#ddd}.divider span{background:#fff;padding:0 12px;position:relative;color:#666;font-size:12px}.oauth-buttons{display:flex;flex-direction:column;gap:12px}.btn-oauth{width:100%;background:#fff;color:#333;border:1px solid #ddd;display:flex;align-items:center;justify-content:center;gap:8px}.btn-oauth:hover:not(:disabled){background:#f8f8f8}.btn-google{border-color:#4285f4}.btn-linkedin{border-color:#0077b5}.btn-apple{border-color:#000}.btn-microsoft{border-color:#00a4ef}.btn-github{border-color:#333}.oauth-icon{font-size:18px}.switch-method{margin-top:12px;text-align:center;font-size:14px}.switch-method a{color:#4285f4;text-decoration:none}.switch-method a:hover{text-decoration:underline}.tenant-list{margin-bottom:20px;display:flex;flex-direction:column;gap:12px}.tenant-item{display:flex;align-items:flex-start;gap:12px;padding:16px;border:2px solid #e0e0e0;border-radius:6px;cursor:pointer;transition:all .2s}.tenant-item:hover{border-color:#4285f4;background:#f8f9ff}.tenant-item.selected{border-color:#4285f4;background:#e8f0fe}.tenant-radio{flex-shrink:0;padding-top:2px}.tenant-radio input[type=radio]{width:18px;height:18px;cursor:pointer}.tenant-info{flex:1}.tenant-name{font-size:16px;font-weight:500;color:#333;margin-bottom:4px}.tenant-meta{font-size:13px;color:#666}.tenant-role{font-weight:500;color:#4285f4}.tenant-separator{margin:0 6px}.error-message{margin-top:16px;padding:12px;background:#fee;color:#c33;border-radius:4px;font-size:14px}.loading-overlay{position:absolute;inset:0;background:#fffc;display:flex;align-items:center;justify-content:center}.spinner{width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #4285f4;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.register-link{margin-top:16px;text-align:center;font-size:14px;color:#666}.register-link a{color:#4285f4;text-decoration:none}.register-link a:hover{text-decoration:underline}.create-tenant-link{margin-top:16px;padding-top:16px;border-top:1px solid #e0e0e0;text-align:center;font-size:14px;color:#666}.create-tenant-link a{color:#4285f4;text-decoration:none;font-weight:500}.create-tenant-link a:hover{text-decoration:underline}\n"] }]
2191
- }], ctorParameters: () => [{ type: AuthService }], propDecorators: { title: [{
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