@progalaxyelabs/ngx-stonescriptphp-client 1.22.1 → 1.23.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { 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,254 @@ 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}` : `+${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
|
+
if (this.loading)
|
|
2476
|
+
return;
|
|
2477
|
+
const code = this.otpCode;
|
|
2478
|
+
if (code.length < 6) {
|
|
2479
|
+
this.error = 'Please enter the complete 6-digit code';
|
|
2480
|
+
return;
|
|
2481
|
+
}
|
|
2482
|
+
this.loading = true;
|
|
2483
|
+
this.error = '';
|
|
2484
|
+
try {
|
|
2485
|
+
const verifyResult = await this.auth.verifyOtp(this.otpNormalizedIdentifier, code);
|
|
2486
|
+
if (!verifyResult.success) {
|
|
2487
|
+
this.error = 'Invalid code. Please try again.';
|
|
2488
|
+
return;
|
|
2489
|
+
}
|
|
2490
|
+
this.otpVerifiedToken = verifyResult.verified_token;
|
|
2491
|
+
// Auto-attempt login
|
|
2492
|
+
const loginResult = await this.auth.identityLogin(this.otpVerifiedToken);
|
|
2493
|
+
if (loginResult.success) {
|
|
2494
|
+
// Existing user — proceed with standard post-auth flow
|
|
2495
|
+
await this.handlePostAuthFlow(loginResult);
|
|
2496
|
+
return;
|
|
2497
|
+
}
|
|
2498
|
+
if (loginResult.message === 'identity_not_found') {
|
|
2499
|
+
// New user — show registration form
|
|
2500
|
+
this.otpStep = 'register';
|
|
2501
|
+
return;
|
|
2502
|
+
}
|
|
2503
|
+
// Other login error
|
|
2504
|
+
this.error = loginResult.message || 'Login failed. Please try again.';
|
|
2505
|
+
}
|
|
2506
|
+
catch (err) {
|
|
2507
|
+
this.error = err.message || 'Verification failed';
|
|
2508
|
+
}
|
|
2509
|
+
finally {
|
|
2510
|
+
this.loading = false;
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
/** Register a new identity after OTP verification */
|
|
2514
|
+
async onOtpRegister() {
|
|
2515
|
+
const name = this.otpDisplayName.trim();
|
|
2516
|
+
if (!name) {
|
|
2517
|
+
this.error = 'Please enter your name';
|
|
2518
|
+
return;
|
|
2519
|
+
}
|
|
2520
|
+
this.loading = true;
|
|
2521
|
+
this.error = '';
|
|
2522
|
+
try {
|
|
2523
|
+
const result = await this.auth.identityRegister(this.otpVerifiedToken, name);
|
|
2524
|
+
if (!result.success) {
|
|
2525
|
+
// If token expired, restart OTP flow
|
|
2526
|
+
if (result.message?.includes('expired') || result.message?.includes('Invalid')) {
|
|
2527
|
+
this.error = 'Session expired. Please verify again.';
|
|
2528
|
+
this.otpStep = 'identifier';
|
|
2529
|
+
this.otpVerifiedToken = '';
|
|
2530
|
+
this.otpDisplayName = '';
|
|
2531
|
+
return;
|
|
2532
|
+
}
|
|
2533
|
+
this.error = result.message || 'Registration failed';
|
|
2534
|
+
return;
|
|
2535
|
+
}
|
|
2536
|
+
// New identity created — emit onboarding event
|
|
2537
|
+
const identifierType = this.detectIdentifierType(this.otpIdentifier.trim());
|
|
2538
|
+
this.needsOnboarding.emit({
|
|
2539
|
+
auth_method: 'otp',
|
|
2540
|
+
is_new_identity: true,
|
|
2541
|
+
identity: {
|
|
2542
|
+
email: identifierType === 'email' ? this.otpNormalizedIdentifier : '',
|
|
2543
|
+
phone: identifierType === 'phone' ? this.otpNormalizedIdentifier : undefined,
|
|
2544
|
+
display_name: name,
|
|
2545
|
+
},
|
|
2546
|
+
});
|
|
2547
|
+
}
|
|
2548
|
+
catch (err) {
|
|
2549
|
+
this.error = err.message || 'Registration failed';
|
|
2550
|
+
}
|
|
2551
|
+
finally {
|
|
2552
|
+
this.loading = false;
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2555
|
+
/** Resend the OTP */
|
|
2556
|
+
onOtpResend(event) {
|
|
2557
|
+
event.preventDefault();
|
|
2558
|
+
this.onOtpSend();
|
|
2559
|
+
}
|
|
2560
|
+
/** Go back to identifier entry */
|
|
2561
|
+
onOtpBack(event) {
|
|
2562
|
+
event.preventDefault();
|
|
2563
|
+
this.otpStep = 'identifier';
|
|
2564
|
+
this.otpDigits = ['', '', '', '', '', ''];
|
|
2565
|
+
this.otpVerifiedToken = '';
|
|
2566
|
+
this.error = '';
|
|
2567
|
+
this.clearResendTimer();
|
|
2568
|
+
}
|
|
2569
|
+
// ── OTP digit input handling ─────────────────────────────────────────────
|
|
2570
|
+
onOtpDigitInput(event, index) {
|
|
2571
|
+
const input = event.target;
|
|
2572
|
+
const value = input.value.replace(/\D/g, '');
|
|
2573
|
+
this.otpDigits[index] = value ? value[0] : '';
|
|
2574
|
+
input.value = this.otpDigits[index];
|
|
2575
|
+
// Auto-advance to next input
|
|
2576
|
+
if (value && index < 5) {
|
|
2577
|
+
this.focusOtpInput(index + 1);
|
|
2578
|
+
}
|
|
2579
|
+
// Auto-verify when all 6 digits entered
|
|
2580
|
+
if (this.otpCode.length === 6) {
|
|
2581
|
+
this.onOtpVerify();
|
|
2582
|
+
}
|
|
2583
|
+
}
|
|
2584
|
+
onOtpDigitKeydown(event, index) {
|
|
2585
|
+
if (event.key === 'Backspace' && !this.otpDigits[index] && index > 0) {
|
|
2586
|
+
this.focusOtpInput(index - 1);
|
|
2587
|
+
}
|
|
2588
|
+
}
|
|
2589
|
+
onOtpPaste(event) {
|
|
2590
|
+
event.preventDefault();
|
|
2591
|
+
const pasted = (event.clipboardData?.getData('text') || '').replace(/\D/g, '').slice(0, 6);
|
|
2592
|
+
for (let i = 0; i < 6; i++) {
|
|
2593
|
+
this.otpDigits[i] = pasted[i] || '';
|
|
2594
|
+
}
|
|
2595
|
+
// Update all input elements
|
|
2596
|
+
const inputs = this.otpInputs?.toArray();
|
|
2597
|
+
if (inputs) {
|
|
2598
|
+
for (let i = 0; i < 6; i++) {
|
|
2599
|
+
inputs[i].nativeElement.value = this.otpDigits[i];
|
|
2600
|
+
}
|
|
2601
|
+
}
|
|
2602
|
+
if (pasted.length >= 6) {
|
|
2603
|
+
this.onOtpVerify();
|
|
2604
|
+
}
|
|
2605
|
+
else {
|
|
2606
|
+
this.focusOtpInput(Math.min(pasted.length, 5));
|
|
2607
|
+
}
|
|
2608
|
+
}
|
|
2609
|
+
get otpCode() {
|
|
2610
|
+
return this.otpDigits.join('');
|
|
2611
|
+
}
|
|
2612
|
+
// ── OTP helpers ──────────────────────────────────────────────────────────
|
|
2613
|
+
detectIdentifierType(value) {
|
|
2614
|
+
if (value.includes('@')) {
|
|
2615
|
+
// Basic email validation
|
|
2616
|
+
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value) ? 'email' : null;
|
|
2617
|
+
}
|
|
2618
|
+
const digits = value.replace(/\D/g, '');
|
|
2619
|
+
if (digits.length >= 10 && digits.length <= 15) {
|
|
2620
|
+
return 'phone';
|
|
2621
|
+
}
|
|
2622
|
+
return null;
|
|
2623
|
+
}
|
|
2624
|
+
focusOtpInput(index) {
|
|
2625
|
+
const inputs = this.otpInputs?.toArray();
|
|
2626
|
+
if (inputs && inputs[index]) {
|
|
2627
|
+
inputs[index].nativeElement.focus();
|
|
2628
|
+
inputs[index].nativeElement.select();
|
|
2629
|
+
}
|
|
2630
|
+
}
|
|
2631
|
+
startResendCountdown(seconds) {
|
|
2632
|
+
this.clearResendTimer();
|
|
2633
|
+
this.otpResendCountdown = seconds;
|
|
2634
|
+
this.otpResendTimer = setInterval(() => {
|
|
2635
|
+
this.otpResendCountdown--;
|
|
2636
|
+
if (this.otpResendCountdown <= 0) {
|
|
2637
|
+
this.clearResendTimer();
|
|
2638
|
+
}
|
|
2639
|
+
}, 1000);
|
|
2640
|
+
}
|
|
2641
|
+
clearResendTimer() {
|
|
2642
|
+
if (this.otpResendTimer) {
|
|
2643
|
+
clearInterval(this.otpResendTimer);
|
|
2644
|
+
this.otpResendTimer = null;
|
|
2645
|
+
}
|
|
2646
|
+
this.otpResendCountdown = 0;
|
|
2647
|
+
}
|
|
2648
|
+
formatCountdown(seconds) {
|
|
2649
|
+
const m = Math.floor(seconds / 60);
|
|
2650
|
+
const s = seconds % 60;
|
|
2651
|
+
return `${m}:${s.toString().padStart(2, '0')}`;
|
|
2652
|
+
}
|
|
2653
|
+
// ── Formatting helpers ───────────────────────────────────────────────────
|
|
2240
2654
|
formatRole(role) {
|
|
2241
2655
|
return role.charAt(0).toUpperCase() + role.slice(1);
|
|
2242
2656
|
}
|
|
@@ -2265,14 +2679,125 @@ class TenantLoginComponent {
|
|
|
2265
2679
|
this.createTenant.emit();
|
|
2266
2680
|
}
|
|
2267
2681
|
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: `
|
|
2682
|
+
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
2683
|
<div class="tenant-login-dialog">
|
|
2270
2684
|
@if (!showingTenantSelector) {
|
|
2271
2685
|
<!-- Step 1: Authentication -->
|
|
2272
2686
|
<h2 class="login-title">{{ title }}</h2>
|
|
2273
2687
|
|
|
2688
|
+
<!-- OTP Flow -->
|
|
2689
|
+
@if (isProviderEnabled('otp') && otpActive) {
|
|
2690
|
+
<!-- OTP Step 1: Identifier entry -->
|
|
2691
|
+
@if (otpStep === 'identifier') {
|
|
2692
|
+
<form (ngSubmit)="onOtpSend()" class="otp-form">
|
|
2693
|
+
<div class="form-group">
|
|
2694
|
+
<input
|
|
2695
|
+
[(ngModel)]="otpIdentifier"
|
|
2696
|
+
name="otpIdentifier"
|
|
2697
|
+
placeholder="Enter Email or Phone Number"
|
|
2698
|
+
type="text"
|
|
2699
|
+
required
|
|
2700
|
+
autocomplete="email tel"
|
|
2701
|
+
class="form-control"
|
|
2702
|
+
(input)="onOtpIdentifierChange()">
|
|
2703
|
+
@if (otpIdentifierHint) {
|
|
2704
|
+
<div class="field-hint">{{ otpIdentifierHint }}</div>
|
|
2705
|
+
}
|
|
2706
|
+
</div>
|
|
2707
|
+
<button
|
|
2708
|
+
type="submit"
|
|
2709
|
+
[disabled]="loading || !otpIdentifier.trim()"
|
|
2710
|
+
class="btn btn-primary btn-block">
|
|
2711
|
+
{{ loading ? 'Sending...' : 'Send OTP' }}
|
|
2712
|
+
</button>
|
|
2713
|
+
</form>
|
|
2714
|
+
}
|
|
2715
|
+
|
|
2716
|
+
<!-- OTP Step 2: Code entry -->
|
|
2717
|
+
@if (otpStep === 'code') {
|
|
2718
|
+
<div class="otp-code-section">
|
|
2719
|
+
<p class="otp-subtitle">
|
|
2720
|
+
Enter the 6-digit code sent to
|
|
2721
|
+
<strong>{{ otpMaskedIdentifier }}</strong>
|
|
2722
|
+
</p>
|
|
2723
|
+
<div class="otp-digits">
|
|
2724
|
+
@for (digit of otpDigits; track $index; let i = $index) {
|
|
2725
|
+
<input
|
|
2726
|
+
#otpInput
|
|
2727
|
+
type="text"
|
|
2728
|
+
inputmode="numeric"
|
|
2729
|
+
maxlength="1"
|
|
2730
|
+
class="otp-digit-input"
|
|
2731
|
+
[value]="otpDigits[i]"
|
|
2732
|
+
(input)="onOtpDigitInput($event, i)"
|
|
2733
|
+
(keydown)="onOtpDigitKeydown($event, i)"
|
|
2734
|
+
(paste)="onOtpPaste($event)">
|
|
2735
|
+
}
|
|
2736
|
+
</div>
|
|
2737
|
+
<div class="otp-actions">
|
|
2738
|
+
<button
|
|
2739
|
+
type="button"
|
|
2740
|
+
(click)="onOtpVerify()"
|
|
2741
|
+
[disabled]="loading || otpCode.length < 6"
|
|
2742
|
+
class="btn btn-primary btn-block">
|
|
2743
|
+
{{ loading ? 'Verifying...' : 'Verify' }}
|
|
2744
|
+
</button>
|
|
2745
|
+
</div>
|
|
2746
|
+
<div class="otp-resend">
|
|
2747
|
+
@if (otpResendCountdown > 0) {
|
|
2748
|
+
<span class="resend-timer">
|
|
2749
|
+
Resend in {{ formatCountdown(otpResendCountdown) }}
|
|
2750
|
+
</span>
|
|
2751
|
+
} @else {
|
|
2752
|
+
<a href="#" (click)="onOtpResend($event)" class="resend-link">
|
|
2753
|
+
Resend OTP
|
|
2754
|
+
</a>
|
|
2755
|
+
}
|
|
2756
|
+
</div>
|
|
2757
|
+
<div class="otp-back">
|
|
2758
|
+
<a href="#" (click)="onOtpBack($event)">
|
|
2759
|
+
Use a different email or phone
|
|
2760
|
+
</a>
|
|
2761
|
+
</div>
|
|
2762
|
+
</div>
|
|
2763
|
+
}
|
|
2764
|
+
|
|
2765
|
+
<!-- OTP Step 3: Registration (new user) -->
|
|
2766
|
+
@if (otpStep === 'register') {
|
|
2767
|
+
<div class="otp-register-section">
|
|
2768
|
+
<p class="otp-subtitle">
|
|
2769
|
+
Welcome! Enter your name to get started.
|
|
2770
|
+
</p>
|
|
2771
|
+
<form (ngSubmit)="onOtpRegister()" class="otp-form">
|
|
2772
|
+
<div class="form-group">
|
|
2773
|
+
<input
|
|
2774
|
+
[(ngModel)]="otpDisplayName"
|
|
2775
|
+
name="displayName"
|
|
2776
|
+
placeholder="Your Name"
|
|
2777
|
+
type="text"
|
|
2778
|
+
required
|
|
2779
|
+
class="form-control">
|
|
2780
|
+
</div>
|
|
2781
|
+
<button
|
|
2782
|
+
type="submit"
|
|
2783
|
+
[disabled]="loading || !otpDisplayName.trim()"
|
|
2784
|
+
class="btn btn-primary btn-block">
|
|
2785
|
+
{{ loading ? 'Creating account...' : 'Continue' }}
|
|
2786
|
+
</button>
|
|
2787
|
+
</form>
|
|
2788
|
+
</div>
|
|
2789
|
+
}
|
|
2790
|
+
|
|
2791
|
+
<!-- Divider before other providers -->
|
|
2792
|
+
@if (effectiveOauthProviders.length > 0 || isProviderEnabled('emailPassword')) {
|
|
2793
|
+
<div class="divider">
|
|
2794
|
+
<span>OR</span>
|
|
2795
|
+
</div>
|
|
2796
|
+
}
|
|
2797
|
+
}
|
|
2798
|
+
|
|
2274
2799
|
<!-- Email/Password Form (if enabled) -->
|
|
2275
|
-
@if (isProviderEnabled('emailPassword') && !useOAuth) {
|
|
2800
|
+
@if (isProviderEnabled('emailPassword') && !useOAuth && !otpActive) {
|
|
2276
2801
|
<form (ngSubmit)="onEmailLogin()" class="email-form">
|
|
2277
2802
|
<div class="form-group">
|
|
2278
2803
|
<input
|
|
@@ -2296,7 +2821,7 @@ class TenantLoginComponent {
|
|
|
2296
2821
|
class="password-toggle"
|
|
2297
2822
|
(click)="showPassword = !showPassword"
|
|
2298
2823
|
[attr.aria-label]="showPassword ? 'Hide password' : 'Show password'">
|
|
2299
|
-
{{ showPassword ? '
|
|
2824
|
+
{{ showPassword ? '👁' : '👁‍🗨' }}
|
|
2300
2825
|
</button>
|
|
2301
2826
|
</div>
|
|
2302
2827
|
<button
|
|
@@ -2308,7 +2833,7 @@ class TenantLoginComponent {
|
|
|
2308
2833
|
</form>
|
|
2309
2834
|
|
|
2310
2835
|
<!-- Divider -->
|
|
2311
|
-
@if (
|
|
2836
|
+
@if (effectiveOauthProviders.length > 0) {
|
|
2312
2837
|
<div class="divider">
|
|
2313
2838
|
<span>OR</span>
|
|
2314
2839
|
</div>
|
|
@@ -2316,9 +2841,9 @@ class TenantLoginComponent {
|
|
|
2316
2841
|
}
|
|
2317
2842
|
|
|
2318
2843
|
<!-- OAuth Providers -->
|
|
2319
|
-
@if (
|
|
2844
|
+
@if (effectiveOauthProviders.length > 0 && !otpActive && (useOAuth || !isProviderEnabled('emailPassword'))) {
|
|
2320
2845
|
<div class="oauth-buttons">
|
|
2321
|
-
@for (provider of
|
|
2846
|
+
@for (provider of effectiveOauthProviders; track provider) {
|
|
2322
2847
|
<button
|
|
2323
2848
|
type="button"
|
|
2324
2849
|
(click)="onOAuthLogin(provider)"
|
|
@@ -2336,7 +2861,7 @@ class TenantLoginComponent {
|
|
|
2336
2861
|
</div>
|
|
2337
2862
|
|
|
2338
2863
|
<!-- Switch to Email/Password -->
|
|
2339
|
-
@if (isProviderEnabled('emailPassword') &&
|
|
2864
|
+
@if (isProviderEnabled('emailPassword') && effectiveOauthProviders.length > 0) {
|
|
2340
2865
|
<div class="switch-method">
|
|
2341
2866
|
<a href="#" (click)="toggleAuthMethod($event)">
|
|
2342
2867
|
{{ useOAuth ? 'Use email/password instead' : 'Use OAuth instead' }}
|
|
@@ -2431,7 +2956,7 @@ class TenantLoginComponent {
|
|
|
2431
2956
|
</div>
|
|
2432
2957
|
}
|
|
2433
2958
|
</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"] }] });
|
|
2959
|
+
`, 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
2960
|
}
|
|
2436
2961
|
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImport: i0, type: TenantLoginComponent, decorators: [{
|
|
2437
2962
|
type: Component,
|
|
@@ -2441,8 +2966,119 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
|
|
|
2441
2966
|
<!-- Step 1: Authentication -->
|
|
2442
2967
|
<h2 class="login-title">{{ title }}</h2>
|
|
2443
2968
|
|
|
2969
|
+
<!-- OTP Flow -->
|
|
2970
|
+
@if (isProviderEnabled('otp') && otpActive) {
|
|
2971
|
+
<!-- OTP Step 1: Identifier entry -->
|
|
2972
|
+
@if (otpStep === 'identifier') {
|
|
2973
|
+
<form (ngSubmit)="onOtpSend()" class="otp-form">
|
|
2974
|
+
<div class="form-group">
|
|
2975
|
+
<input
|
|
2976
|
+
[(ngModel)]="otpIdentifier"
|
|
2977
|
+
name="otpIdentifier"
|
|
2978
|
+
placeholder="Enter Email or Phone Number"
|
|
2979
|
+
type="text"
|
|
2980
|
+
required
|
|
2981
|
+
autocomplete="email tel"
|
|
2982
|
+
class="form-control"
|
|
2983
|
+
(input)="onOtpIdentifierChange()">
|
|
2984
|
+
@if (otpIdentifierHint) {
|
|
2985
|
+
<div class="field-hint">{{ otpIdentifierHint }}</div>
|
|
2986
|
+
}
|
|
2987
|
+
</div>
|
|
2988
|
+
<button
|
|
2989
|
+
type="submit"
|
|
2990
|
+
[disabled]="loading || !otpIdentifier.trim()"
|
|
2991
|
+
class="btn btn-primary btn-block">
|
|
2992
|
+
{{ loading ? 'Sending...' : 'Send OTP' }}
|
|
2993
|
+
</button>
|
|
2994
|
+
</form>
|
|
2995
|
+
}
|
|
2996
|
+
|
|
2997
|
+
<!-- OTP Step 2: Code entry -->
|
|
2998
|
+
@if (otpStep === 'code') {
|
|
2999
|
+
<div class="otp-code-section">
|
|
3000
|
+
<p class="otp-subtitle">
|
|
3001
|
+
Enter the 6-digit code sent to
|
|
3002
|
+
<strong>{{ otpMaskedIdentifier }}</strong>
|
|
3003
|
+
</p>
|
|
3004
|
+
<div class="otp-digits">
|
|
3005
|
+
@for (digit of otpDigits; track $index; let i = $index) {
|
|
3006
|
+
<input
|
|
3007
|
+
#otpInput
|
|
3008
|
+
type="text"
|
|
3009
|
+
inputmode="numeric"
|
|
3010
|
+
maxlength="1"
|
|
3011
|
+
class="otp-digit-input"
|
|
3012
|
+
[value]="otpDigits[i]"
|
|
3013
|
+
(input)="onOtpDigitInput($event, i)"
|
|
3014
|
+
(keydown)="onOtpDigitKeydown($event, i)"
|
|
3015
|
+
(paste)="onOtpPaste($event)">
|
|
3016
|
+
}
|
|
3017
|
+
</div>
|
|
3018
|
+
<div class="otp-actions">
|
|
3019
|
+
<button
|
|
3020
|
+
type="button"
|
|
3021
|
+
(click)="onOtpVerify()"
|
|
3022
|
+
[disabled]="loading || otpCode.length < 6"
|
|
3023
|
+
class="btn btn-primary btn-block">
|
|
3024
|
+
{{ loading ? 'Verifying...' : 'Verify' }}
|
|
3025
|
+
</button>
|
|
3026
|
+
</div>
|
|
3027
|
+
<div class="otp-resend">
|
|
3028
|
+
@if (otpResendCountdown > 0) {
|
|
3029
|
+
<span class="resend-timer">
|
|
3030
|
+
Resend in {{ formatCountdown(otpResendCountdown) }}
|
|
3031
|
+
</span>
|
|
3032
|
+
} @else {
|
|
3033
|
+
<a href="#" (click)="onOtpResend($event)" class="resend-link">
|
|
3034
|
+
Resend OTP
|
|
3035
|
+
</a>
|
|
3036
|
+
}
|
|
3037
|
+
</div>
|
|
3038
|
+
<div class="otp-back">
|
|
3039
|
+
<a href="#" (click)="onOtpBack($event)">
|
|
3040
|
+
Use a different email or phone
|
|
3041
|
+
</a>
|
|
3042
|
+
</div>
|
|
3043
|
+
</div>
|
|
3044
|
+
}
|
|
3045
|
+
|
|
3046
|
+
<!-- OTP Step 3: Registration (new user) -->
|
|
3047
|
+
@if (otpStep === 'register') {
|
|
3048
|
+
<div class="otp-register-section">
|
|
3049
|
+
<p class="otp-subtitle">
|
|
3050
|
+
Welcome! Enter your name to get started.
|
|
3051
|
+
</p>
|
|
3052
|
+
<form (ngSubmit)="onOtpRegister()" class="otp-form">
|
|
3053
|
+
<div class="form-group">
|
|
3054
|
+
<input
|
|
3055
|
+
[(ngModel)]="otpDisplayName"
|
|
3056
|
+
name="displayName"
|
|
3057
|
+
placeholder="Your Name"
|
|
3058
|
+
type="text"
|
|
3059
|
+
required
|
|
3060
|
+
class="form-control">
|
|
3061
|
+
</div>
|
|
3062
|
+
<button
|
|
3063
|
+
type="submit"
|
|
3064
|
+
[disabled]="loading || !otpDisplayName.trim()"
|
|
3065
|
+
class="btn btn-primary btn-block">
|
|
3066
|
+
{{ loading ? 'Creating account...' : 'Continue' }}
|
|
3067
|
+
</button>
|
|
3068
|
+
</form>
|
|
3069
|
+
</div>
|
|
3070
|
+
}
|
|
3071
|
+
|
|
3072
|
+
<!-- Divider before other providers -->
|
|
3073
|
+
@if (effectiveOauthProviders.length > 0 || isProviderEnabled('emailPassword')) {
|
|
3074
|
+
<div class="divider">
|
|
3075
|
+
<span>OR</span>
|
|
3076
|
+
</div>
|
|
3077
|
+
}
|
|
3078
|
+
}
|
|
3079
|
+
|
|
2444
3080
|
<!-- Email/Password Form (if enabled) -->
|
|
2445
|
-
@if (isProviderEnabled('emailPassword') && !useOAuth) {
|
|
3081
|
+
@if (isProviderEnabled('emailPassword') && !useOAuth && !otpActive) {
|
|
2446
3082
|
<form (ngSubmit)="onEmailLogin()" class="email-form">
|
|
2447
3083
|
<div class="form-group">
|
|
2448
3084
|
<input
|
|
@@ -2466,7 +3102,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
|
|
|
2466
3102
|
class="password-toggle"
|
|
2467
3103
|
(click)="showPassword = !showPassword"
|
|
2468
3104
|
[attr.aria-label]="showPassword ? 'Hide password' : 'Show password'">
|
|
2469
|
-
{{ showPassword ? '
|
|
3105
|
+
{{ showPassword ? '👁' : '👁‍🗨' }}
|
|
2470
3106
|
</button>
|
|
2471
3107
|
</div>
|
|
2472
3108
|
<button
|
|
@@ -2478,7 +3114,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
|
|
|
2478
3114
|
</form>
|
|
2479
3115
|
|
|
2480
3116
|
<!-- Divider -->
|
|
2481
|
-
@if (
|
|
3117
|
+
@if (effectiveOauthProviders.length > 0) {
|
|
2482
3118
|
<div class="divider">
|
|
2483
3119
|
<span>OR</span>
|
|
2484
3120
|
</div>
|
|
@@ -2486,9 +3122,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
|
|
|
2486
3122
|
}
|
|
2487
3123
|
|
|
2488
3124
|
<!-- OAuth Providers -->
|
|
2489
|
-
@if (
|
|
3125
|
+
@if (effectiveOauthProviders.length > 0 && !otpActive && (useOAuth || !isProviderEnabled('emailPassword'))) {
|
|
2490
3126
|
<div class="oauth-buttons">
|
|
2491
|
-
@for (provider of
|
|
3127
|
+
@for (provider of effectiveOauthProviders; track provider) {
|
|
2492
3128
|
<button
|
|
2493
3129
|
type="button"
|
|
2494
3130
|
(click)="onOAuthLogin(provider)"
|
|
@@ -2506,7 +3142,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
|
|
|
2506
3142
|
</div>
|
|
2507
3143
|
|
|
2508
3144
|
<!-- Switch to Email/Password -->
|
|
2509
|
-
@if (isProviderEnabled('emailPassword') &&
|
|
3145
|
+
@if (isProviderEnabled('emailPassword') && effectiveOauthProviders.length > 0) {
|
|
2510
3146
|
<div class="switch-method">
|
|
2511
3147
|
<a href="#" (click)="toggleAuthMethod($event)">
|
|
2512
3148
|
{{ useOAuth ? 'Use email/password instead' : 'Use OAuth instead' }}
|
|
@@ -2601,8 +3237,11 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.17", ngImpo
|
|
|
2601
3237
|
</div>
|
|
2602
3238
|
}
|
|
2603
3239
|
</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: {
|
|
3240
|
+
`, 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"] }]
|
|
3241
|
+
}], ctorParameters: () => [{ type: AuthService }, { type: ProviderRegistryService }], propDecorators: { otpInputs: [{
|
|
3242
|
+
type: ViewChildren,
|
|
3243
|
+
args: ['otpInput']
|
|
3244
|
+
}], title: [{
|
|
2606
3245
|
type: Input
|
|
2607
3246
|
}], providers: [{
|
|
2608
3247
|
type: Input
|