@progalaxyelabs/ngx-stonescriptphp-client 1.3.1 → 1.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { Injectable, Inject, NgModule,
|
|
2
|
+
import { Injectable, Inject, NgModule, EventEmitter, Output, Input, Component, Optional } from '@angular/core';
|
|
3
3
|
import { BehaviorSubject } from 'rxjs';
|
|
4
|
+
import * as i2$1 from '@angular/common';
|
|
4
5
|
import { CommonModule } from '@angular/common';
|
|
5
6
|
import * as i2 from '@angular/forms';
|
|
6
7
|
import { FormsModule } from '@angular/forms';
|
|
@@ -194,6 +195,11 @@ class MyEnvironmentModel {
|
|
|
194
195
|
csrfTokenCookieName: 'csrf_token',
|
|
195
196
|
csrfHeaderName: 'X-CSRF-Token'
|
|
196
197
|
};
|
|
198
|
+
/**
|
|
199
|
+
* Branding configuration for auth components
|
|
200
|
+
* Allows platforms to customize login/register pages without creating wrappers
|
|
201
|
+
*/
|
|
202
|
+
branding;
|
|
197
203
|
}
|
|
198
204
|
|
|
199
205
|
/**
|
|
@@ -1251,29 +1257,59 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
|
|
|
1251
1257
|
}]
|
|
1252
1258
|
}] });
|
|
1253
1259
|
|
|
1254
|
-
class
|
|
1260
|
+
class TenantLoginComponent {
|
|
1255
1261
|
auth;
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
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
|
|
1261
1282
|
email = '';
|
|
1262
1283
|
password = '';
|
|
1284
|
+
// State
|
|
1263
1285
|
error = '';
|
|
1264
1286
|
loading = false;
|
|
1287
|
+
showPassword = false;
|
|
1288
|
+
useOAuth = true;
|
|
1265
1289
|
oauthProviders = [];
|
|
1290
|
+
// Tenant Selection State
|
|
1291
|
+
showingTenantSelector = false;
|
|
1292
|
+
memberships = [];
|
|
1293
|
+
selectedTenantId = null;
|
|
1294
|
+
userName = '';
|
|
1266
1295
|
constructor(auth) {
|
|
1267
1296
|
this.auth = auth;
|
|
1268
1297
|
}
|
|
1269
1298
|
ngOnInit() {
|
|
1270
1299
|
if (!this.providers || this.providers.length === 0) {
|
|
1271
|
-
this.error = 'Configuration Error: No authentication providers specified.
|
|
1272
|
-
throw new Error('
|
|
1300
|
+
this.error = 'Configuration Error: No authentication providers specified.';
|
|
1301
|
+
throw new Error('TenantLoginComponent requires providers input.');
|
|
1302
|
+
}
|
|
1303
|
+
this.oauthProviders = this.providers.filter(p => p !== 'emailPassword');
|
|
1304
|
+
// If only emailPassword is available, use it by default
|
|
1305
|
+
if (this.oauthProviders.length === 0 && this.isProviderEnabled('emailPassword')) {
|
|
1306
|
+
this.useOAuth = false;
|
|
1307
|
+
}
|
|
1308
|
+
// Prefill email if provided (for account linking flow)
|
|
1309
|
+
if (this.prefillEmail) {
|
|
1310
|
+
this.email = this.prefillEmail;
|
|
1311
|
+
this.useOAuth = false; // Switch to email/password form
|
|
1273
1312
|
}
|
|
1274
|
-
// Get OAuth providers (excluding emailPassword)
|
|
1275
|
-
this.oauthProviders = this.providers
|
|
1276
|
-
.filter(p => p !== 'emailPassword');
|
|
1277
1313
|
}
|
|
1278
1314
|
isProviderEnabled(provider) {
|
|
1279
1315
|
return this.providers.includes(provider);
|
|
@@ -1290,9 +1326,13 @@ class LoginDialogComponent {
|
|
|
1290
1326
|
return labels[provider];
|
|
1291
1327
|
}
|
|
1292
1328
|
getProviderIcon(provider) {
|
|
1293
|
-
// Platforms can customize icons via CSS classes: .btn-google, .btn-linkedin, etc.
|
|
1294
1329
|
return undefined;
|
|
1295
1330
|
}
|
|
1331
|
+
toggleAuthMethod(event) {
|
|
1332
|
+
event.preventDefault();
|
|
1333
|
+
this.useOAuth = !this.useOAuth;
|
|
1334
|
+
this.error = '';
|
|
1335
|
+
}
|
|
1296
1336
|
async onEmailLogin() {
|
|
1297
1337
|
if (!this.email || !this.password) {
|
|
1298
1338
|
this.error = 'Please enter email and password';
|
|
@@ -1304,11 +1344,13 @@ class LoginDialogComponent {
|
|
|
1304
1344
|
const result = await this.auth.loginWithEmail(this.email, this.password);
|
|
1305
1345
|
if (!result.success) {
|
|
1306
1346
|
this.error = result.message || 'Login failed';
|
|
1347
|
+
return;
|
|
1307
1348
|
}
|
|
1308
|
-
//
|
|
1349
|
+
// Authentication successful, now handle tenant selection
|
|
1350
|
+
await this.handlePostAuthFlow();
|
|
1309
1351
|
}
|
|
1310
1352
|
catch (err) {
|
|
1311
|
-
this.error = 'An unexpected error occurred';
|
|
1353
|
+
this.error = err.message || 'An unexpected error occurred';
|
|
1312
1354
|
}
|
|
1313
1355
|
finally {
|
|
1314
1356
|
this.loading = false;
|
|
@@ -1321,194 +1363,499 @@ class LoginDialogComponent {
|
|
|
1321
1363
|
const result = await this.auth.loginWithProvider(provider);
|
|
1322
1364
|
if (!result.success) {
|
|
1323
1365
|
this.error = result.message || 'OAuth login failed';
|
|
1366
|
+
return;
|
|
1324
1367
|
}
|
|
1325
|
-
//
|
|
1368
|
+
// Authentication successful, now handle tenant selection
|
|
1369
|
+
await this.handlePostAuthFlow();
|
|
1326
1370
|
}
|
|
1327
1371
|
catch (err) {
|
|
1328
|
-
this.error = 'An unexpected error occurred';
|
|
1372
|
+
this.error = err.message || 'An unexpected error occurred';
|
|
1329
1373
|
}
|
|
1330
1374
|
finally {
|
|
1331
1375
|
this.loading = false;
|
|
1332
1376
|
}
|
|
1333
1377
|
}
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
class="form-control">
|
|
1356
|
-
</div>
|
|
1357
|
-
<div class="form-group">
|
|
1358
|
-
<input
|
|
1359
|
-
[(ngModel)]="password"
|
|
1360
|
-
name="password"
|
|
1361
|
-
placeholder="Password"
|
|
1362
|
-
type="password"
|
|
1363
|
-
required
|
|
1364
|
-
class="form-control">
|
|
1365
|
-
</div>
|
|
1366
|
-
<button
|
|
1367
|
-
type="submit"
|
|
1368
|
-
[disabled]="loading"
|
|
1369
|
-
class="btn btn-primary btn-block">
|
|
1370
|
-
{{ loading ? 'Signing in...' : getProviderLabel('emailPassword') }}
|
|
1371
|
-
</button>
|
|
1372
|
-
</form>
|
|
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;
|
|
1373
1399
|
}
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
</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;
|
|
1380
1405
|
}
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
<div class="oauth-buttons">
|
|
1385
|
-
@for (provider of oauthProviders; track provider) {
|
|
1386
|
-
<button
|
|
1387
|
-
(click)="onOAuthLogin(provider)"
|
|
1388
|
-
[disabled]="loading"
|
|
1389
|
-
class="btn btn-oauth btn-{{ provider }}">
|
|
1390
|
-
@if (getProviderIcon(provider)) {
|
|
1391
|
-
<span class="oauth-icon">
|
|
1392
|
-
{{ getProviderIcon(provider) }}
|
|
1393
|
-
</span>
|
|
1394
|
-
}
|
|
1395
|
-
{{ getProviderLabel(provider) }}
|
|
1396
|
-
</button>
|
|
1397
|
-
}
|
|
1398
|
-
</div>
|
|
1406
|
+
// Auto-select if user has only one tenant
|
|
1407
|
+
if (this.memberships.length === 1 && this.autoSelectSingleTenant) {
|
|
1408
|
+
await this.selectAndContinue(this.memberships[0]);
|
|
1399
1409
|
}
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
<div class="error-message">
|
|
1404
|
-
{{ error }}
|
|
1405
|
-
</div>
|
|
1410
|
+
else {
|
|
1411
|
+
// Show tenant selector
|
|
1412
|
+
this.showingTenantSelector = true;
|
|
1406
1413
|
}
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
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;
|
|
1413
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>
|
|
1414
1493
|
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
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>
|
|
1428
1529
|
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
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
|
+
}
|
|
1440
1555
|
</div>
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
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 }}
|
|
1449
1571
|
</div>
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
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
|
+
}
|
|
1457
1644
|
}
|
|
1458
1645
|
|
|
1459
|
-
<!--
|
|
1460
|
-
@if (
|
|
1461
|
-
<div class="
|
|
1462
|
-
<
|
|
1646
|
+
<!-- Loading Overlay -->
|
|
1647
|
+
@if (loading) {
|
|
1648
|
+
<div class="loading-overlay">
|
|
1649
|
+
<div class="spinner"></div>
|
|
1463
1650
|
</div>
|
|
1464
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>
|
|
1465
1662
|
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
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>
|
|
1470
1691
|
<button
|
|
1471
|
-
|
|
1692
|
+
type="submit"
|
|
1472
1693
|
[disabled]="loading"
|
|
1473
|
-
class="btn btn-
|
|
1474
|
-
|
|
1475
|
-
<span class="oauth-icon">
|
|
1476
|
-
{{ getProviderIcon(provider) }}
|
|
1477
|
-
</span>
|
|
1478
|
-
}
|
|
1479
|
-
{{ getProviderLabel(provider) }}
|
|
1694
|
+
class="btn btn-primary btn-block">
|
|
1695
|
+
{{ loading ? 'Signing in...' : 'Sign in with Email' }}
|
|
1480
1696
|
</button>
|
|
1697
|
+
</form>
|
|
1698
|
+
|
|
1699
|
+
<!-- Divider -->
|
|
1700
|
+
@if (oauthProviders.length > 0) {
|
|
1701
|
+
<div class="divider">
|
|
1702
|
+
<span>OR</span>
|
|
1703
|
+
</div>
|
|
1481
1704
|
}
|
|
1482
|
-
|
|
1483
|
-
}
|
|
1705
|
+
}
|
|
1484
1706
|
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
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
|
+
}
|
|
1489
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
|
+
}
|
|
1490
1813
|
}
|
|
1491
1814
|
|
|
1492
|
-
<!-- Loading
|
|
1815
|
+
<!-- Loading Overlay -->
|
|
1493
1816
|
@if (loading) {
|
|
1494
1817
|
<div class="loading-overlay">
|
|
1495
1818
|
<div class="spinner"></div>
|
|
1496
1819
|
</div>
|
|
1497
1820
|
}
|
|
1498
|
-
|
|
1499
|
-
<!-- Register Link -->
|
|
1500
|
-
<div class="register-link">
|
|
1501
|
-
Don't have an account?
|
|
1502
|
-
<a href="#" (click)="onRegisterClick($event)">Sign up</a>
|
|
1503
|
-
</div>
|
|
1504
1821
|
</div>
|
|
1505
|
-
`, styles: [".login-dialog{padding:24px;max-width:
|
|
1506
|
-
}], ctorParameters: () => [{ type: AuthService }], propDecorators: {
|
|
1822
|
+
`, styles: [".tenant-login-dialog{padding:24px;max-width:450px;position:relative}.login-title{margin:0 0 24px;font-size:24px;font-weight:500;text-align:center}.welcome-message{margin-bottom:16px;padding:12px;background:#e8f5e9;border-radius:4px;text-align:center;font-size:14px;color:#2e7d32}.selector-description{margin-bottom:20px;font-size:14px;color:#666;text-align:center}.email-form,.form-group{margin-bottom:16px}.password-group{position:relative}.form-control{width:100%;padding:12px;border:1px solid #ddd;border-radius:4px;font-size:14px;box-sizing:border-box}.password-input{padding-right:45px}.password-toggle{position:absolute;right:8px;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;font-size:18px;padding:8px;line-height:1;opacity:.6;transition:opacity .2s}.password-toggle:hover{opacity:1}.password-toggle:focus{outline:2px solid #4285f4;outline-offset:2px;border-radius:4px}.form-control:focus{outline:none;border-color:#4285f4}.btn{padding:12px 24px;border:none;border-radius:4px;font-size:14px;font-weight:500;cursor:pointer;transition:background-color .2s}.btn:disabled{opacity:.6;cursor:not-allowed}.btn-block{width:100%}.btn-primary{background-color:#4285f4;color:#fff}.btn-primary:hover:not(:disabled){background-color:#357ae8}.divider{margin:16px 0;text-align:center;position:relative}.divider:before{content:\"\";position:absolute;top:50%;left:0;right:0;height:1px;background:#ddd}.divider span{background:#fff;padding:0 12px;position:relative;color:#666;font-size:12px}.oauth-buttons{display:flex;flex-direction:column;gap:12px}.btn-oauth{width:100%;background:#fff;color:#333;border:1px solid #ddd;display:flex;align-items:center;justify-content:center;gap:8px}.btn-oauth:hover:not(:disabled){background:#f8f8f8}.btn-google{border-color:#4285f4}.btn-linkedin{border-color:#0077b5}.btn-apple{border-color:#000}.btn-microsoft{border-color:#00a4ef}.btn-github{border-color:#333}.oauth-icon{font-size:18px}.switch-method{margin-top:12px;text-align:center;font-size:14px}.switch-method a{color:#4285f4;text-decoration:none}.switch-method a:hover{text-decoration:underline}.tenant-list{margin-bottom:20px;display:flex;flex-direction:column;gap:12px}.tenant-item{display:flex;align-items:flex-start;gap:12px;padding:16px;border:2px solid #e0e0e0;border-radius:6px;cursor:pointer;transition:all .2s}.tenant-item:hover{border-color:#4285f4;background:#f8f9ff}.tenant-item.selected{border-color:#4285f4;background:#e8f0fe}.tenant-radio{flex-shrink:0;padding-top:2px}.tenant-radio input[type=radio]{width:18px;height:18px;cursor:pointer}.tenant-info{flex:1}.tenant-name{font-size:16px;font-weight:500;color:#333;margin-bottom:4px}.tenant-meta{font-size:13px;color:#666}.tenant-role{font-weight:500;color:#4285f4}.tenant-separator{margin:0 6px}.error-message{margin-top:16px;padding:12px;background:#fee;color:#c33;border-radius:4px;font-size:14px}.loading-overlay{position:absolute;inset:0;background:#fffc;display:flex;align-items:center;justify-content:center}.spinner{width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #4285f4;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.register-link{margin-top:16px;text-align:center;font-size:14px;color:#666}.register-link a{color:#4285f4;text-decoration:none}.register-link a:hover{text-decoration:underline}.create-tenant-link{margin-top:16px;padding-top:16px;border-top:1px solid #e0e0e0;text-align:center;font-size:14px;color:#666}.create-tenant-link a{color:#4285f4;text-decoration:none;font-weight:500}.create-tenant-link a:hover{text-decoration:underline}\n"] }]
|
|
1823
|
+
}], ctorParameters: () => [{ type: AuthService }], propDecorators: { title: [{
|
|
1824
|
+
type: Input
|
|
1825
|
+
}], providers: [{
|
|
1826
|
+
type: Input
|
|
1827
|
+
}], showTenantSelector: [{
|
|
1828
|
+
type: Input
|
|
1829
|
+
}], autoSelectSingleTenant: [{
|
|
1830
|
+
type: Input
|
|
1831
|
+
}], prefillEmail: [{
|
|
1832
|
+
type: Input
|
|
1833
|
+
}], allowTenantCreation: [{
|
|
1834
|
+
type: Input
|
|
1835
|
+
}], tenantSelectorTitle: [{
|
|
1836
|
+
type: Input
|
|
1837
|
+
}], tenantSelectorDescription: [{
|
|
1838
|
+
type: Input
|
|
1839
|
+
}], continueButtonText: [{
|
|
1840
|
+
type: Input
|
|
1841
|
+
}], registerLinkText: [{
|
|
1842
|
+
type: Input
|
|
1843
|
+
}], registerLinkAction: [{
|
|
1844
|
+
type: Input
|
|
1845
|
+
}], createTenantLinkText: [{
|
|
1846
|
+
type: Input
|
|
1847
|
+
}], createTenantLinkAction: [{
|
|
1507
1848
|
type: Input
|
|
1849
|
+
}], tenantSelected: [{
|
|
1850
|
+
type: Output
|
|
1851
|
+
}], createTenant: [{
|
|
1852
|
+
type: Output
|
|
1508
1853
|
}] } });
|
|
1509
1854
|
|
|
1510
1855
|
class RegisterComponent {
|
|
1511
1856
|
auth;
|
|
1857
|
+
environment;
|
|
1858
|
+
navigateToLogin = new EventEmitter();
|
|
1512
1859
|
displayName = '';
|
|
1513
1860
|
email = '';
|
|
1514
1861
|
password = '';
|
|
@@ -1516,8 +1863,13 @@ class RegisterComponent {
|
|
|
1516
1863
|
error = '';
|
|
1517
1864
|
success = '';
|
|
1518
1865
|
loading = false;
|
|
1519
|
-
|
|
1866
|
+
showAccountLinkPrompt = false;
|
|
1867
|
+
existingEmail = '';
|
|
1868
|
+
showPassword = false;
|
|
1869
|
+
showConfirmPassword = false;
|
|
1870
|
+
constructor(auth, environment) {
|
|
1520
1871
|
this.auth = auth;
|
|
1872
|
+
this.environment = environment;
|
|
1521
1873
|
}
|
|
1522
1874
|
async onRegister() {
|
|
1523
1875
|
// Reset messages
|
|
@@ -1544,14 +1896,40 @@ class RegisterComponent {
|
|
|
1544
1896
|
}
|
|
1545
1897
|
this.loading = true;
|
|
1546
1898
|
try {
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
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
|
+
}
|
|
1552
1921
|
}
|
|
1553
1922
|
else {
|
|
1554
|
-
|
|
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
|
+
}
|
|
1555
1933
|
}
|
|
1556
1934
|
}
|
|
1557
1935
|
catch (err) {
|
|
@@ -1563,16 +1941,50 @@ class RegisterComponent {
|
|
|
1563
1941
|
}
|
|
1564
1942
|
onLoginClick(event) {
|
|
1565
1943
|
event.preventDefault();
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
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: `
|
|
1572
1961
|
<div class="register-dialog">
|
|
1573
1962
|
<h2 class="register-title">Create Account</h2>
|
|
1574
1963
|
|
|
1575
|
-
|
|
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">
|
|
1576
1988
|
<div class="form-group">
|
|
1577
1989
|
<label for="displayName">Full Name</label>
|
|
1578
1990
|
<input
|
|
@@ -1597,30 +2009,44 @@ class RegisterComponent {
|
|
|
1597
2009
|
class="form-control">
|
|
1598
2010
|
</div>
|
|
1599
2011
|
|
|
1600
|
-
<div class="form-group">
|
|
2012
|
+
<div class="form-group password-group">
|
|
1601
2013
|
<label for="password">Password</label>
|
|
1602
2014
|
<input
|
|
1603
2015
|
id="password"
|
|
1604
2016
|
[(ngModel)]="password"
|
|
1605
2017
|
name="password"
|
|
1606
2018
|
placeholder="Create a password"
|
|
1607
|
-
type="password"
|
|
2019
|
+
[type]="showPassword ? 'text' : 'password'"
|
|
1608
2020
|
required
|
|
1609
2021
|
minlength="8"
|
|
1610
|
-
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>
|
|
1611
2030
|
<small class="form-hint">At least 8 characters</small>
|
|
1612
2031
|
</div>
|
|
1613
2032
|
|
|
1614
|
-
<div class="form-group">
|
|
2033
|
+
<div class="form-group password-group">
|
|
1615
2034
|
<label for="confirmPassword">Confirm Password</label>
|
|
1616
2035
|
<input
|
|
1617
2036
|
id="confirmPassword"
|
|
1618
2037
|
[(ngModel)]="confirmPassword"
|
|
1619
2038
|
name="confirmPassword"
|
|
1620
2039
|
placeholder="Confirm your password"
|
|
1621
|
-
type="password"
|
|
2040
|
+
[type]="showConfirmPassword ? 'text' : 'password'"
|
|
1622
2041
|
required
|
|
1623
|
-
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>
|
|
1624
2050
|
</div>
|
|
1625
2051
|
|
|
1626
2052
|
<button
|
|
@@ -1632,7 +2058,7 @@ class RegisterComponent {
|
|
|
1632
2058
|
</form>
|
|
1633
2059
|
|
|
1634
2060
|
<!-- Error Message -->
|
|
1635
|
-
@if (error) {
|
|
2061
|
+
@if (error && !showAccountLinkPrompt) {
|
|
1636
2062
|
<div class="error-message">
|
|
1637
2063
|
{{ error }}
|
|
1638
2064
|
</div>
|
|
@@ -1653,12 +2079,12 @@ class RegisterComponent {
|
|
|
1653
2079
|
}
|
|
1654
2080
|
|
|
1655
2081
|
<!-- Login Link -->
|
|
1656
|
-
<div class="login-link">
|
|
2082
|
+
<div *ngIf="!showAccountLinkPrompt" class="login-link">
|
|
1657
2083
|
Already have an account?
|
|
1658
2084
|
<a href="#" (click)="onLoginClick($event)">Sign in</a>
|
|
1659
2085
|
</div>
|
|
1660
2086
|
</div>
|
|
1661
|
-
`, isInline: true, styles: [".register-dialog{padding:24px;max-width:400px;position:relative}.register-title{margin:0 0 24px;font-size:24px;font-weight:500;text-align:center}.register-form,.form-group{margin-bottom:16px}.form-group label{display:block;margin-bottom:6px;font-size:14px;font-weight:500;color:#333}.form-control{width:100%;padding:12px;border:1px solid #ddd;border-radius:4px;font-size:14px;box-sizing:border-box}.form-control:focus{outline:none;border-color:#4285f4}.form-hint{display:block;margin-top:4px;font-size:12px;color:#666}.btn{padding:12px 24px;border:none;border-radius:4px;font-size:14px;font-weight:500;cursor:pointer;transition:background-color .2s}.btn:disabled{opacity:.6;cursor:not-allowed}.btn-block{width:100%}.btn-primary{background-color:#4285f4;color:#fff}.btn-primary:hover:not(:disabled){background-color:#357ae8}.error-message{margin-top:16px;padding:12px;background:#fee;color:#c33;border-radius:4px;font-size:14px}.success-message{margin-top:16px;padding:12px;background:#efe;color:#3a3;border-radius:4px;font-size:14px}.loading-overlay{position:absolute;inset:0;background:#fffc;display:flex;align-items:center;justify-content:center}.spinner{width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #4285f4;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.login-link{margin-top:16px;text-align:center;font-size:14px;color:#666}.login-link a{color:#4285f4;text-decoration:none}.login-link a:hover{text-decoration:underline}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i2.MinLengthValidator, selector: "[minlength][formControlName],[minlength][formControl],[minlength][ngModel]", inputs: ["minlength"] }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i2.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }] });
|
|
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"] }] });
|
|
1662
2088
|
}
|
|
1663
2089
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: RegisterComponent, decorators: [{
|
|
1664
2090
|
type: Component,
|
|
@@ -1666,7 +2092,30 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
|
|
|
1666
2092
|
<div class="register-dialog">
|
|
1667
2093
|
<h2 class="register-title">Create Account</h2>
|
|
1668
2094
|
|
|
1669
|
-
|
|
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">
|
|
1670
2119
|
<div class="form-group">
|
|
1671
2120
|
<label for="displayName">Full Name</label>
|
|
1672
2121
|
<input
|
|
@@ -1691,30 +2140,44 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
|
|
|
1691
2140
|
class="form-control">
|
|
1692
2141
|
</div>
|
|
1693
2142
|
|
|
1694
|
-
<div class="form-group">
|
|
2143
|
+
<div class="form-group password-group">
|
|
1695
2144
|
<label for="password">Password</label>
|
|
1696
2145
|
<input
|
|
1697
2146
|
id="password"
|
|
1698
2147
|
[(ngModel)]="password"
|
|
1699
2148
|
name="password"
|
|
1700
2149
|
placeholder="Create a password"
|
|
1701
|
-
type="password"
|
|
2150
|
+
[type]="showPassword ? 'text' : 'password'"
|
|
1702
2151
|
required
|
|
1703
2152
|
minlength="8"
|
|
1704
|
-
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>
|
|
1705
2161
|
<small class="form-hint">At least 8 characters</small>
|
|
1706
2162
|
</div>
|
|
1707
2163
|
|
|
1708
|
-
<div class="form-group">
|
|
2164
|
+
<div class="form-group password-group">
|
|
1709
2165
|
<label for="confirmPassword">Confirm Password</label>
|
|
1710
2166
|
<input
|
|
1711
2167
|
id="confirmPassword"
|
|
1712
2168
|
[(ngModel)]="confirmPassword"
|
|
1713
2169
|
name="confirmPassword"
|
|
1714
2170
|
placeholder="Confirm your password"
|
|
1715
|
-
type="password"
|
|
2171
|
+
[type]="showConfirmPassword ? 'text' : 'password'"
|
|
1716
2172
|
required
|
|
1717
|
-
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>
|
|
1718
2181
|
</div>
|
|
1719
2182
|
|
|
1720
2183
|
<button
|
|
@@ -1726,7 +2189,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
|
|
|
1726
2189
|
</form>
|
|
1727
2190
|
|
|
1728
2191
|
<!-- Error Message -->
|
|
1729
|
-
@if (error) {
|
|
2192
|
+
@if (error && !showAccountLinkPrompt) {
|
|
1730
2193
|
<div class="error-message">
|
|
1731
2194
|
{{ error }}
|
|
1732
2195
|
</div>
|
|
@@ -1747,60 +2210,157 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
|
|
|
1747
2210
|
}
|
|
1748
2211
|
|
|
1749
2212
|
<!-- Login Link -->
|
|
1750
|
-
<div class="login-link">
|
|
2213
|
+
<div *ngIf="!showAccountLinkPrompt" class="login-link">
|
|
1751
2214
|
Already have an account?
|
|
1752
2215
|
<a href="#" (click)="onLoginClick($event)">Sign in</a>
|
|
1753
2216
|
</div>
|
|
1754
2217
|
</div>
|
|
1755
|
-
`, styles: [".register-dialog{padding:24px;max-width:400px;position:relative}.register-title{margin:0 0 24px;font-size:24px;font-weight:500;text-align:center}.register-form,.form-group{margin-bottom:16px}.form-group label{display:block;margin-bottom:6px;font-size:14px;font-weight:500;color:#333}.form-control{width:100%;padding:12px;border:1px solid #ddd;border-radius:4px;font-size:14px;box-sizing:border-box}.form-control:focus{outline:none;border-color:#4285f4}.form-hint{display:block;margin-top:4px;font-size:12px;color:#666}.btn{padding:12px 24px;border:none;border-radius:4px;font-size:14px;font-weight:500;cursor:pointer;transition:background-color .2s}.btn:disabled{opacity:.6;cursor:not-allowed}.btn-block{width:100%}.btn-primary{background-color:#4285f4;color:#fff}.btn-primary:hover:not(:disabled){background-color:#357ae8}.error-message{margin-top:16px;padding:12px;background:#fee;color:#c33;border-radius:4px;font-size:14px}.success-message{margin-top:16px;padding:12px;background:#efe;color:#3a3;border-radius:4px;font-size:14px}.loading-overlay{position:absolute;inset:0;background:#fffc;display:flex;align-items:center;justify-content:center}.spinner{width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #4285f4;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.login-link{margin-top:16px;text-align:center;font-size:14px;color:#666}.login-link a{color:#4285f4;text-decoration:none}.login-link a:hover{text-decoration:underline}\n"] }]
|
|
1756
|
-
}], ctorParameters: () => [{ type: AuthService }
|
|
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
|
+
}] } });
|
|
1757
2225
|
|
|
1758
|
-
class
|
|
2226
|
+
class AuthPageComponent {
|
|
2227
|
+
environment;
|
|
2228
|
+
providers = ['google', 'emailPassword'];
|
|
2229
|
+
authenticated = new EventEmitter();
|
|
2230
|
+
mode = 'login';
|
|
2231
|
+
appName = '';
|
|
2232
|
+
logo;
|
|
2233
|
+
subtitle;
|
|
2234
|
+
gradientStyle = 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)';
|
|
2235
|
+
constructor(environment) {
|
|
2236
|
+
this.environment = environment;
|
|
2237
|
+
}
|
|
2238
|
+
ngOnInit() {
|
|
2239
|
+
const branding = this.environment.branding;
|
|
2240
|
+
if (branding) {
|
|
2241
|
+
this.appName = branding.appName || 'Sign In';
|
|
2242
|
+
this.logo = branding.logo;
|
|
2243
|
+
this.subtitle = branding.subtitle;
|
|
2244
|
+
if (branding.gradientStart && branding.gradientEnd) {
|
|
2245
|
+
this.gradientStyle = `linear-gradient(135deg, ${branding.gradientStart} 0%, ${branding.gradientEnd} 100%)`;
|
|
2246
|
+
}
|
|
2247
|
+
else if (branding.primaryColor) {
|
|
2248
|
+
const color = branding.primaryColor;
|
|
2249
|
+
this.gradientStyle = `linear-gradient(135deg, ${color} 0%, ${this.adjustColor(color, -20)} 100%)`;
|
|
2250
|
+
}
|
|
2251
|
+
}
|
|
2252
|
+
else {
|
|
2253
|
+
this.appName = 'Sign In';
|
|
2254
|
+
}
|
|
2255
|
+
}
|
|
2256
|
+
onAuthenticated(event) {
|
|
2257
|
+
this.authenticated.emit(event);
|
|
2258
|
+
}
|
|
2259
|
+
/**
|
|
2260
|
+
* Adjust color brightness (simple implementation)
|
|
2261
|
+
* @param color Hex color (e.g., '#667eea')
|
|
2262
|
+
* @param percent Percentage to darken (negative) or lighten (positive)
|
|
2263
|
+
*/
|
|
2264
|
+
adjustColor(color, percent) {
|
|
2265
|
+
const num = parseInt(color.replace('#', ''), 16);
|
|
2266
|
+
const amt = Math.round(2.55 * percent);
|
|
2267
|
+
const R = (num >> 16) + amt;
|
|
2268
|
+
const G = (num >> 8 & 0x00FF) + amt;
|
|
2269
|
+
const B = (num & 0x0000FF) + amt;
|
|
2270
|
+
return '#' + (0x1000000 + (R < 255 ? R < 1 ? 0 : R : 255) * 0x10000 +
|
|
2271
|
+
(G < 255 ? G < 1 ? 0 : G : 255) * 0x100 +
|
|
2272
|
+
(B < 255 ? B < 1 ? 0 : B : 255))
|
|
2273
|
+
.toString(16).slice(1);
|
|
2274
|
+
}
|
|
2275
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: AuthPageComponent, deps: [{ token: MyEnvironmentModel }], target: i0.ɵɵFactoryTarget.Component });
|
|
2276
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.15", type: AuthPageComponent, isStandalone: true, selector: "lib-auth-page", inputs: { providers: "providers" }, outputs: { authenticated: "authenticated" }, ngImport: i0, template: `
|
|
2277
|
+
<div class="auth-container" [style.background]="gradientStyle">
|
|
2278
|
+
<div class="auth-card">
|
|
2279
|
+
@if (logo) {
|
|
2280
|
+
<img [src]="logo" [alt]="appName + ' logo'" class="logo">
|
|
2281
|
+
}
|
|
2282
|
+
<h1 class="app-name">{{ appName }}</h1>
|
|
2283
|
+
@if (subtitle) {
|
|
2284
|
+
<p class="subtitle">{{ subtitle }}</p>
|
|
2285
|
+
}
|
|
2286
|
+
|
|
2287
|
+
@if (mode === 'login') {
|
|
2288
|
+
<lib-tenant-login
|
|
2289
|
+
[providers]="providers"
|
|
2290
|
+
[allowTenantCreation]="false"
|
|
2291
|
+
(tenantSelected)="onAuthenticated($event)"
|
|
2292
|
+
(createTenant)="mode = 'register'">
|
|
2293
|
+
</lib-tenant-login>
|
|
2294
|
+
} @else {
|
|
2295
|
+
<lib-register
|
|
2296
|
+
(navigateToLogin)="mode = 'login'">
|
|
2297
|
+
</lib-register>
|
|
2298
|
+
}
|
|
2299
|
+
</div>
|
|
2300
|
+
</div>
|
|
2301
|
+
`, isInline: true, styles: [".auth-container{min-height:100vh;display:flex;align-items:center;justify-content:center;padding:20px;background:linear-gradient(135deg,#667eea,#764ba2)}.auth-card{background:#fff;border-radius:12px;box-shadow:0 10px 40px #0000001a;padding:40px;width:100%;max-width:480px}.logo{display:block;max-width:200px;max-height:80px;margin:0 auto 24px}.app-name{margin:0 0 12px;font-size:28px;font-weight:600;text-align:center;color:#1a202c}.subtitle{margin:0 0 32px;font-size:16px;text-align:center;color:#718096}:host ::ng-deep .tenant-login-dialog,:host ::ng-deep .register-dialog{padding:0;max-width:none}:host ::ng-deep .login-title,:host ::ng-deep .register-title{display:none}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: TenantLoginComponent, selector: "lib-tenant-login", inputs: ["title", "providers", "showTenantSelector", "autoSelectSingleTenant", "prefillEmail", "allowTenantCreation", "tenantSelectorTitle", "tenantSelectorDescription", "continueButtonText", "registerLinkText", "registerLinkAction", "createTenantLinkText", "createTenantLinkAction"], outputs: ["tenantSelected", "createTenant"] }, { kind: "component", type: RegisterComponent, selector: "lib-register", outputs: ["navigateToLogin"] }] });
|
|
2302
|
+
}
|
|
2303
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: AuthPageComponent, decorators: [{
|
|
2304
|
+
type: Component,
|
|
2305
|
+
args: [{ selector: 'lib-auth-page', standalone: true, imports: [CommonModule, TenantLoginComponent, RegisterComponent], template: `
|
|
2306
|
+
<div class="auth-container" [style.background]="gradientStyle">
|
|
2307
|
+
<div class="auth-card">
|
|
2308
|
+
@if (logo) {
|
|
2309
|
+
<img [src]="logo" [alt]="appName + ' logo'" class="logo">
|
|
2310
|
+
}
|
|
2311
|
+
<h1 class="app-name">{{ appName }}</h1>
|
|
2312
|
+
@if (subtitle) {
|
|
2313
|
+
<p class="subtitle">{{ subtitle }}</p>
|
|
2314
|
+
}
|
|
2315
|
+
|
|
2316
|
+
@if (mode === 'login') {
|
|
2317
|
+
<lib-tenant-login
|
|
2318
|
+
[providers]="providers"
|
|
2319
|
+
[allowTenantCreation]="false"
|
|
2320
|
+
(tenantSelected)="onAuthenticated($event)"
|
|
2321
|
+
(createTenant)="mode = 'register'">
|
|
2322
|
+
</lib-tenant-login>
|
|
2323
|
+
} @else {
|
|
2324
|
+
<lib-register
|
|
2325
|
+
(navigateToLogin)="mode = 'login'">
|
|
2326
|
+
</lib-register>
|
|
2327
|
+
}
|
|
2328
|
+
</div>
|
|
2329
|
+
</div>
|
|
2330
|
+
`, styles: [".auth-container{min-height:100vh;display:flex;align-items:center;justify-content:center;padding:20px;background:linear-gradient(135deg,#667eea,#764ba2)}.auth-card{background:#fff;border-radius:12px;box-shadow:0 10px 40px #0000001a;padding:40px;width:100%;max-width:480px}.logo{display:block;max-width:200px;max-height:80px;margin:0 auto 24px}.app-name{margin:0 0 12px;font-size:28px;font-weight:600;text-align:center;color:#1a202c}.subtitle{margin:0 0 32px;font-size:16px;text-align:center;color:#718096}:host ::ng-deep .tenant-login-dialog,:host ::ng-deep .register-dialog{padding:0;max-width:none}:host ::ng-deep .login-title,:host ::ng-deep .register-title{display:none}\n"] }]
|
|
2331
|
+
}], ctorParameters: () => [{ type: MyEnvironmentModel, decorators: [{
|
|
2332
|
+
type: Inject,
|
|
2333
|
+
args: [MyEnvironmentModel]
|
|
2334
|
+
}] }], propDecorators: { providers: [{
|
|
2335
|
+
type: Input
|
|
2336
|
+
}], authenticated: [{
|
|
2337
|
+
type: Output
|
|
2338
|
+
}] } });
|
|
2339
|
+
|
|
2340
|
+
class LoginDialogComponent {
|
|
1759
2341
|
auth;
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
allowTenantCreation = true;
|
|
1766
|
-
// Tenant Selector Labels
|
|
1767
|
-
tenantSelectorTitle = 'Select Organization';
|
|
1768
|
-
tenantSelectorDescription = 'Choose which organization you want to access:';
|
|
1769
|
-
continueButtonText = 'Continue';
|
|
1770
|
-
// Link Labels
|
|
1771
|
-
registerLinkText = "Don't have an account?";
|
|
1772
|
-
registerLinkAction = 'Sign up';
|
|
1773
|
-
createTenantLinkText = "Don't see your organization?";
|
|
1774
|
-
createTenantLinkAction = 'Create New Organization';
|
|
1775
|
-
// Outputs
|
|
1776
|
-
tenantSelected = new EventEmitter();
|
|
1777
|
-
createTenant = new EventEmitter();
|
|
1778
|
-
// Form Fields
|
|
2342
|
+
/**
|
|
2343
|
+
* REQUIRED: Which authentication providers to show in this dialog
|
|
2344
|
+
* @example ['google', 'linkedin', 'emailPassword']
|
|
2345
|
+
*/
|
|
2346
|
+
providers = [];
|
|
1779
2347
|
email = '';
|
|
1780
2348
|
password = '';
|
|
1781
|
-
// State
|
|
1782
2349
|
error = '';
|
|
1783
2350
|
loading = false;
|
|
1784
|
-
|
|
2351
|
+
showPassword = false;
|
|
1785
2352
|
oauthProviders = [];
|
|
1786
|
-
// Tenant Selection State
|
|
1787
|
-
showingTenantSelector = false;
|
|
1788
|
-
memberships = [];
|
|
1789
|
-
selectedTenantId = null;
|
|
1790
|
-
userName = '';
|
|
1791
2353
|
constructor(auth) {
|
|
1792
2354
|
this.auth = auth;
|
|
1793
2355
|
}
|
|
1794
2356
|
ngOnInit() {
|
|
1795
2357
|
if (!this.providers || this.providers.length === 0) {
|
|
1796
|
-
this.error = 'Configuration Error: No authentication providers specified.';
|
|
1797
|
-
throw new Error('
|
|
1798
|
-
}
|
|
1799
|
-
this.oauthProviders = this.providers.filter(p => p !== 'emailPassword');
|
|
1800
|
-
// If only emailPassword is available, use it by default
|
|
1801
|
-
if (this.oauthProviders.length === 0 && this.isProviderEnabled('emailPassword')) {
|
|
1802
|
-
this.useOAuth = false;
|
|
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\']');
|
|
1803
2360
|
}
|
|
2361
|
+
// Get OAuth providers (excluding emailPassword)
|
|
2362
|
+
this.oauthProviders = this.providers
|
|
2363
|
+
.filter(p => p !== 'emailPassword');
|
|
1804
2364
|
}
|
|
1805
2365
|
isProviderEnabled(provider) {
|
|
1806
2366
|
return this.providers.includes(provider);
|
|
@@ -1817,514 +2377,235 @@ class TenantLoginComponent {
|
|
|
1817
2377
|
return labels[provider];
|
|
1818
2378
|
}
|
|
1819
2379
|
getProviderIcon(provider) {
|
|
1820
|
-
|
|
1821
|
-
|
|
1822
|
-
|
|
1823
|
-
|
|
1824
|
-
this.
|
|
1825
|
-
|
|
1826
|
-
}
|
|
1827
|
-
async onEmailLogin() {
|
|
1828
|
-
if (!this.email || !this.password) {
|
|
1829
|
-
this.error = 'Please enter email and password';
|
|
1830
|
-
return;
|
|
1831
|
-
}
|
|
1832
|
-
this.loading = true;
|
|
1833
|
-
this.error = '';
|
|
1834
|
-
try {
|
|
1835
|
-
const result = await this.auth.loginWithEmail(this.email, this.password);
|
|
1836
|
-
if (!result.success) {
|
|
1837
|
-
this.error = result.message || 'Login failed';
|
|
1838
|
-
return;
|
|
1839
|
-
}
|
|
1840
|
-
// Authentication successful, now handle tenant selection
|
|
1841
|
-
await this.handlePostAuthFlow();
|
|
1842
|
-
}
|
|
1843
|
-
catch (err) {
|
|
1844
|
-
this.error = err.message || 'An unexpected error occurred';
|
|
1845
|
-
}
|
|
1846
|
-
finally {
|
|
1847
|
-
this.loading = false;
|
|
1848
|
-
}
|
|
1849
|
-
}
|
|
1850
|
-
async onOAuthLogin(provider) {
|
|
1851
|
-
this.loading = true;
|
|
1852
|
-
this.error = '';
|
|
1853
|
-
try {
|
|
1854
|
-
const result = await this.auth.loginWithProvider(provider);
|
|
1855
|
-
if (!result.success) {
|
|
1856
|
-
this.error = result.message || 'OAuth login failed';
|
|
1857
|
-
return;
|
|
1858
|
-
}
|
|
1859
|
-
// Authentication successful, now handle tenant selection
|
|
1860
|
-
await this.handlePostAuthFlow();
|
|
1861
|
-
}
|
|
1862
|
-
catch (err) {
|
|
1863
|
-
this.error = err.message || 'An unexpected error occurred';
|
|
1864
|
-
}
|
|
1865
|
-
finally {
|
|
1866
|
-
this.loading = false;
|
|
1867
|
-
}
|
|
1868
|
-
}
|
|
1869
|
-
async handlePostAuthFlow() {
|
|
1870
|
-
if (!this.showTenantSelector) {
|
|
1871
|
-
// Tenant selection is disabled, emit event immediately
|
|
1872
|
-
this.tenantSelected.emit({
|
|
1873
|
-
tenantId: '',
|
|
1874
|
-
tenantSlug: '',
|
|
1875
|
-
role: ''
|
|
1876
|
-
});
|
|
1877
|
-
return;
|
|
1878
|
-
}
|
|
1879
|
-
// Fetch user's tenant memberships
|
|
1880
|
-
this.loading = true;
|
|
1881
|
-
try {
|
|
1882
|
-
const result = await this.auth.getTenantMemberships();
|
|
1883
|
-
if (!result.memberships || result.memberships.length === 0) {
|
|
1884
|
-
// User has no tenants, prompt to create one
|
|
1885
|
-
this.error = 'You are not a member of any organization. Please create one.';
|
|
1886
|
-
if (this.allowTenantCreation) {
|
|
1887
|
-
setTimeout(() => this.createTenant.emit(), 2000);
|
|
1888
|
-
}
|
|
1889
|
-
return;
|
|
1890
|
-
}
|
|
1891
|
-
this.memberships = result.memberships;
|
|
1892
|
-
// Get user name if available
|
|
1893
|
-
const currentUser = this.auth.getCurrentUser();
|
|
1894
|
-
if (currentUser) {
|
|
1895
|
-
this.userName = currentUser.display_name || currentUser.email;
|
|
1896
|
-
}
|
|
1897
|
-
// Auto-select if user has only one tenant
|
|
1898
|
-
if (this.memberships.length === 1 && this.autoSelectSingleTenant) {
|
|
1899
|
-
await this.selectAndContinue(this.memberships[0]);
|
|
1900
|
-
}
|
|
1901
|
-
else {
|
|
1902
|
-
// Show tenant selector
|
|
1903
|
-
this.showingTenantSelector = true;
|
|
1904
|
-
}
|
|
1905
|
-
}
|
|
1906
|
-
catch (err) {
|
|
1907
|
-
this.error = err.message || 'Failed to load organizations';
|
|
1908
|
-
}
|
|
1909
|
-
finally {
|
|
1910
|
-
this.loading = false;
|
|
1911
|
-
}
|
|
1912
|
-
}
|
|
1913
|
-
selectTenantItem(tenantId) {
|
|
1914
|
-
this.selectedTenantId = tenantId;
|
|
1915
|
-
}
|
|
1916
|
-
async onContinueWithTenant() {
|
|
1917
|
-
if (!this.selectedTenantId) {
|
|
1918
|
-
this.error = 'Please select an organization';
|
|
1919
|
-
return;
|
|
1920
|
-
}
|
|
1921
|
-
const membership = this.memberships.find(m => m.tenant_id === this.selectedTenantId);
|
|
1922
|
-
if (!membership) {
|
|
1923
|
-
this.error = 'Selected organization not found';
|
|
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';
|
|
1924
2386
|
return;
|
|
1925
2387
|
}
|
|
1926
|
-
await this.selectAndContinue(membership);
|
|
1927
|
-
}
|
|
1928
|
-
async selectAndContinue(membership) {
|
|
1929
2388
|
this.loading = true;
|
|
1930
2389
|
this.error = '';
|
|
1931
2390
|
try {
|
|
1932
|
-
const result = await this.auth.
|
|
2391
|
+
const result = await this.auth.loginWithEmail(this.email, this.password);
|
|
1933
2392
|
if (!result.success) {
|
|
1934
|
-
this.error = result.message || '
|
|
1935
|
-
return;
|
|
2393
|
+
this.error = result.message || 'Login failed';
|
|
1936
2394
|
}
|
|
1937
|
-
//
|
|
1938
|
-
this.tenantSelected.emit({
|
|
1939
|
-
tenantId: membership.tenant_id,
|
|
1940
|
-
tenantSlug: membership.slug,
|
|
1941
|
-
role: membership.role
|
|
1942
|
-
});
|
|
2395
|
+
// On success, parent component/dialog should close automatically via user$ subscription
|
|
1943
2396
|
}
|
|
1944
2397
|
catch (err) {
|
|
1945
|
-
this.error =
|
|
2398
|
+
this.error = 'An unexpected error occurred';
|
|
1946
2399
|
}
|
|
1947
2400
|
finally {
|
|
1948
2401
|
this.loading = false;
|
|
1949
2402
|
}
|
|
1950
2403
|
}
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
formatLastAccessed(dateStr) {
|
|
2404
|
+
async onOAuthLogin(provider) {
|
|
2405
|
+
this.loading = true;
|
|
2406
|
+
this.error = '';
|
|
1955
2407
|
try {
|
|
1956
|
-
const
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
return 'today';
|
|
1962
|
-
if (diffDays === 1)
|
|
1963
|
-
return 'yesterday';
|
|
1964
|
-
if (diffDays < 7)
|
|
1965
|
-
return `${diffDays} days ago`;
|
|
1966
|
-
if (diffDays < 30)
|
|
1967
|
-
return `${Math.floor(diffDays / 7)} weeks ago`;
|
|
1968
|
-
return `${Math.floor(diffDays / 30)} months ago`;
|
|
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
|
|
1969
2413
|
}
|
|
1970
|
-
catch {
|
|
1971
|
-
|
|
2414
|
+
catch (err) {
|
|
2415
|
+
this.error = 'An unexpected error occurred';
|
|
2416
|
+
}
|
|
2417
|
+
finally {
|
|
2418
|
+
this.loading = false;
|
|
1972
2419
|
}
|
|
1973
2420
|
}
|
|
1974
|
-
|
|
2421
|
+
onRegisterClick(event) {
|
|
1975
2422
|
event.preventDefault();
|
|
1976
|
-
this
|
|
2423
|
+
// Platforms can override this or listen for a custom event
|
|
2424
|
+
// For now, just emit a console message
|
|
2425
|
+
console.log('Register clicked - platform should handle navigation');
|
|
1977
2426
|
}
|
|
1978
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type:
|
|
1979
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.15", type:
|
|
1980
|
-
<div class="
|
|
1981
|
-
|
|
1982
|
-
<!-- Step 1: Authentication -->
|
|
1983
|
-
<h2 class="login-title">{{ title }}</h2>
|
|
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>
|
|
1984
2431
|
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
1999
|
-
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
</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">
|
|
2006
2452
|
<button
|
|
2007
|
-
type="
|
|
2008
|
-
|
|
2009
|
-
|
|
2010
|
-
|
|
2453
|
+
type="button"
|
|
2454
|
+
class="password-toggle"
|
|
2455
|
+
(click)="showPassword = !showPassword"
|
|
2456
|
+
[attr.aria-label]="showPassword ? 'Hide password' : 'Show password'">
|
|
2457
|
+
{{ showPassword ? '👁️' : '👁️🗨️' }}
|
|
2011
2458
|
</button>
|
|
2012
|
-
</form>
|
|
2013
|
-
|
|
2014
|
-
<!-- Divider -->
|
|
2015
|
-
@if (oauthProviders.length > 0) {
|
|
2016
|
-
<div class="divider">
|
|
2017
|
-
<span>OR</span>
|
|
2018
|
-
</div>
|
|
2019
|
-
}
|
|
2020
|
-
}
|
|
2021
|
-
|
|
2022
|
-
<!-- OAuth Providers -->
|
|
2023
|
-
@if (oauthProviders.length > 0 && (useOAuth || !isProviderEnabled('emailPassword'))) {
|
|
2024
|
-
<div class="oauth-buttons">
|
|
2025
|
-
@for (provider of oauthProviders; track provider) {
|
|
2026
|
-
<button
|
|
2027
|
-
type="button"
|
|
2028
|
-
(click)="onOAuthLogin(provider)"
|
|
2029
|
-
[disabled]="loading"
|
|
2030
|
-
class="btn btn-oauth btn-{{ provider }}">
|
|
2031
|
-
@if (getProviderIcon(provider)) {
|
|
2032
|
-
<span class="oauth-icon">
|
|
2033
|
-
{{ getProviderIcon(provider) }}
|
|
2034
|
-
</span>
|
|
2035
|
-
}
|
|
2036
|
-
{{ getProviderLabel(provider) }}
|
|
2037
|
-
</button>
|
|
2038
|
-
}
|
|
2039
|
-
</div>
|
|
2040
|
-
|
|
2041
|
-
<!-- Switch to Email/Password -->
|
|
2042
|
-
@if (isProviderEnabled('emailPassword') && oauthProviders.length > 0) {
|
|
2043
|
-
<div class="switch-method">
|
|
2044
|
-
<a href="#" (click)="toggleAuthMethod($event)">
|
|
2045
|
-
{{ useOAuth ? 'Use email/password instead' : 'Use OAuth instead' }}
|
|
2046
|
-
</a>
|
|
2047
|
-
</div>
|
|
2048
|
-
}
|
|
2049
|
-
}
|
|
2050
|
-
|
|
2051
|
-
<!-- Error Message -->
|
|
2052
|
-
@if (error) {
|
|
2053
|
-
<div class="error-message">
|
|
2054
|
-
{{ error }}
|
|
2055
|
-
</div>
|
|
2056
|
-
}
|
|
2057
|
-
|
|
2058
|
-
<!-- Register Link -->
|
|
2059
|
-
@if (allowTenantCreation) {
|
|
2060
|
-
<div class="register-link">
|
|
2061
|
-
{{ registerLinkText }}
|
|
2062
|
-
<a href="#" (click)="onCreateTenantClick($event)">{{ registerLinkAction }}</a>
|
|
2063
|
-
</div>
|
|
2064
|
-
}
|
|
2065
|
-
} @else {
|
|
2066
|
-
<!-- Step 2: Tenant Selection -->
|
|
2067
|
-
<h2 class="login-title">{{ tenantSelectorTitle }}</h2>
|
|
2068
|
-
|
|
2069
|
-
@if (userName) {
|
|
2070
|
-
<div class="welcome-message">
|
|
2071
|
-
Welcome back, <strong>{{ userName }}</strong>!
|
|
2072
2459
|
</div>
|
|
2073
|
-
|
|
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
|
+
}
|
|
2074
2468
|
|
|
2075
|
-
|
|
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
|
+
}
|
|
2076
2475
|
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
(click)="
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
<div class="tenant-meta">
|
|
2093
|
-
<span class="tenant-role">{{ formatRole(membership.role) }}</span>
|
|
2094
|
-
@if (membership.last_accessed) {
|
|
2095
|
-
<span class="tenant-separator">·</span>
|
|
2096
|
-
<span class="tenant-last-accessed">
|
|
2097
|
-
Last accessed {{ formatLastAccessed(membership.last_accessed) }}
|
|
2098
|
-
</span>
|
|
2099
|
-
}
|
|
2100
|
-
</div>
|
|
2101
|
-
</div>
|
|
2102
|
-
</div>
|
|
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>
|
|
2103
2491
|
}
|
|
2104
2492
|
</div>
|
|
2493
|
+
}
|
|
2105
2494
|
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
2109
|
-
|
|
2110
|
-
|
|
2111
|
-
{{ loading ? 'Loading...' : continueButtonText }}
|
|
2112
|
-
</button>
|
|
2113
|
-
|
|
2114
|
-
<!-- Error Message -->
|
|
2115
|
-
@if (error) {
|
|
2116
|
-
<div class="error-message">
|
|
2117
|
-
{{ error }}
|
|
2118
|
-
</div>
|
|
2119
|
-
}
|
|
2120
|
-
|
|
2121
|
-
<!-- Create New Tenant Link -->
|
|
2122
|
-
@if (allowTenantCreation) {
|
|
2123
|
-
<div class="create-tenant-link">
|
|
2124
|
-
{{ createTenantLinkText }}
|
|
2125
|
-
<a href="#" (click)="onCreateTenantClick($event)">{{ createTenantLinkAction }}</a>
|
|
2126
|
-
</div>
|
|
2127
|
-
}
|
|
2495
|
+
<!-- Error Message -->
|
|
2496
|
+
@if (error) {
|
|
2497
|
+
<div class="error-message">
|
|
2498
|
+
{{ error }}
|
|
2499
|
+
</div>
|
|
2128
2500
|
}
|
|
2129
2501
|
|
|
2130
|
-
<!-- Loading
|
|
2502
|
+
<!-- Loading State -->
|
|
2131
2503
|
@if (loading) {
|
|
2132
2504
|
<div class="loading-overlay">
|
|
2133
2505
|
<div class="spinner"></div>
|
|
2134
2506
|
</div>
|
|
2135
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>
|
|
2136
2514
|
</div>
|
|
2137
|
-
`, isInline: true, styles: [".
|
|
2515
|
+
`, isInline: true, styles: [".login-dialog{padding:24px;max-width:400px;position:relative}.login-title{margin:0 0 24px;font-size:24px;font-weight:500;text-align:center}.email-form,.form-group{margin-bottom:16px}.password-group{position:relative}.form-control{width:100%;padding:12px;border:1px solid #ddd;border-radius:4px;font-size:14px;box-sizing:border-box}.password-input{padding-right:45px}.password-toggle{position:absolute;right:8px;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;font-size:18px;padding:8px;line-height:1;opacity:.6;transition:opacity .2s}.password-toggle:hover{opacity:1}.password-toggle:focus{outline:2px solid #4285f4;outline-offset:2px;border-radius:4px}.form-control:focus{outline:none;border-color:#4285f4}.btn{padding:12px 24px;border:none;border-radius:4px;font-size:14px;font-weight:500;cursor:pointer;transition:background-color .2s}.btn:disabled{opacity:.6;cursor:not-allowed}.btn-block{width:100%}.btn-primary{background-color:#4285f4;color:#fff}.btn-primary:hover:not(:disabled){background-color:#357ae8}.divider{margin:16px 0;text-align:center;position:relative}.divider:before{content:\"\";position:absolute;top:50%;left:0;right:0;height:1px;background:#ddd}.divider span{background:#fff;padding:0 12px;position:relative;color:#666;font-size:12px}.oauth-buttons{display:flex;flex-direction:column;gap:12px}.btn-oauth{width:100%;background:#fff;color:#333;border:1px solid #ddd;display:flex;align-items:center;justify-content:center;gap:8px}.btn-oauth:hover:not(:disabled){background:#f8f8f8}.btn-google{border-color:#4285f4}.btn-linkedin{border-color:#0077b5}.btn-apple{border-color:#000}.btn-microsoft{border-color:#00a4ef}.btn-github{border-color:#333}.oauth-icon{font-size:18px}.error-message{margin-top:16px;padding:12px;background:#fee;color:#c33;border-radius:4px;font-size:14px}.loading-overlay{position:absolute;inset:0;background:#fffc;display:flex;align-items:center;justify-content:center}.spinner{width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #4285f4;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.register-link{margin-top:16px;text-align:center;font-size:14px;color:#666}.register-link a{color:#4285f4;text-decoration:none}.register-link a:hover{text-decoration:underline}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i2.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }] });
|
|
2138
2516
|
}
|
|
2139
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type:
|
|
2517
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: LoginDialogComponent, decorators: [{
|
|
2140
2518
|
type: Component,
|
|
2141
|
-
args: [{ selector: 'lib-
|
|
2142
|
-
<div class="
|
|
2143
|
-
|
|
2144
|
-
<!-- Step 1: Authentication -->
|
|
2145
|
-
<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>
|
|
2146
2522
|
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
</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">
|
|
2168
2543
|
<button
|
|
2169
|
-
type="
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2544
|
+
type="button"
|
|
2545
|
+
class="password-toggle"
|
|
2546
|
+
(click)="showPassword = !showPassword"
|
|
2547
|
+
[attr.aria-label]="showPassword ? 'Hide password' : 'Show password'">
|
|
2548
|
+
{{ showPassword ? '👁️' : '👁️🗨️' }}
|
|
2173
2549
|
</button>
|
|
2174
|
-
</form>
|
|
2175
|
-
|
|
2176
|
-
<!-- Divider -->
|
|
2177
|
-
@if (oauthProviders.length > 0) {
|
|
2178
|
-
<div class="divider">
|
|
2179
|
-
<span>OR</span>
|
|
2180
|
-
</div>
|
|
2181
|
-
}
|
|
2182
|
-
}
|
|
2183
|
-
|
|
2184
|
-
<!-- OAuth Providers -->
|
|
2185
|
-
@if (oauthProviders.length > 0 && (useOAuth || !isProviderEnabled('emailPassword'))) {
|
|
2186
|
-
<div class="oauth-buttons">
|
|
2187
|
-
@for (provider of oauthProviders; track provider) {
|
|
2188
|
-
<button
|
|
2189
|
-
type="button"
|
|
2190
|
-
(click)="onOAuthLogin(provider)"
|
|
2191
|
-
[disabled]="loading"
|
|
2192
|
-
class="btn btn-oauth btn-{{ provider }}">
|
|
2193
|
-
@if (getProviderIcon(provider)) {
|
|
2194
|
-
<span class="oauth-icon">
|
|
2195
|
-
{{ getProviderIcon(provider) }}
|
|
2196
|
-
</span>
|
|
2197
|
-
}
|
|
2198
|
-
{{ getProviderLabel(provider) }}
|
|
2199
|
-
</button>
|
|
2200
|
-
}
|
|
2201
|
-
</div>
|
|
2202
|
-
|
|
2203
|
-
<!-- Switch to Email/Password -->
|
|
2204
|
-
@if (isProviderEnabled('emailPassword') && oauthProviders.length > 0) {
|
|
2205
|
-
<div class="switch-method">
|
|
2206
|
-
<a href="#" (click)="toggleAuthMethod($event)">
|
|
2207
|
-
{{ useOAuth ? 'Use email/password instead' : 'Use OAuth instead' }}
|
|
2208
|
-
</a>
|
|
2209
|
-
</div>
|
|
2210
|
-
}
|
|
2211
|
-
}
|
|
2212
|
-
|
|
2213
|
-
<!-- Error Message -->
|
|
2214
|
-
@if (error) {
|
|
2215
|
-
<div class="error-message">
|
|
2216
|
-
{{ error }}
|
|
2217
|
-
</div>
|
|
2218
|
-
}
|
|
2219
|
-
|
|
2220
|
-
<!-- Register Link -->
|
|
2221
|
-
@if (allowTenantCreation) {
|
|
2222
|
-
<div class="register-link">
|
|
2223
|
-
{{ registerLinkText }}
|
|
2224
|
-
<a href="#" (click)="onCreateTenantClick($event)">{{ registerLinkAction }}</a>
|
|
2225
|
-
</div>
|
|
2226
|
-
}
|
|
2227
|
-
} @else {
|
|
2228
|
-
<!-- Step 2: Tenant Selection -->
|
|
2229
|
-
<h2 class="login-title">{{ tenantSelectorTitle }}</h2>
|
|
2230
|
-
|
|
2231
|
-
@if (userName) {
|
|
2232
|
-
<div class="welcome-message">
|
|
2233
|
-
Welcome back, <strong>{{ userName }}</strong>!
|
|
2234
2550
|
</div>
|
|
2235
|
-
|
|
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
|
+
}
|
|
2236
2559
|
|
|
2237
|
-
|
|
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
|
+
}
|
|
2238
2566
|
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
(click)="
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
<div class="tenant-meta">
|
|
2255
|
-
<span class="tenant-role">{{ formatRole(membership.role) }}</span>
|
|
2256
|
-
@if (membership.last_accessed) {
|
|
2257
|
-
<span class="tenant-separator">·</span>
|
|
2258
|
-
<span class="tenant-last-accessed">
|
|
2259
|
-
Last accessed {{ formatLastAccessed(membership.last_accessed) }}
|
|
2260
|
-
</span>
|
|
2261
|
-
}
|
|
2262
|
-
</div>
|
|
2263
|
-
</div>
|
|
2264
|
-
</div>
|
|
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>
|
|
2265
2582
|
}
|
|
2266
2583
|
</div>
|
|
2584
|
+
}
|
|
2267
2585
|
|
|
2268
|
-
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
{{ loading ? 'Loading...' : continueButtonText }}
|
|
2274
|
-
</button>
|
|
2275
|
-
|
|
2276
|
-
<!-- Error Message -->
|
|
2277
|
-
@if (error) {
|
|
2278
|
-
<div class="error-message">
|
|
2279
|
-
{{ error }}
|
|
2280
|
-
</div>
|
|
2281
|
-
}
|
|
2282
|
-
|
|
2283
|
-
<!-- Create New Tenant Link -->
|
|
2284
|
-
@if (allowTenantCreation) {
|
|
2285
|
-
<div class="create-tenant-link">
|
|
2286
|
-
{{ createTenantLinkText }}
|
|
2287
|
-
<a href="#" (click)="onCreateTenantClick($event)">{{ createTenantLinkAction }}</a>
|
|
2288
|
-
</div>
|
|
2289
|
-
}
|
|
2586
|
+
<!-- Error Message -->
|
|
2587
|
+
@if (error) {
|
|
2588
|
+
<div class="error-message">
|
|
2589
|
+
{{ error }}
|
|
2590
|
+
</div>
|
|
2290
2591
|
}
|
|
2291
2592
|
|
|
2292
|
-
<!-- Loading
|
|
2593
|
+
<!-- Loading State -->
|
|
2293
2594
|
@if (loading) {
|
|
2294
2595
|
<div class="loading-overlay">
|
|
2295
2596
|
<div class="spinner"></div>
|
|
2296
2597
|
</div>
|
|
2297
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>
|
|
2298
2605
|
</div>
|
|
2299
|
-
`, styles: [".
|
|
2300
|
-
}], ctorParameters: () => [{ type: AuthService }], propDecorators: {
|
|
2301
|
-
type: Input
|
|
2302
|
-
}], providers: [{
|
|
2303
|
-
type: Input
|
|
2304
|
-
}], showTenantSelector: [{
|
|
2305
|
-
type: Input
|
|
2306
|
-
}], autoSelectSingleTenant: [{
|
|
2307
|
-
type: Input
|
|
2308
|
-
}], allowTenantCreation: [{
|
|
2309
|
-
type: Input
|
|
2310
|
-
}], tenantSelectorTitle: [{
|
|
2311
|
-
type: Input
|
|
2312
|
-
}], tenantSelectorDescription: [{
|
|
2313
|
-
type: Input
|
|
2314
|
-
}], continueButtonText: [{
|
|
2315
|
-
type: Input
|
|
2316
|
-
}], registerLinkText: [{
|
|
2317
|
-
type: Input
|
|
2318
|
-
}], registerLinkAction: [{
|
|
2319
|
-
type: Input
|
|
2320
|
-
}], createTenantLinkText: [{
|
|
2321
|
-
type: Input
|
|
2322
|
-
}], createTenantLinkAction: [{
|
|
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: [{
|
|
2323
2608
|
type: Input
|
|
2324
|
-
}], tenantSelected: [{
|
|
2325
|
-
type: Output
|
|
2326
|
-
}], createTenant: [{
|
|
2327
|
-
type: Output
|
|
2328
2609
|
}] } });
|
|
2329
2610
|
|
|
2330
2611
|
class TenantRegisterComponent {
|
|
@@ -2371,6 +2652,8 @@ class TenantRegisterComponent {
|
|
|
2371
2652
|
slugError = '';
|
|
2372
2653
|
useEmailPassword = false;
|
|
2373
2654
|
oauthProviders = [];
|
|
2655
|
+
showPassword = false;
|
|
2656
|
+
showConfirmPassword = false;
|
|
2374
2657
|
constructor(auth) {
|
|
2375
2658
|
this.auth = auth;
|
|
2376
2659
|
}
|
|
@@ -2668,30 +2951,44 @@ class TenantRegisterComponent {
|
|
|
2668
2951
|
class="form-control">
|
|
2669
2952
|
</div>
|
|
2670
2953
|
|
|
2671
|
-
<div class="form-group">
|
|
2954
|
+
<div class="form-group password-group">
|
|
2672
2955
|
<label for="password">Password *</label>
|
|
2673
2956
|
<input
|
|
2674
2957
|
id="password"
|
|
2675
2958
|
[(ngModel)]="password"
|
|
2676
2959
|
name="password"
|
|
2677
2960
|
placeholder="Create a password"
|
|
2678
|
-
type="password"
|
|
2961
|
+
[type]="showPassword ? 'text' : 'password'"
|
|
2679
2962
|
required
|
|
2680
2963
|
minlength="8"
|
|
2681
|
-
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>
|
|
2682
2972
|
<small class="form-hint">At least 8 characters</small>
|
|
2683
2973
|
</div>
|
|
2684
2974
|
|
|
2685
|
-
<div class="form-group">
|
|
2975
|
+
<div class="form-group password-group">
|
|
2686
2976
|
<label for="confirmPassword">Confirm Password *</label>
|
|
2687
2977
|
<input
|
|
2688
2978
|
id="confirmPassword"
|
|
2689
2979
|
[(ngModel)]="confirmPassword"
|
|
2690
2980
|
name="confirmPassword"
|
|
2691
2981
|
placeholder="Confirm your password"
|
|
2692
|
-
type="password"
|
|
2982
|
+
[type]="showConfirmPassword ? 'text' : 'password'"
|
|
2693
2983
|
required
|
|
2694
|
-
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>
|
|
2695
2992
|
</div>
|
|
2696
2993
|
|
|
2697
2994
|
<button
|
|
@@ -2740,7 +3037,7 @@ class TenantRegisterComponent {
|
|
|
2740
3037
|
<a href="#" (click)="onLoginClick($event)">{{ loginLinkAction }}</a>
|
|
2741
3038
|
</div>
|
|
2742
3039
|
</div>
|
|
2743
|
-
`, isInline: true, styles: [".tenant-register-dialog{padding:24px;max-width:500px;position:relative}.register-title{margin:0 0 20px;font-size:24px;font-weight:500;text-align:center}.warning-box{display:flex;gap:12px;padding:16px;background:#fff3cd;border:1px solid #ffc107;border-radius:6px;margin-bottom:24px}.warning-icon{font-size:24px;line-height:1}.warning-content{flex:1}.warning-content strong{display:block;margin-bottom:4px;color:#856404;font-size:14px}.warning-content p{margin:0;color:#856404;font-size:13px;line-height:1.5}.section-header{font-size:16px;font-weight:600;margin:20px 0 12px;padding-bottom:8px;border-bottom:2px solid #e0e0e0;color:#333}.register-form,.form-group{margin-bottom:16px}.form-group label{display:block;margin-bottom:6px;font-size:14px;font-weight:500;color:#333}.form-control{width:100%;padding:12px;border:1px solid #ddd;border-radius:4px;font-size:14px;box-sizing:border-box;transition:border-color .2s}.form-control:focus{outline:none;border-color:#4285f4}.form-control.input-error{border-color:#dc3545}.form-control.input-success{border-color:#28a745}.form-hint{display:block;margin-top:4px;font-size:12px;color:#666}.form-hint .checking{color:#666}.form-hint .available{color:#28a745;font-weight:500}.form-hint .error-text{color:#dc3545}.oauth-section{margin:16px 0}.oauth-description{margin-bottom:12px;font-size:14px;color:#666;text-align:center}.oauth-buttons{display:flex;flex-direction:column;gap:12px}.btn{padding:12px 24px;border:none;border-radius:4px;font-size:14px;font-weight:500;cursor:pointer;transition:background-color .2s,opacity .2s}.btn:disabled{opacity:.6;cursor:not-allowed}.btn-block{width:100%}.btn-primary{background-color:#4285f4;color:#fff}.btn-primary:hover:not(:disabled){background-color:#357ae8}.btn-oauth{width:100%;background:#fff;color:#333;border:1px solid #ddd;display:flex;align-items:center;justify-content:center;gap:8px}.btn-oauth:hover:not(:disabled){background:#f8f8f8}.btn-google{border-color:#4285f4}.btn-linkedin{border-color:#0077b5}.btn-apple{border-color:#000}.btn-microsoft{border-color:#00a4ef}.btn-github{border-color:#333}.oauth-icon{font-size:18px}.switch-method{margin-top:12px;text-align:center;font-size:14px}.switch-method a{color:#4285f4;text-decoration:none}.switch-method a:hover{text-decoration:underline}.error-message{margin-top:16px;padding:12px;background:#fee;color:#c33;border-radius:4px;font-size:14px}.success-message{margin-top:16px;padding:12px;background:#efe;color:#3a3;border-radius:4px;font-size:14px}.loading-overlay{position:absolute;inset:0;background:#fffffff2;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:16px}.spinner{width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #4285f4;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.loading-text{margin:0;font-size:14px;color:#666}.login-link{margin-top:16px;text-align:center;font-size:14px;color:#666}.login-link a{color:#4285f4;text-decoration:none}.login-link a:hover{text-decoration:underline}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i2.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i2.DefaultValueAccessor, selector: "input:not([type=checkbox])[formControlName],textarea[formControlName],input:not([type=checkbox])[formControl],textarea[formControl],input:not([type=checkbox])[ngModel],textarea[ngModel],[ngDefaultControl]" }, { kind: "directive", type: i2.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i2.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i2.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i2.MinLengthValidator, selector: "[minlength][formControlName],[minlength][formControl],[minlength][ngModel]", inputs: ["minlength"] }, { kind: "directive", type: i2.PatternValidator, selector: "[pattern][formControlName],[pattern][formControl],[pattern][ngModel]", inputs: ["pattern"] }, { kind: "directive", type: i2.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i2.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }] });
|
|
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"] }] });
|
|
2744
3041
|
}
|
|
2745
3042
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TenantRegisterComponent, decorators: [{
|
|
2746
3043
|
type: Component,
|
|
@@ -2865,30 +3162,44 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
|
|
|
2865
3162
|
class="form-control">
|
|
2866
3163
|
</div>
|
|
2867
3164
|
|
|
2868
|
-
<div class="form-group">
|
|
3165
|
+
<div class="form-group password-group">
|
|
2869
3166
|
<label for="password">Password *</label>
|
|
2870
3167
|
<input
|
|
2871
3168
|
id="password"
|
|
2872
3169
|
[(ngModel)]="password"
|
|
2873
3170
|
name="password"
|
|
2874
3171
|
placeholder="Create a password"
|
|
2875
|
-
type="password"
|
|
3172
|
+
[type]="showPassword ? 'text' : 'password'"
|
|
2876
3173
|
required
|
|
2877
3174
|
minlength="8"
|
|
2878
|
-
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>
|
|
2879
3183
|
<small class="form-hint">At least 8 characters</small>
|
|
2880
3184
|
</div>
|
|
2881
3185
|
|
|
2882
|
-
<div class="form-group">
|
|
3186
|
+
<div class="form-group password-group">
|
|
2883
3187
|
<label for="confirmPassword">Confirm Password *</label>
|
|
2884
3188
|
<input
|
|
2885
3189
|
id="confirmPassword"
|
|
2886
3190
|
[(ngModel)]="confirmPassword"
|
|
2887
3191
|
name="confirmPassword"
|
|
2888
3192
|
placeholder="Confirm your password"
|
|
2889
|
-
type="password"
|
|
3193
|
+
[type]="showConfirmPassword ? 'text' : 'password'"
|
|
2890
3194
|
required
|
|
2891
|
-
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>
|
|
2892
3203
|
</div>
|
|
2893
3204
|
|
|
2894
3205
|
<button
|
|
@@ -2937,7 +3248,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
|
|
|
2937
3248
|
<a href="#" (click)="onLoginClick($event)">{{ loginLinkAction }}</a>
|
|
2938
3249
|
</div>
|
|
2939
3250
|
</div>
|
|
2940
|
-
`, styles: [".tenant-register-dialog{padding:24px;max-width:500px;position:relative}.register-title{margin:0 0 20px;font-size:24px;font-weight:500;text-align:center}.warning-box{display:flex;gap:12px;padding:16px;background:#fff3cd;border:1px solid #ffc107;border-radius:6px;margin-bottom:24px}.warning-icon{font-size:24px;line-height:1}.warning-content{flex:1}.warning-content strong{display:block;margin-bottom:4px;color:#856404;font-size:14px}.warning-content p{margin:0;color:#856404;font-size:13px;line-height:1.5}.section-header{font-size:16px;font-weight:600;margin:20px 0 12px;padding-bottom:8px;border-bottom:2px solid #e0e0e0;color:#333}.register-form,.form-group{margin-bottom:16px}.form-group label{display:block;margin-bottom:6px;font-size:14px;font-weight:500;color:#333}.form-control{width:100%;padding:12px;border:1px solid #ddd;border-radius:4px;font-size:14px;box-sizing:border-box;transition:border-color .2s}.form-control:focus{outline:none;border-color:#4285f4}.form-control.input-error{border-color:#dc3545}.form-control.input-success{border-color:#28a745}.form-hint{display:block;margin-top:4px;font-size:12px;color:#666}.form-hint .checking{color:#666}.form-hint .available{color:#28a745;font-weight:500}.form-hint .error-text{color:#dc3545}.oauth-section{margin:16px 0}.oauth-description{margin-bottom:12px;font-size:14px;color:#666;text-align:center}.oauth-buttons{display:flex;flex-direction:column;gap:12px}.btn{padding:12px 24px;border:none;border-radius:4px;font-size:14px;font-weight:500;cursor:pointer;transition:background-color .2s,opacity .2s}.btn:disabled{opacity:.6;cursor:not-allowed}.btn-block{width:100%}.btn-primary{background-color:#4285f4;color:#fff}.btn-primary:hover:not(:disabled){background-color:#357ae8}.btn-oauth{width:100%;background:#fff;color:#333;border:1px solid #ddd;display:flex;align-items:center;justify-content:center;gap:8px}.btn-oauth:hover:not(:disabled){background:#f8f8f8}.btn-google{border-color:#4285f4}.btn-linkedin{border-color:#0077b5}.btn-apple{border-color:#000}.btn-microsoft{border-color:#00a4ef}.btn-github{border-color:#333}.oauth-icon{font-size:18px}.switch-method{margin-top:12px;text-align:center;font-size:14px}.switch-method a{color:#4285f4;text-decoration:none}.switch-method a:hover{text-decoration:underline}.error-message{margin-top:16px;padding:12px;background:#fee;color:#c33;border-radius:4px;font-size:14px}.success-message{margin-top:16px;padding:12px;background:#efe;color:#3a3;border-radius:4px;font-size:14px}.loading-overlay{position:absolute;inset:0;background:#fffffff2;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:16px}.spinner{width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #4285f4;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.loading-text{margin:0;font-size:14px;color:#666}.login-link{margin-top:16px;text-align:center;font-size:14px;color:#666}.login-link a{color:#4285f4;text-decoration:none}.login-link a:hover{text-decoration:underline}\n"] }]
|
|
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"] }]
|
|
2941
3252
|
}], ctorParameters: () => [{ type: AuthService }], propDecorators: { title: [{
|
|
2942
3253
|
type: Input
|
|
2943
3254
|
}], providers: [{
|
|
@@ -3047,7 +3358,7 @@ class TenantLoginDialogComponent {
|
|
|
3047
3358
|
(createTenant)="onCreateTenant()">
|
|
3048
3359
|
</lib-tenant-login>
|
|
3049
3360
|
</div>
|
|
3050
|
-
`, isInline: true, styles: [".dialog-wrapper{padding:0}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "component", type: TenantLoginComponent, selector: "lib-tenant-login", inputs: ["title", "providers", "showTenantSelector", "autoSelectSingleTenant", "allowTenantCreation", "tenantSelectorTitle", "tenantSelectorDescription", "continueButtonText", "registerLinkText", "registerLinkAction", "createTenantLinkText", "createTenantLinkAction"], outputs: ["tenantSelected", "createTenant"] }] });
|
|
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"] }] });
|
|
3051
3362
|
}
|
|
3052
3363
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImport: i0, type: TenantLoginDialogComponent, decorators: [{
|
|
3053
3364
|
type: Component,
|
|
@@ -3204,5 +3515,5 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.15", ngImpo
|
|
|
3204
3515
|
* Generated bundle index. Do not edit.
|
|
3205
3516
|
*/
|
|
3206
3517
|
|
|
3207
|
-
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 };
|
|
3208
3519
|
//# sourceMappingURL=progalaxyelabs-ngx-stonescriptphp-client.mjs.map
|