@progalaxyelabs/ngx-stonescriptphp-client 1.22.1 → 1.23.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,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { InjectionToken, makeEnvironmentProviders, Injectable, Inject, Optional, EventEmitter, Output, Input, Component, input, output, signal, computed } from '@angular/core';
|
|
2
|
+
import { InjectionToken, makeEnvironmentProviders, Injectable, Inject, Optional, EventEmitter, Output, Input, ViewChildren, Component, input, output, signal, computed } from '@angular/core';
|
|
3
3
|
import { BehaviorSubject } from 'rxjs';
|
|
4
4
|
import * as i3 from '@angular/common';
|
|
5
5
|
import { CommonModule } from '@angular/common';
|
|
@@ -777,6 +777,108 @@ class ProgalaxyElabsAuth {
|
|
|
777
777
|
return { exists: false };
|
|
778
778
|
}
|
|
779
779
|
}
|
|
780
|
+
// -- OTP authentication --------------------------------------------------
|
|
781
|
+
async sendOtp(identifier) {
|
|
782
|
+
try {
|
|
783
|
+
const response = await fetch(`${this.host}/api/auth/otp/send`, {
|
|
784
|
+
method: 'POST',
|
|
785
|
+
headers: { 'Content-Type': 'application/json' },
|
|
786
|
+
body: JSON.stringify({ identifier })
|
|
787
|
+
});
|
|
788
|
+
const data = await response.json();
|
|
789
|
+
if (!response.ok) {
|
|
790
|
+
return {
|
|
791
|
+
success: false,
|
|
792
|
+
identifier_type: 'email',
|
|
793
|
+
masked_identifier: '',
|
|
794
|
+
expires_in: 0,
|
|
795
|
+
resend_after: 0,
|
|
796
|
+
...data
|
|
797
|
+
};
|
|
798
|
+
}
|
|
799
|
+
return {
|
|
800
|
+
success: data.success ?? true,
|
|
801
|
+
identifier_type: data.identifier_type,
|
|
802
|
+
masked_identifier: data.masked_identifier,
|
|
803
|
+
expires_in: data.expires_in,
|
|
804
|
+
resend_after: data.resend_after
|
|
805
|
+
};
|
|
806
|
+
}
|
|
807
|
+
catch {
|
|
808
|
+
return {
|
|
809
|
+
success: false,
|
|
810
|
+
identifier_type: 'email',
|
|
811
|
+
masked_identifier: '',
|
|
812
|
+
expires_in: 0,
|
|
813
|
+
resend_after: 0
|
|
814
|
+
};
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
async verifyOtp(identifier, code) {
|
|
818
|
+
try {
|
|
819
|
+
const response = await fetch(`${this.host}/api/auth/otp/verify`, {
|
|
820
|
+
method: 'POST',
|
|
821
|
+
headers: { 'Content-Type': 'application/json' },
|
|
822
|
+
body: JSON.stringify({ identifier, code })
|
|
823
|
+
});
|
|
824
|
+
const data = await response.json();
|
|
825
|
+
if (!response.ok) {
|
|
826
|
+
return { success: false, verified_token: '', ...data };
|
|
827
|
+
}
|
|
828
|
+
return {
|
|
829
|
+
success: data.success ?? true,
|
|
830
|
+
verified_token: data.verified_token
|
|
831
|
+
};
|
|
832
|
+
}
|
|
833
|
+
catch {
|
|
834
|
+
return { success: false, verified_token: '' };
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
async identityLogin(verifiedToken) {
|
|
838
|
+
try {
|
|
839
|
+
const response = await fetch(`${this.host}/api/identity/login`, {
|
|
840
|
+
method: 'POST',
|
|
841
|
+
headers: { 'Content-Type': 'application/json' },
|
|
842
|
+
body: JSON.stringify({
|
|
843
|
+
verified_token: verifiedToken,
|
|
844
|
+
platform: this.config.platformCode
|
|
845
|
+
})
|
|
846
|
+
});
|
|
847
|
+
// 404 means no identity found — caller should show registration form
|
|
848
|
+
if (response.status === 404) {
|
|
849
|
+
return { success: false, message: 'identity_not_found' };
|
|
850
|
+
}
|
|
851
|
+
const data = await response.json();
|
|
852
|
+
if (!response.ok) {
|
|
853
|
+
return { success: false, message: data.error || data.message || 'Login failed' };
|
|
854
|
+
}
|
|
855
|
+
return this.handleLoginResponse(data);
|
|
856
|
+
}
|
|
857
|
+
catch {
|
|
858
|
+
return { success: false, message: 'Network error. Please try again.' };
|
|
859
|
+
}
|
|
860
|
+
}
|
|
861
|
+
async identityRegister(verifiedToken, displayName) {
|
|
862
|
+
try {
|
|
863
|
+
const response = await fetch(`${this.host}/api/identity/register`, {
|
|
864
|
+
method: 'POST',
|
|
865
|
+
headers: { 'Content-Type': 'application/json' },
|
|
866
|
+
body: JSON.stringify({
|
|
867
|
+
verified_token: verifiedToken,
|
|
868
|
+
display_name: displayName,
|
|
869
|
+
platform: this.config.platformCode
|
|
870
|
+
})
|
|
871
|
+
});
|
|
872
|
+
const data = await response.json();
|
|
873
|
+
if (!response.ok) {
|
|
874
|
+
return { success: false, message: data.error || data.message || 'Registration failed' };
|
|
875
|
+
}
|
|
876
|
+
return this.handleLoginResponse(data);
|
|
877
|
+
}
|
|
878
|
+
catch {
|
|
879
|
+
return { success: false, message: 'Network error. Please try again.' };
|
|
880
|
+
}
|
|
881
|
+
}
|
|
780
882
|
// -- OAuth ----------------------------------------------------------------
|
|
781
883
|
async loginWithProvider(provider) {
|
|
782
884
|
return new Promise((resolve) => {
|
|
@@ -877,7 +979,8 @@ class ProgalaxyElabsAuth {
|
|
|
877
979
|
if (!raw)
|
|
878
980
|
return undefined;
|
|
879
981
|
return {
|
|
880
|
-
email: raw.email,
|
|
982
|
+
email: raw.email ?? '',
|
|
983
|
+
phone: raw.phone,
|
|
881
984
|
display_name: raw.display_name ?? raw.email?.split('@')[0] ?? '',
|
|
882
985
|
photo_url: raw.photo_url ?? raw.picture,
|
|
883
986
|
is_email_verified: raw.is_email_verified ?? false
|
|
@@ -1201,6 +1304,37 @@ class AuthService {
|
|
|
1201
1304
|
getCurrentUser() {
|
|
1202
1305
|
return this.userSubject.value;
|
|
1203
1306
|
}
|
|
1307
|
+
// ── OTP authentication ─────────────────────────────────────────────────────
|
|
1308
|
+
async sendOtp(identifier) {
|
|
1309
|
+
if (!this.plugin.sendOtp) {
|
|
1310
|
+
return { success: false, identifier_type: 'email', masked_identifier: '', expires_in: 0, resend_after: 0 };
|
|
1311
|
+
}
|
|
1312
|
+
return this.plugin.sendOtp(identifier);
|
|
1313
|
+
}
|
|
1314
|
+
async verifyOtp(identifier, code) {
|
|
1315
|
+
if (!this.plugin.verifyOtp) {
|
|
1316
|
+
return { success: false, verified_token: '' };
|
|
1317
|
+
}
|
|
1318
|
+
return this.plugin.verifyOtp(identifier, code);
|
|
1319
|
+
}
|
|
1320
|
+
async identityLogin(verifiedToken) {
|
|
1321
|
+
if (!this.plugin.identityLogin) {
|
|
1322
|
+
return { success: false, message: 'OTP login not supported by the configured auth plugin' };
|
|
1323
|
+
}
|
|
1324
|
+
const result = await this.plugin.identityLogin(verifiedToken);
|
|
1325
|
+
if (result.success)
|
|
1326
|
+
this.storeAuthResult(result);
|
|
1327
|
+
return result;
|
|
1328
|
+
}
|
|
1329
|
+
async identityRegister(verifiedToken, displayName) {
|
|
1330
|
+
if (!this.plugin.identityRegister) {
|
|
1331
|
+
return { success: false, message: 'OTP registration not supported by the configured auth plugin' };
|
|
1332
|
+
}
|
|
1333
|
+
const result = await this.plugin.identityRegister(verifiedToken, displayName);
|
|
1334
|
+
if (result.success)
|
|
1335
|
+
this.storeAuthResult(result);
|
|
1336
|
+
return result;
|
|
1337
|
+
}
|
|
1204
1338
|
// ── Multi-tenant operations ───────────────────────────────────────────────
|
|
1205
1339
|
async getTenantMemberships(serverName) {
|
|
1206
1340
|
if (!this.plugin.getTenantMemberships)
|
|
@@ -2003,6 +2137,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
|
|
|
2003
2137
|
class TenantLoginComponent {
|
|
2004
2138
|
auth;
|
|
2005
2139
|
providerRegistry;
|
|
2140
|
+
otpInputs;
|
|
2006
2141
|
// Component Configuration
|
|
2007
2142
|
title = 'Sign In';
|
|
2008
2143
|
providers = ['google'];
|
|
@@ -2032,6 +2167,22 @@ class TenantLoginComponent {
|
|
|
2032
2167
|
showPassword = false;
|
|
2033
2168
|
useOAuth = true;
|
|
2034
2169
|
oauthProviders = [];
|
|
2170
|
+
// Effective OAuth providers (filtered for Android WebView)
|
|
2171
|
+
effectiveOauthProviders = [];
|
|
2172
|
+
// OTP State
|
|
2173
|
+
otpActive = false;
|
|
2174
|
+
otpStep = 'identifier';
|
|
2175
|
+
otpIdentifier = '';
|
|
2176
|
+
otpIdentifierHint = '';
|
|
2177
|
+
otpNormalizedIdentifier = ''; // E.164 for phone, as-is for email
|
|
2178
|
+
otpMaskedIdentifier = '';
|
|
2179
|
+
otpDigits = ['', '', '', '', '', ''];
|
|
2180
|
+
otpVerifiedToken = '';
|
|
2181
|
+
otpDisplayName = '';
|
|
2182
|
+
otpResendCountdown = 0;
|
|
2183
|
+
otpResendTimer = null;
|
|
2184
|
+
// Android WebView detection
|
|
2185
|
+
isAndroidWebView = false;
|
|
2035
2186
|
// Tenant Selection State
|
|
2036
2187
|
showingTenantSelector = false;
|
|
2037
2188
|
memberships = [];
|
|
@@ -2046,17 +2197,32 @@ class TenantLoginComponent {
|
|
|
2046
2197
|
this.error = 'Configuration Error: No authentication providers specified.';
|
|
2047
2198
|
throw new Error('TenantLoginComponent requires providers input.');
|
|
2048
2199
|
}
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2200
|
+
// Detect Android WebView
|
|
2201
|
+
this.isAndroidWebView = /wv|Android.*Version\//.test(navigator.userAgent);
|
|
2202
|
+
// Filter out 'otp' and 'emailPassword' for OAuth list
|
|
2203
|
+
this.oauthProviders = this.providers.filter(p => p !== 'emailPassword' && p !== 'otp');
|
|
2204
|
+
// Auto-hide Google on Android WebView
|
|
2205
|
+
this.effectiveOauthProviders = this.isAndroidWebView
|
|
2206
|
+
? this.oauthProviders.filter(p => p !== 'google')
|
|
2207
|
+
: [...this.oauthProviders];
|
|
2208
|
+
// If OTP is configured, it becomes the primary login method
|
|
2209
|
+
if (this.isProviderEnabled('otp')) {
|
|
2210
|
+
this.otpActive = true;
|
|
2211
|
+
}
|
|
2212
|
+
// If only emailPassword is available (no OTP, no OAuth), use it by default
|
|
2213
|
+
if (!this.otpActive && this.effectiveOauthProviders.length === 0 && this.isProviderEnabled('emailPassword')) {
|
|
2052
2214
|
this.useOAuth = false;
|
|
2053
2215
|
}
|
|
2054
2216
|
// Prefill email if provided (for account linking flow)
|
|
2055
2217
|
if (this.prefillEmail) {
|
|
2056
2218
|
this.email = this.prefillEmail;
|
|
2057
|
-
this.useOAuth = false;
|
|
2219
|
+
this.useOAuth = false;
|
|
2220
|
+
this.otpActive = false; // Switch to email/password form for linking
|
|
2058
2221
|
}
|
|
2059
2222
|
}
|
|
2223
|
+
ngOnDestroy() {
|
|
2224
|
+
this.clearResendTimer();
|
|
2225
|
+
}
|
|
2060
2226
|
isProviderEnabled(provider) {
|
|
2061
2227
|
return this.providers.includes(provider);
|
|
2062
2228
|
}
|
|
@@ -2237,6 +2403,244 @@ class TenantLoginComponent {
|
|
|
2237
2403
|
this.loading = false;
|
|
2238
2404
|
}
|
|
2239
2405
|
}
|
|
2406
|
+
// ── OTP methods ────────────────────────────────────────────────────────────
|
|
2407
|
+
/** Detect identifier type from input and show hint */
|
|
2408
|
+
onOtpIdentifierChange() {
|
|
2409
|
+
const value = this.otpIdentifier.trim();
|
|
2410
|
+
if (!value) {
|
|
2411
|
+
this.otpIdentifierHint = '';
|
|
2412
|
+
return;
|
|
2413
|
+
}
|
|
2414
|
+
const detected = this.detectIdentifierType(value);
|
|
2415
|
+
if (detected === 'email') {
|
|
2416
|
+
this.otpIdentifierHint = 'OTP will be sent to this email';
|
|
2417
|
+
}
|
|
2418
|
+
else if (detected === 'phone') {
|
|
2419
|
+
const digits = value.replace(/\D/g, '');
|
|
2420
|
+
if (digits.length === 10) {
|
|
2421
|
+
this.otpIdentifierHint = 'OTP will be sent to +91 ' + digits;
|
|
2422
|
+
}
|
|
2423
|
+
else {
|
|
2424
|
+
this.otpIdentifierHint = 'OTP will be sent to this number';
|
|
2425
|
+
}
|
|
2426
|
+
}
|
|
2427
|
+
else {
|
|
2428
|
+
this.otpIdentifierHint = '';
|
|
2429
|
+
}
|
|
2430
|
+
}
|
|
2431
|
+
/** Send OTP to the entered identifier */
|
|
2432
|
+
async onOtpSend() {
|
|
2433
|
+
const raw = this.otpIdentifier.trim();
|
|
2434
|
+
if (!raw) {
|
|
2435
|
+
this.error = 'Please enter your email or phone number';
|
|
2436
|
+
return;
|
|
2437
|
+
}
|
|
2438
|
+
const type = this.detectIdentifierType(raw);
|
|
2439
|
+
if (!type) {
|
|
2440
|
+
this.error = 'Please enter a valid email address or phone number';
|
|
2441
|
+
return;
|
|
2442
|
+
}
|
|
2443
|
+
// Normalize: auto-prepend +91 for 10-digit Indian numbers
|
|
2444
|
+
if (type === 'phone') {
|
|
2445
|
+
const digits = raw.replace(/\D/g, '');
|
|
2446
|
+
this.otpNormalizedIdentifier = digits.length === 10 ? `+91${digits}` : (raw.startsWith('+') ? raw : `+${digits}`);
|
|
2447
|
+
}
|
|
2448
|
+
else {
|
|
2449
|
+
this.otpNormalizedIdentifier = raw;
|
|
2450
|
+
}
|
|
2451
|
+
this.loading = true;
|
|
2452
|
+
this.error = '';
|
|
2453
|
+
try {
|
|
2454
|
+
const result = await this.auth.sendOtp(this.otpNormalizedIdentifier);
|
|
2455
|
+
if (!result.success) {
|
|
2456
|
+
this.error = 'Failed to send OTP. Please try again.';
|
|
2457
|
+
return;
|
|
2458
|
+
}
|
|
2459
|
+
this.otpMaskedIdentifier = result.masked_identifier;
|
|
2460
|
+
this.otpDigits = ['', '', '', '', '', ''];
|
|
2461
|
+
this.otpStep = 'code';
|
|
2462
|
+
this.startResendCountdown(result.resend_after || 60);
|
|
2463
|
+
// Focus first digit input after view update
|
|
2464
|
+
setTimeout(() => this.focusOtpInput(0), 50);
|
|
2465
|
+
}
|
|
2466
|
+
catch (err) {
|
|
2467
|
+
this.error = err.message || 'Failed to send OTP';
|
|
2468
|
+
}
|
|
2469
|
+
finally {
|
|
2470
|
+
this.loading = false;
|
|
2471
|
+
}
|
|
2472
|
+
}
|
|
2473
|
+
/** Verify the entered OTP code */
|
|
2474
|
+
async onOtpVerify() {
|
|
2475
|
+
const code = this.otpCode;
|
|
2476
|
+
if (code.length < 6) {
|
|
2477
|
+
this.error = 'Please enter the complete 6-digit code';
|
|
2478
|
+
return;
|
|
2479
|
+
}
|
|
2480
|
+
this.loading = true;
|
|
2481
|
+
this.error = '';
|
|
2482
|
+
try {
|
|
2483
|
+
const verifyResult = await this.auth.verifyOtp(this.otpNormalizedIdentifier, code);
|
|
2484
|
+
if (!verifyResult.success) {
|
|
2485
|
+
this.error = 'Invalid code. Please try again.';
|
|
2486
|
+
return;
|
|
2487
|
+
}
|
|
2488
|
+
this.otpVerifiedToken = verifyResult.verified_token;
|
|
2489
|
+
// Auto-attempt login
|
|
2490
|
+
const loginResult = await this.auth.identityLogin(this.otpVerifiedToken);
|
|
2491
|
+
if (loginResult.success) {
|
|
2492
|
+
// Existing user — proceed with standard post-auth flow
|
|
2493
|
+
await this.handlePostAuthFlow(loginResult);
|
|
2494
|
+
return;
|
|
2495
|
+
}
|
|
2496
|
+
if (loginResult.message === 'identity_not_found') {
|
|
2497
|
+
// New user — show registration form
|
|
2498
|
+
this.otpStep = 'register';
|
|
2499
|
+
return;
|
|
2500
|
+
}
|
|
2501
|
+
// Other login error
|
|
2502
|
+
this.error = loginResult.message || 'Login failed. Please try again.';
|
|
2503
|
+
}
|
|
2504
|
+
catch (err) {
|
|
2505
|
+
this.error = err.message || 'Verification failed';
|
|
2506
|
+
}
|
|
2507
|
+
finally {
|
|
2508
|
+
this.loading = false;
|
|
2509
|
+
}
|
|
2510
|
+
}
|
|
2511
|
+
/** Register a new identity after OTP verification */
|
|
2512
|
+
async onOtpRegister() {
|
|
2513
|
+
const name = this.otpDisplayName.trim();
|
|
2514
|
+
if (!name) {
|
|
2515
|
+
this.error = 'Please enter your name';
|
|
2516
|
+
return;
|
|
2517
|
+
}
|
|
2518
|
+
this.loading = true;
|
|
2519
|
+
this.error = '';
|
|
2520
|
+
try {
|
|
2521
|
+
const result = await this.auth.identityRegister(this.otpVerifiedToken, name);
|
|
2522
|
+
if (!result.success) {
|
|
2523
|
+
this.error = result.message || 'Registration failed';
|
|
2524
|
+
return;
|
|
2525
|
+
}
|
|
2526
|
+
// New identity created — emit onboarding event
|
|
2527
|
+
const identifierType = this.detectIdentifierType(this.otpIdentifier.trim());
|
|
2528
|
+
this.needsOnboarding.emit({
|
|
2529
|
+
auth_method: 'otp',
|
|
2530
|
+
is_new_identity: true,
|
|
2531
|
+
identity: {
|
|
2532
|
+
email: identifierType === 'email' ? this.otpNormalizedIdentifier : '',
|
|
2533
|
+
phone: identifierType === 'phone' ? this.otpNormalizedIdentifier : undefined,
|
|
2534
|
+
display_name: name,
|
|
2535
|
+
},
|
|
2536
|
+
});
|
|
2537
|
+
}
|
|
2538
|
+
catch (err) {
|
|
2539
|
+
this.error = err.message || 'Registration failed';
|
|
2540
|
+
}
|
|
2541
|
+
finally {
|
|
2542
|
+
this.loading = false;
|
|
2543
|
+
}
|
|
2544
|
+
}
|
|
2545
|
+
/** Resend the OTP */
|
|
2546
|
+
onOtpResend(event) {
|
|
2547
|
+
event.preventDefault();
|
|
2548
|
+
this.onOtpSend();
|
|
2549
|
+
}
|
|
2550
|
+
/** Go back to identifier entry */
|
|
2551
|
+
onOtpBack(event) {
|
|
2552
|
+
event.preventDefault();
|
|
2553
|
+
this.otpStep = 'identifier';
|
|
2554
|
+
this.otpDigits = ['', '', '', '', '', ''];
|
|
2555
|
+
this.otpVerifiedToken = '';
|
|
2556
|
+
this.error = '';
|
|
2557
|
+
this.clearResendTimer();
|
|
2558
|
+
}
|
|
2559
|
+
// ── OTP digit input handling ─────────────────────────────────────────────
|
|
2560
|
+
onOtpDigitInput(event, index) {
|
|
2561
|
+
const input = event.target;
|
|
2562
|
+
const value = input.value.replace(/\D/g, '');
|
|
2563
|
+
this.otpDigits[index] = value ? value[0] : '';
|
|
2564
|
+
input.value = this.otpDigits[index];
|
|
2565
|
+
// Auto-advance to next input
|
|
2566
|
+
if (value && index < 5) {
|
|
2567
|
+
this.focusOtpInput(index + 1);
|
|
2568
|
+
}
|
|
2569
|
+
// Auto-verify when all 6 digits entered
|
|
2570
|
+
if (this.otpCode.length === 6) {
|
|
2571
|
+
this.onOtpVerify();
|
|
2572
|
+
}
|
|
2573
|
+
}
|
|
2574
|
+
onOtpDigitKeydown(event, index) {
|
|
2575
|
+
if (event.key === 'Backspace' && !this.otpDigits[index] && index > 0) {
|
|
2576
|
+
this.focusOtpInput(index - 1);
|
|
2577
|
+
}
|
|
2578
|
+
}
|
|
2579
|
+
onOtpPaste(event) {
|
|
2580
|
+
event.preventDefault();
|
|
2581
|
+
const pasted = (event.clipboardData?.getData('text') || '').replace(/\D/g, '').slice(0, 6);
|
|
2582
|
+
for (let i = 0; i < 6; i++) {
|
|
2583
|
+
this.otpDigits[i] = pasted[i] || '';
|
|
2584
|
+
}
|
|
2585
|
+
// Update all input elements
|
|
2586
|
+
const inputs = this.otpInputs?.toArray();
|
|
2587
|
+
if (inputs) {
|
|
2588
|
+
for (let i = 0; i < 6; i++) {
|
|
2589
|
+
inputs[i].nativeElement.value = this.otpDigits[i];
|
|
2590
|
+
}
|
|
2591
|
+
}
|
|
2592
|
+
if (pasted.length >= 6) {
|
|
2593
|
+
this.onOtpVerify();
|
|
2594
|
+
}
|
|
2595
|
+
else {
|
|
2596
|
+
this.focusOtpInput(Math.min(pasted.length, 5));
|
|
2597
|
+
}
|
|
2598
|
+
}
|
|
2599
|
+
get otpCode() {
|
|
2600
|
+
return this.otpDigits.join('');
|
|
2601
|
+
}
|
|
2602
|
+
// ── OTP helpers ──────────────────────────────────────────────────────────
|
|
2603
|
+
detectIdentifierType(value) {
|
|
2604
|
+
if (value.includes('@')) {
|
|
2605
|
+
// Basic email validation
|
|
2606
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) ? 'email' : null;
|
|
2607
|
+
}
|
|
2608
|
+
const digits = value.replace(/\D/g, '');
|
|
2609
|
+
if (digits.length >= 10 && digits.length <= 15) {
|
|
2610
|
+
return 'phone';
|
|
2611
|
+
}
|
|
2612
|
+
return null;
|
|
2613
|
+
}
|
|
2614
|
+
focusOtpInput(index) {
|
|
2615
|
+
const inputs = this.otpInputs?.toArray();
|
|
2616
|
+
if (inputs && inputs[index]) {
|
|
2617
|
+
inputs[index].nativeElement.focus();
|
|
2618
|
+
inputs[index].nativeElement.select();
|
|
2619
|
+
}
|
|
2620
|
+
}
|
|
2621
|
+
startResendCountdown(seconds) {
|
|
2622
|
+
this.clearResendTimer();
|
|
2623
|
+
this.otpResendCountdown = seconds;
|
|
2624
|
+
this.otpResendTimer = setInterval(() => {
|
|
2625
|
+
this.otpResendCountdown--;
|
|
2626
|
+
if (this.otpResendCountdown <= 0) {
|
|
2627
|
+
this.clearResendTimer();
|
|
2628
|
+
}
|
|
2629
|
+
}, 1000);
|
|
2630
|
+
}
|
|
2631
|
+
clearResendTimer() {
|
|
2632
|
+
if (this.otpResendTimer) {
|
|
2633
|
+
clearInterval(this.otpResendTimer);
|
|
2634
|
+
this.otpResendTimer = null;
|
|
2635
|
+
}
|
|
2636
|
+
this.otpResendCountdown = 0;
|
|
2637
|
+
}
|
|
2638
|
+
formatCountdown(seconds) {
|
|
2639
|
+
const m = Math.floor(seconds / 60);
|
|
2640
|
+
const s = seconds % 60;
|
|
2641
|
+
return `${m}:${s.toString().padStart(2, '0')}`;
|
|
2642
|
+
}
|
|
2643
|
+
// ── Formatting helpers ───────────────────────────────────────────────────
|
|
2240
2644
|
formatRole(role) {
|
|
2241
2645
|
return role.charAt(0).toUpperCase() + role.slice(1);
|
|
2242
2646
|
}
|
|
@@ -2265,14 +2669,125 @@ class TenantLoginComponent {
|
|
|
2265
2669
|
this.createTenant.emit();
|
|
2266
2670
|
}
|
|
2267
2671
|
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: TenantLoginComponent, deps: [{ token: AuthService }, { token: ProviderRegistryService }], target: i0.ɵɵFactoryTarget.Component });
|
|
2268
|
-
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.17", 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", needsOnboarding: "needsOnboarding", createTenant: "createTenant" }, ngImport: i0, template: `
|
|
2672
|
+
static ɵcmp = i0.ɵɵngDeclareComponent({ minVersion: "17.0.0", version: "20.3.17", 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", needsOnboarding: "needsOnboarding", createTenant: "createTenant" }, viewQueries: [{ propertyName: "otpInputs", predicate: ["otpInput"], descendants: true }], ngImport: i0, template: `
|
|
2269
2673
|
<div class="tenant-login-dialog">
|
|
2270
2674
|
@if (!showingTenantSelector) {
|
|
2271
2675
|
<!-- Step 1: Authentication -->
|
|
2272
2676
|
<h2 class="login-title">{{ title }}</h2>
|
|
2273
2677
|
|
|
2678
|
+
<!-- OTP Flow -->
|
|
2679
|
+
@if (isProviderEnabled('otp') && otpActive) {
|
|
2680
|
+
<!-- OTP Step 1: Identifier entry -->
|
|
2681
|
+
@if (otpStep === 'identifier') {
|
|
2682
|
+
<form (ngSubmit)="onOtpSend()" class="otp-form">
|
|
2683
|
+
<div class="form-group">
|
|
2684
|
+
<input
|
|
2685
|
+
[(ngModel)]="otpIdentifier"
|
|
2686
|
+
name="otpIdentifier"
|
|
2687
|
+
placeholder="Enter Email or Phone Number"
|
|
2688
|
+
type="text"
|
|
2689
|
+
required
|
|
2690
|
+
autocomplete="email tel"
|
|
2691
|
+
class="form-control"
|
|
2692
|
+
(input)="onOtpIdentifierChange()">
|
|
2693
|
+
@if (otpIdentifierHint) {
|
|
2694
|
+
<div class="field-hint">{{ otpIdentifierHint }}</div>
|
|
2695
|
+
}
|
|
2696
|
+
</div>
|
|
2697
|
+
<button
|
|
2698
|
+
type="submit"
|
|
2699
|
+
[disabled]="loading || !otpIdentifier.trim()"
|
|
2700
|
+
class="btn btn-primary btn-block">
|
|
2701
|
+
{{ loading ? 'Sending...' : 'Send OTP' }}
|
|
2702
|
+
</button>
|
|
2703
|
+
</form>
|
|
2704
|
+
}
|
|
2705
|
+
|
|
2706
|
+
<!-- OTP Step 2: Code entry -->
|
|
2707
|
+
@if (otpStep === 'code') {
|
|
2708
|
+
<div class="otp-code-section">
|
|
2709
|
+
<p class="otp-subtitle">
|
|
2710
|
+
Enter the 6-digit code sent to
|
|
2711
|
+
<strong>{{ otpMaskedIdentifier }}</strong>
|
|
2712
|
+
</p>
|
|
2713
|
+
<div class="otp-digits">
|
|
2714
|
+
@for (digit of otpDigits; track $index; let i = $index) {
|
|
2715
|
+
<input
|
|
2716
|
+
#otpInput
|
|
2717
|
+
type="text"
|
|
2718
|
+
inputmode="numeric"
|
|
2719
|
+
maxlength="1"
|
|
2720
|
+
class="otp-digit-input"
|
|
2721
|
+
[value]="otpDigits[i]"
|
|
2722
|
+
(input)="onOtpDigitInput($event, i)"
|
|
2723
|
+
(keydown)="onOtpDigitKeydown($event, i)"
|
|
2724
|
+
(paste)="onOtpPaste($event)">
|
|
2725
|
+
}
|
|
2726
|
+
</div>
|
|
2727
|
+
<div class="otp-actions">
|
|
2728
|
+
<button
|
|
2729
|
+
type="button"
|
|
2730
|
+
(click)="onOtpVerify()"
|
|
2731
|
+
[disabled]="loading || otpCode.length < 6"
|
|
2732
|
+
class="btn btn-primary btn-block">
|
|
2733
|
+
{{ loading ? 'Verifying...' : 'Verify' }}
|
|
2734
|
+
</button>
|
|
2735
|
+
</div>
|
|
2736
|
+
<div class="otp-resend">
|
|
2737
|
+
@if (otpResendCountdown > 0) {
|
|
2738
|
+
<span class="resend-timer">
|
|
2739
|
+
Resend in {{ formatCountdown(otpResendCountdown) }}
|
|
2740
|
+
</span>
|
|
2741
|
+
} @else {
|
|
2742
|
+
<a href="#" (click)="onOtpResend($event)" class="resend-link">
|
|
2743
|
+
Resend OTP
|
|
2744
|
+
</a>
|
|
2745
|
+
}
|
|
2746
|
+
</div>
|
|
2747
|
+
<div class="otp-back">
|
|
2748
|
+
<a href="#" (click)="onOtpBack($event)">
|
|
2749
|
+
Use a different email or phone
|
|
2750
|
+
</a>
|
|
2751
|
+
</div>
|
|
2752
|
+
</div>
|
|
2753
|
+
}
|
|
2754
|
+
|
|
2755
|
+
<!-- OTP Step 3: Registration (new user) -->
|
|
2756
|
+
@if (otpStep === 'register') {
|
|
2757
|
+
<div class="otp-register-section">
|
|
2758
|
+
<p class="otp-subtitle">
|
|
2759
|
+
Welcome! Enter your name to get started.
|
|
2760
|
+
</p>
|
|
2761
|
+
<form (ngSubmit)="onOtpRegister()" class="otp-form">
|
|
2762
|
+
<div class="form-group">
|
|
2763
|
+
<input
|
|
2764
|
+
[(ngModel)]="otpDisplayName"
|
|
2765
|
+
name="displayName"
|
|
2766
|
+
placeholder="Your Name"
|
|
2767
|
+
type="text"
|
|
2768
|
+
required
|
|
2769
|
+
class="form-control">
|
|
2770
|
+
</div>
|
|
2771
|
+
<button
|
|
2772
|
+
type="submit"
|
|
2773
|
+
[disabled]="loading || !otpDisplayName.trim()"
|
|
2774
|
+
class="btn btn-primary btn-block">
|
|
2775
|
+
{{ loading ? 'Creating account...' : 'Continue' }}
|
|
2776
|
+
</button>
|
|
2777
|
+
</form>
|
|
2778
|
+
</div>
|
|
2779
|
+
}
|
|
2780
|
+
|
|
2781
|
+
<!-- Divider before other providers -->
|
|
2782
|
+
@if (effectiveOauthProviders.length > 0 || isProviderEnabled('emailPassword')) {
|
|
2783
|
+
<div class="divider">
|
|
2784
|
+
<span>OR</span>
|
|
2785
|
+
</div>
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
|
|
2274
2789
|
<!-- Email/Password Form (if enabled) -->
|
|
2275
|
-
@if (isProviderEnabled('emailPassword') && !useOAuth) {
|
|
2790
|
+
@if (isProviderEnabled('emailPassword') && !useOAuth && !otpActive) {
|
|
2276
2791
|
<form (ngSubmit)="onEmailLogin()" class="email-form">
|
|
2277
2792
|
<div class="form-group">
|
|
2278
2793
|
<input
|
|
@@ -2296,7 +2811,7 @@ class TenantLoginComponent {
|
|
|
2296
2811
|
class="password-toggle"
|
|
2297
2812
|
(click)="showPassword = !showPassword"
|
|
2298
2813
|
[attr.aria-label]="showPassword ? 'Hide password' : 'Show password'">
|
|
2299
|
-
{{ showPassword ? '
|
|
2814
|
+
{{ showPassword ? '👁' : '👁‍🗨' }}
|
|
2300
2815
|
</button>
|
|
2301
2816
|
</div>
|
|
2302
2817
|
<button
|
|
@@ -2308,7 +2823,7 @@ class TenantLoginComponent {
|
|
|
2308
2823
|
</form>
|
|
2309
2824
|
|
|
2310
2825
|
<!-- Divider -->
|
|
2311
|
-
@if (
|
|
2826
|
+
@if (effectiveOauthProviders.length > 0) {
|
|
2312
2827
|
<div class="divider">
|
|
2313
2828
|
<span>OR</span>
|
|
2314
2829
|
</div>
|
|
@@ -2316,9 +2831,9 @@ class TenantLoginComponent {
|
|
|
2316
2831
|
}
|
|
2317
2832
|
|
|
2318
2833
|
<!-- OAuth Providers -->
|
|
2319
|
-
@if (
|
|
2834
|
+
@if (effectiveOauthProviders.length > 0 && !otpActive && (useOAuth || !isProviderEnabled('emailPassword'))) {
|
|
2320
2835
|
<div class="oauth-buttons">
|
|
2321
|
-
@for (provider of
|
|
2836
|
+
@for (provider of effectiveOauthProviders; track provider) {
|
|
2322
2837
|
<button
|
|
2323
2838
|
type="button"
|
|
2324
2839
|
(click)="onOAuthLogin(provider)"
|
|
@@ -2336,7 +2851,7 @@ class TenantLoginComponent {
|
|
|
2336
2851
|
</div>
|
|
2337
2852
|
|
|
2338
2853
|
<!-- Switch to Email/Password -->
|
|
2339
|
-
@if (isProviderEnabled('emailPassword') &&
|
|
2854
|
+
@if (isProviderEnabled('emailPassword') && effectiveOauthProviders.length > 0) {
|
|
2340
2855
|
<div class="switch-method">
|
|
2341
2856
|
<a href="#" (click)="toggleAuthMethod($event)">
|
|
2342
2857
|
{{ useOAuth ? 'Use email/password instead' : 'Use OAuth instead' }}
|
|
@@ -2431,7 +2946,7 @@ class TenantLoginComponent {
|
|
|
2431
2946
|
</div>
|
|
2432
2947
|
}
|
|
2433
2948
|
</div>
|
|
2434
|
-
`, isInline: true, styles: [".tenant-login-dialog{padding:24px;max-width:450px;position:relative}.login-title{margin:0 0 24px;font-size:24px;font-weight:500;text-align:center}.welcome-message{margin-bottom:16px;padding:12px;background:#e8f5e9;border-radius:4px;text-align:center;font-size:14px;color:#2e7d32}.selector-description{margin-bottom:20px;font-size:14px;color:#666;text-align:center}.email-form,.form-group{margin-bottom:16px}.password-group{position:relative}.form-control{width:100%;padding:12px;border:1px solid #ddd;border-radius:4px;font-size:14px;box-sizing:border-box}.password-input{padding-right:45px}.password-toggle{position:absolute;right:8px;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;font-size:18px;padding:8px;line-height:1;opacity:.6;transition:opacity .2s}.password-toggle:hover{opacity:1}.password-toggle:focus{outline:2px solid #4285f4;outline-offset:2px;border-radius:4px}.form-control:focus{outline:none;border-color:#4285f4}.btn{padding:12px 24px;border:none;border-radius:4px;font-size:14px;font-weight:500;cursor:pointer;transition:background-color .2s}.btn:disabled{opacity:.6;cursor:not-allowed}.btn-block{width:100%}.btn-primary{background-color:#4285f4;color:#fff}.btn-primary:hover:not(:disabled){background-color:#357ae8}.divider{margin:16px 0;text-align:center;position:relative}.divider:before{content:\"\";position:absolute;top:50%;left:0;right:0;height:1px;background:#ddd}.divider span{background:#fff;padding:0 12px;position:relative;color:#666;font-size:12px}.oauth-buttons{display:flex;flex-direction:column;gap:12px}.btn-oauth{width:100%;background:#fff;color:#333;border:1px solid #ddd;display:flex;align-items:center;justify-content:center;gap:8px}.btn-oauth:hover:not(:disabled){background:#f8f8f8}.btn-google{border-color:#4285f4}.btn-linkedin{border-color:#0077b5}.btn-apple{border-color:#000}.btn-microsoft{border-color:#00a4ef}.btn-github{border-color:#333}.btn-zoho{background-color:#f0483e;color:#fff;border:1px solid #d63b32}.btn-zoho:hover{background-color:#d63b32}.oauth-icon{font-size:18px}.switch-method{margin-top:12px;text-align:center;font-size:14px}.switch-method a{color:#4285f4;text-decoration:none}.switch-method a:hover{text-decoration:underline}.tenant-list{margin-bottom:20px;display:flex;flex-direction:column;gap:12px}.tenant-item{display:flex;align-items:flex-start;gap:12px;padding:16px;border:2px solid #e0e0e0;border-radius:6px;cursor:pointer;transition:all .2s}.tenant-item:hover{border-color:#4285f4;background:#f8f9ff}.tenant-item.selected{border-color:#4285f4;background:#e8f0fe}.tenant-radio{flex-shrink:0;padding-top:2px}.tenant-radio input[type=radio]{width:18px;height:18px;cursor:pointer}.tenant-info{flex:1}.tenant-name{font-size:16px;font-weight:500;color:#333;margin-bottom:4px}.tenant-meta{font-size:13px;color:#666}.tenant-role{font-weight:500;color:#4285f4}.tenant-separator{margin:0 6px}.error-message{margin-top:16px;padding:12px;background:#fee;color:#c33;border-radius:4px;font-size:14px}.loading-overlay{position:absolute;inset:0;background:#fffc;display:flex;align-items:center;justify-content:center}.spinner{width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #4285f4;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.register-link{margin-top:16px;text-align:center;font-size:14px;color:#666}.register-link a{color:#4285f4;text-decoration:none}.register-link a:hover{text-decoration:underline}.create-tenant-link{margin-top:16px;padding-top:16px;border-top:1px solid #e0e0e0;text-align:center;font-size:14px;color:#666}.create-tenant-link a{color:#4285f4;text-decoration:none;font-weight:500}.create-tenant-link a:hover{text-decoration:underline}\n"], dependencies: [{ kind: "ngmodule", type: CommonModule }, { kind: "directive", type: i3.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i4.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i4.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: i4.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i4.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i4.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i4.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i4.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }] });
|
|
2949
|
+
`, 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,.otp-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}.field-hint{margin-top:4px;font-size:12px;color:#888}.btn{padding:12px 24px;border:none;border-radius:4px;font-size:14px;font-weight:500;cursor:pointer;transition:background-color .2s}.btn:disabled{opacity:.6;cursor:not-allowed}.btn-block{width:100%}.btn-primary{background-color:#4285f4;color:#fff}.btn-primary:hover:not(:disabled){background-color:#357ae8}.divider{margin:16px 0;text-align:center;position:relative}.divider:before{content:\"\";position:absolute;top:50%;left:0;right:0;height:1px;background:#ddd}.divider span{background:#fff;padding:0 12px;position:relative;color:#666;font-size:12px}.oauth-buttons{display:flex;flex-direction:column;gap:12px}.btn-oauth{width:100%;background:#fff;color:#333;border:1px solid #ddd;display:flex;align-items:center;justify-content:center;gap:8px}.btn-oauth:hover:not(:disabled){background:#f8f8f8}.btn-google{border-color:#4285f4}.btn-linkedin{border-color:#0077b5}.btn-apple{border-color:#000}.btn-microsoft{border-color:#00a4ef}.btn-github{border-color:#333}.btn-zoho{background-color:#f0483e;color:#fff;border:1px solid #d63b32}.btn-zoho:hover{background-color:#d63b32}.oauth-icon{font-size:18px}.switch-method{margin-top:12px;text-align:center;font-size:14px}.switch-method a{color:#4285f4;text-decoration:none}.switch-method a:hover{text-decoration:underline}.otp-subtitle{text-align:center;font-size:14px;color:#555;margin-bottom:20px}.otp-digits{display:flex;justify-content:center;gap:8px;margin-bottom:20px}.otp-digit-input{width:44px;height:52px;text-align:center;font-size:22px;font-weight:600;border:2px solid #ddd;border-radius:8px;outline:none;transition:border-color .2s;box-sizing:border-box}.otp-digit-input:focus{border-color:#4285f4}.otp-actions{margin-bottom:12px}.otp-resend{text-align:center;font-size:14px;margin-bottom:8px}.resend-timer{color:#888}.resend-link{color:#4285f4;text-decoration:none;cursor:pointer}.resend-link:hover{text-decoration:underline}.otp-back{text-align:center;font-size:13px}.otp-back a{color:#888;text-decoration:none}.otp-back a:hover{text-decoration:underline;color:#4285f4}.otp-register-section .otp-subtitle{color:#2e7d32}.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: "directive", type: i3.NgStyle, selector: "[ngStyle]", inputs: ["ngStyle"] }, { kind: "ngmodule", type: FormsModule }, { kind: "directive", type: i4.ɵNgNoValidate, selector: "form:not([ngNoForm]):not([ngNativeValidate])" }, { kind: "directive", type: i4.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: i4.NgControlStatus, selector: "[formControlName],[ngModel],[formControl]" }, { kind: "directive", type: i4.NgControlStatusGroup, selector: "[formGroupName],[formArrayName],[ngModelGroup],[formGroup],form:not([ngNoForm]),[ngForm]" }, { kind: "directive", type: i4.RequiredValidator, selector: ":not([type=checkbox])[required][formControlName],:not([type=checkbox])[required][formControl],:not([type=checkbox])[required][ngModel]", inputs: ["required"] }, { kind: "directive", type: i4.NgModel, selector: "[ngModel]:not([formControlName]):not([formControl])", inputs: ["name", "disabled", "ngModel", "ngModelOptions"], outputs: ["ngModelChange"], exportAs: ["ngModel"] }, { kind: "directive", type: i4.NgForm, selector: "form:not([ngNoForm]):not([formGroup]),ng-form,[ngForm]", inputs: ["ngFormOptions"], outputs: ["ngSubmit"], exportAs: ["ngForm"] }] });
|
|
2435
2950
|
}
|
|
2436
2951
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: TenantLoginComponent, decorators: [{
|
|
2437
2952
|
type: Component,
|
|
@@ -2441,8 +2956,119 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
|
|
|
2441
2956
|
<!-- Step 1: Authentication -->
|
|
2442
2957
|
<h2 class="login-title">{{ title }}</h2>
|
|
2443
2958
|
|
|
2959
|
+
<!-- OTP Flow -->
|
|
2960
|
+
@if (isProviderEnabled('otp') && otpActive) {
|
|
2961
|
+
<!-- OTP Step 1: Identifier entry -->
|
|
2962
|
+
@if (otpStep === 'identifier') {
|
|
2963
|
+
<form (ngSubmit)="onOtpSend()" class="otp-form">
|
|
2964
|
+
<div class="form-group">
|
|
2965
|
+
<input
|
|
2966
|
+
[(ngModel)]="otpIdentifier"
|
|
2967
|
+
name="otpIdentifier"
|
|
2968
|
+
placeholder="Enter Email or Phone Number"
|
|
2969
|
+
type="text"
|
|
2970
|
+
required
|
|
2971
|
+
autocomplete="email tel"
|
|
2972
|
+
class="form-control"
|
|
2973
|
+
(input)="onOtpIdentifierChange()">
|
|
2974
|
+
@if (otpIdentifierHint) {
|
|
2975
|
+
<div class="field-hint">{{ otpIdentifierHint }}</div>
|
|
2976
|
+
}
|
|
2977
|
+
</div>
|
|
2978
|
+
<button
|
|
2979
|
+
type="submit"
|
|
2980
|
+
[disabled]="loading || !otpIdentifier.trim()"
|
|
2981
|
+
class="btn btn-primary btn-block">
|
|
2982
|
+
{{ loading ? 'Sending...' : 'Send OTP' }}
|
|
2983
|
+
</button>
|
|
2984
|
+
</form>
|
|
2985
|
+
}
|
|
2986
|
+
|
|
2987
|
+
<!-- OTP Step 2: Code entry -->
|
|
2988
|
+
@if (otpStep === 'code') {
|
|
2989
|
+
<div class="otp-code-section">
|
|
2990
|
+
<p class="otp-subtitle">
|
|
2991
|
+
Enter the 6-digit code sent to
|
|
2992
|
+
<strong>{{ otpMaskedIdentifier }}</strong>
|
|
2993
|
+
</p>
|
|
2994
|
+
<div class="otp-digits">
|
|
2995
|
+
@for (digit of otpDigits; track $index; let i = $index) {
|
|
2996
|
+
<input
|
|
2997
|
+
#otpInput
|
|
2998
|
+
type="text"
|
|
2999
|
+
inputmode="numeric"
|
|
3000
|
+
maxlength="1"
|
|
3001
|
+
class="otp-digit-input"
|
|
3002
|
+
[value]="otpDigits[i]"
|
|
3003
|
+
(input)="onOtpDigitInput($event, i)"
|
|
3004
|
+
(keydown)="onOtpDigitKeydown($event, i)"
|
|
3005
|
+
(paste)="onOtpPaste($event)">
|
|
3006
|
+
}
|
|
3007
|
+
</div>
|
|
3008
|
+
<div class="otp-actions">
|
|
3009
|
+
<button
|
|
3010
|
+
type="button"
|
|
3011
|
+
(click)="onOtpVerify()"
|
|
3012
|
+
[disabled]="loading || otpCode.length < 6"
|
|
3013
|
+
class="btn btn-primary btn-block">
|
|
3014
|
+
{{ loading ? 'Verifying...' : 'Verify' }}
|
|
3015
|
+
</button>
|
|
3016
|
+
</div>
|
|
3017
|
+
<div class="otp-resend">
|
|
3018
|
+
@if (otpResendCountdown > 0) {
|
|
3019
|
+
<span class="resend-timer">
|
|
3020
|
+
Resend in {{ formatCountdown(otpResendCountdown) }}
|
|
3021
|
+
</span>
|
|
3022
|
+
} @else {
|
|
3023
|
+
<a href="#" (click)="onOtpResend($event)" class="resend-link">
|
|
3024
|
+
Resend OTP
|
|
3025
|
+
</a>
|
|
3026
|
+
}
|
|
3027
|
+
</div>
|
|
3028
|
+
<div class="otp-back">
|
|
3029
|
+
<a href="#" (click)="onOtpBack($event)">
|
|
3030
|
+
Use a different email or phone
|
|
3031
|
+
</a>
|
|
3032
|
+
</div>
|
|
3033
|
+
</div>
|
|
3034
|
+
}
|
|
3035
|
+
|
|
3036
|
+
<!-- OTP Step 3: Registration (new user) -->
|
|
3037
|
+
@if (otpStep === 'register') {
|
|
3038
|
+
<div class="otp-register-section">
|
|
3039
|
+
<p class="otp-subtitle">
|
|
3040
|
+
Welcome! Enter your name to get started.
|
|
3041
|
+
</p>
|
|
3042
|
+
<form (ngSubmit)="onOtpRegister()" class="otp-form">
|
|
3043
|
+
<div class="form-group">
|
|
3044
|
+
<input
|
|
3045
|
+
[(ngModel)]="otpDisplayName"
|
|
3046
|
+
name="displayName"
|
|
3047
|
+
placeholder="Your Name"
|
|
3048
|
+
type="text"
|
|
3049
|
+
required
|
|
3050
|
+
class="form-control">
|
|
3051
|
+
</div>
|
|
3052
|
+
<button
|
|
3053
|
+
type="submit"
|
|
3054
|
+
[disabled]="loading || !otpDisplayName.trim()"
|
|
3055
|
+
class="btn btn-primary btn-block">
|
|
3056
|
+
{{ loading ? 'Creating account...' : 'Continue' }}
|
|
3057
|
+
</button>
|
|
3058
|
+
</form>
|
|
3059
|
+
</div>
|
|
3060
|
+
}
|
|
3061
|
+
|
|
3062
|
+
<!-- Divider before other providers -->
|
|
3063
|
+
@if (effectiveOauthProviders.length > 0 || isProviderEnabled('emailPassword')) {
|
|
3064
|
+
<div class="divider">
|
|
3065
|
+
<span>OR</span>
|
|
3066
|
+
</div>
|
|
3067
|
+
}
|
|
3068
|
+
}
|
|
3069
|
+
|
|
2444
3070
|
<!-- Email/Password Form (if enabled) -->
|
|
2445
|
-
@if (isProviderEnabled('emailPassword') && !useOAuth) {
|
|
3071
|
+
@if (isProviderEnabled('emailPassword') && !useOAuth && !otpActive) {
|
|
2446
3072
|
<form (ngSubmit)="onEmailLogin()" class="email-form">
|
|
2447
3073
|
<div class="form-group">
|
|
2448
3074
|
<input
|
|
@@ -2466,7 +3092,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
|
|
|
2466
3092
|
class="password-toggle"
|
|
2467
3093
|
(click)="showPassword = !showPassword"
|
|
2468
3094
|
[attr.aria-label]="showPassword ? 'Hide password' : 'Show password'">
|
|
2469
|
-
{{ showPassword ? '
|
|
3095
|
+
{{ showPassword ? '👁' : '👁‍🗨' }}
|
|
2470
3096
|
</button>
|
|
2471
3097
|
</div>
|
|
2472
3098
|
<button
|
|
@@ -2478,7 +3104,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
|
|
|
2478
3104
|
</form>
|
|
2479
3105
|
|
|
2480
3106
|
<!-- Divider -->
|
|
2481
|
-
@if (
|
|
3107
|
+
@if (effectiveOauthProviders.length > 0) {
|
|
2482
3108
|
<div class="divider">
|
|
2483
3109
|
<span>OR</span>
|
|
2484
3110
|
</div>
|
|
@@ -2486,9 +3112,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
|
|
|
2486
3112
|
}
|
|
2487
3113
|
|
|
2488
3114
|
<!-- OAuth Providers -->
|
|
2489
|
-
@if (
|
|
3115
|
+
@if (effectiveOauthProviders.length > 0 && !otpActive && (useOAuth || !isProviderEnabled('emailPassword'))) {
|
|
2490
3116
|
<div class="oauth-buttons">
|
|
2491
|
-
@for (provider of
|
|
3117
|
+
@for (provider of effectiveOauthProviders; track provider) {
|
|
2492
3118
|
<button
|
|
2493
3119
|
type="button"
|
|
2494
3120
|
(click)="onOAuthLogin(provider)"
|
|
@@ -2506,7 +3132,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
|
|
|
2506
3132
|
</div>
|
|
2507
3133
|
|
|
2508
3134
|
<!-- Switch to Email/Password -->
|
|
2509
|
-
@if (isProviderEnabled('emailPassword') &&
|
|
3135
|
+
@if (isProviderEnabled('emailPassword') && effectiveOauthProviders.length > 0) {
|
|
2510
3136
|
<div class="switch-method">
|
|
2511
3137
|
<a href="#" (click)="toggleAuthMethod($event)">
|
|
2512
3138
|
{{ useOAuth ? 'Use email/password instead' : 'Use OAuth instead' }}
|
|
@@ -2601,8 +3227,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
|
|
|
2601
3227
|
</div>
|
|
2602
3228
|
}
|
|
2603
3229
|
</div>
|
|
2604
|
-
`, styles: [".tenant-login-dialog{padding:24px;max-width:450px;position:relative}.login-title{margin:0 0 24px;font-size:24px;font-weight:500;text-align:center}.welcome-message{margin-bottom:16px;padding:12px;background:#e8f5e9;border-radius:4px;text-align:center;font-size:14px;color:#2e7d32}.selector-description{margin-bottom:20px;font-size:14px;color:#666;text-align:center}.email-form,.form-group{margin-bottom:16px}.password-group{position:relative}.form-control{width:100%;padding:12px;border:1px solid #ddd;border-radius:4px;font-size:14px;box-sizing:border-box}.password-input{padding-right:45px}.password-toggle{position:absolute;right:8px;top:50%;transform:translateY(-50%);background:none;border:none;cursor:pointer;font-size:18px;padding:8px;line-height:1;opacity:.6;transition:opacity .2s}.password-toggle:hover{opacity:1}.password-toggle:focus{outline:2px solid #4285f4;outline-offset:2px;border-radius:4px}.form-control:focus{outline:none;border-color:#4285f4}.btn{padding:12px 24px;border:none;border-radius:4px;font-size:14px;font-weight:500;cursor:pointer;transition:background-color .2s}.btn:disabled{opacity:.6;cursor:not-allowed}.btn-block{width:100%}.btn-primary{background-color:#4285f4;color:#fff}.btn-primary:hover:not(:disabled){background-color:#357ae8}.divider{margin:16px 0;text-align:center;position:relative}.divider:before{content:\"\";position:absolute;top:50%;left:0;right:0;height:1px;background:#ddd}.divider span{background:#fff;padding:0 12px;position:relative;color:#666;font-size:12px}.oauth-buttons{display:flex;flex-direction:column;gap:12px}.btn-oauth{width:100%;background:#fff;color:#333;border:1px solid #ddd;display:flex;align-items:center;justify-content:center;gap:8px}.btn-oauth:hover:not(:disabled){background:#f8f8f8}.btn-google{border-color:#4285f4}.btn-linkedin{border-color:#0077b5}.btn-apple{border-color:#000}.btn-microsoft{border-color:#00a4ef}.btn-github{border-color:#333}.btn-zoho{background-color:#f0483e;color:#fff;border:1px solid #d63b32}.btn-zoho:hover{background-color:#d63b32}.oauth-icon{font-size:18px}.switch-method{margin-top:12px;text-align:center;font-size:14px}.switch-method a{color:#4285f4;text-decoration:none}.switch-method a:hover{text-decoration:underline}.tenant-list{margin-bottom:20px;display:flex;flex-direction:column;gap:12px}.tenant-item{display:flex;align-items:flex-start;gap:12px;padding:16px;border:2px solid #e0e0e0;border-radius:6px;cursor:pointer;transition:all .2s}.tenant-item:hover{border-color:#4285f4;background:#f8f9ff}.tenant-item.selected{border-color:#4285f4;background:#e8f0fe}.tenant-radio{flex-shrink:0;padding-top:2px}.tenant-radio input[type=radio]{width:18px;height:18px;cursor:pointer}.tenant-info{flex:1}.tenant-name{font-size:16px;font-weight:500;color:#333;margin-bottom:4px}.tenant-meta{font-size:13px;color:#666}.tenant-role{font-weight:500;color:#4285f4}.tenant-separator{margin:0 6px}.error-message{margin-top:16px;padding:12px;background:#fee;color:#c33;border-radius:4px;font-size:14px}.loading-overlay{position:absolute;inset:0;background:#fffc;display:flex;align-items:center;justify-content:center}.spinner{width:40px;height:40px;border:4px solid #f3f3f3;border-top:4px solid #4285f4;border-radius:50%;animation:spin 1s linear infinite}@keyframes spin{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.register-link{margin-top:16px;text-align:center;font-size:14px;color:#666}.register-link a{color:#4285f4;text-decoration:none}.register-link a:hover{text-decoration:underline}.create-tenant-link{margin-top:16px;padding-top:16px;border-top:1px solid #e0e0e0;text-align:center;font-size:14px;color:#666}.create-tenant-link a{color:#4285f4;text-decoration:none;font-weight:500}.create-tenant-link a:hover{text-decoration:underline}\n"] }]
|
|
2605
|
-
}], ctorParameters: () => [{ type: AuthService }, { type: ProviderRegistryService }], propDecorators: {
|
|
3230
|
+
`, 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,.otp-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}.field-hint{margin-top:4px;font-size:12px;color:#888}.btn{padding:12px 24px;border:none;border-radius:4px;font-size:14px;font-weight:500;cursor:pointer;transition:background-color .2s}.btn:disabled{opacity:.6;cursor:not-allowed}.btn-block{width:100%}.btn-primary{background-color:#4285f4;color:#fff}.btn-primary:hover:not(:disabled){background-color:#357ae8}.divider{margin:16px 0;text-align:center;position:relative}.divider:before{content:\"\";position:absolute;top:50%;left:0;right:0;height:1px;background:#ddd}.divider span{background:#fff;padding:0 12px;position:relative;color:#666;font-size:12px}.oauth-buttons{display:flex;flex-direction:column;gap:12px}.btn-oauth{width:100%;background:#fff;color:#333;border:1px solid #ddd;display:flex;align-items:center;justify-content:center;gap:8px}.btn-oauth:hover:not(:disabled){background:#f8f8f8}.btn-google{border-color:#4285f4}.btn-linkedin{border-color:#0077b5}.btn-apple{border-color:#000}.btn-microsoft{border-color:#00a4ef}.btn-github{border-color:#333}.btn-zoho{background-color:#f0483e;color:#fff;border:1px solid #d63b32}.btn-zoho:hover{background-color:#d63b32}.oauth-icon{font-size:18px}.switch-method{margin-top:12px;text-align:center;font-size:14px}.switch-method a{color:#4285f4;text-decoration:none}.switch-method a:hover{text-decoration:underline}.otp-subtitle{text-align:center;font-size:14px;color:#555;margin-bottom:20px}.otp-digits{display:flex;justify-content:center;gap:8px;margin-bottom:20px}.otp-digit-input{width:44px;height:52px;text-align:center;font-size:22px;font-weight:600;border:2px solid #ddd;border-radius:8px;outline:none;transition:border-color .2s;box-sizing:border-box}.otp-digit-input:focus{border-color:#4285f4}.otp-actions{margin-bottom:12px}.otp-resend{text-align:center;font-size:14px;margin-bottom:8px}.resend-timer{color:#888}.resend-link{color:#4285f4;text-decoration:none;cursor:pointer}.resend-link:hover{text-decoration:underline}.otp-back{text-align:center;font-size:13px}.otp-back a{color:#888;text-decoration:none}.otp-back a:hover{text-decoration:underline;color:#4285f4}.otp-register-section .otp-subtitle{color:#2e7d32}.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"] }]
|
|
3231
|
+
}], ctorParameters: () => [{ type: AuthService }, { type: ProviderRegistryService }], propDecorators: { otpInputs: [{
|
|
3232
|
+
type: ViewChildren,
|
|
3233
|
+
args: ['otpInput']
|
|
3234
|
+
}], title: [{
|
|
2606
3235
|
type: Input
|
|
2607
3236
|
}], providers: [{
|
|
2608
3237
|
type: Input
|