Refactor code structure for improved readability and maintainability
This commit is contained in:
@@ -1,35 +1,124 @@
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { Observable, of } from 'rxjs';
|
||||
import { inject, Injectable, PLATFORM_ID } from '@angular/core';
|
||||
import { BehaviorSubject, map, Observable, of } from 'rxjs';
|
||||
import { environment } from '../../environments/environment';
|
||||
import { isPlatformBrowser } from '@angular/common';
|
||||
import { Router } from '@angular/router';
|
||||
|
||||
interface ValidateOtpResponse {
|
||||
accessToken: string;
|
||||
}
|
||||
|
||||
interface RefreshTokenResponse {
|
||||
accessToken: string;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AuthService {
|
||||
private readonly baseUrl = (environment.apiUrl ?? '').replace(/\/+$/, '');
|
||||
private accessToken: string | null = null;
|
||||
private platformId = inject(PLATFORM_ID);
|
||||
private accessTokenSub = new BehaviorSubject<string | null>(null);
|
||||
tokenReady$ = new BehaviorSubject<boolean>(false);
|
||||
http = inject(HttpClient);
|
||||
router = inject(Router);
|
||||
|
||||
constructor(private http: HttpClient) {}
|
||||
private readonly storageKey = 'accessToken';
|
||||
|
||||
private api(path: string) {
|
||||
return `${this.baseUrl}${path.startsWith('/') ? '' : '/'}${path}`;
|
||||
}
|
||||
|
||||
sendOtp(email: string): Observable<any> {
|
||||
// Call on app start, or from guard
|
||||
async ensureTokenReady(): Promise<void>{
|
||||
if(this.tokenReady$.value) return;
|
||||
|
||||
const stored = this.safeGetToken();
|
||||
|
||||
// try to restore from storage
|
||||
if(stored){
|
||||
this.accessTokenSub.next(stored);
|
||||
this.tokenReady$.next(true);
|
||||
return;
|
||||
}
|
||||
|
||||
// // Optionally: try a silent refresh on startup to restore session using HttpOnly cookie
|
||||
// try {
|
||||
// const res = await firstValueFrom(this.refreshToken());
|
||||
// this.safeSetToken(res.accessToken);
|
||||
// }
|
||||
// catch{
|
||||
// console.warn('Silent token refresh failed on startup');
|
||||
// }
|
||||
// finally{
|
||||
// this.tokenReady$.next(true);
|
||||
// }
|
||||
}
|
||||
|
||||
safeSetToken(token: string) {
|
||||
this.accessToken = token;
|
||||
|
||||
if (isPlatformBrowser(this.platformId)) {
|
||||
localStorage.setItem(this.storageKey, token);
|
||||
}
|
||||
}
|
||||
|
||||
private safeGetToken(): string | null {
|
||||
if (isPlatformBrowser(this.platformId)) {
|
||||
return localStorage.getItem(this.storageKey);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private safeRemoveToken() {
|
||||
this.accessToken = null;
|
||||
|
||||
if (isPlatformBrowser(this.platformId)) {
|
||||
localStorage.removeItem(this.storageKey);
|
||||
}
|
||||
}
|
||||
|
||||
sendOtp(email: string): Observable<unknown> {
|
||||
const formData = new FormData();
|
||||
formData.append('email', email);
|
||||
return this.http.post(this.api('/api/v1/auth/GenerateOtp'), formData);
|
||||
}
|
||||
|
||||
verifyOtp(userId: string, otpCode: string): Observable<any> {
|
||||
verifyOtp(userId: string, otpCode: string): Observable<void> {
|
||||
const body = {
|
||||
UserId: userId,
|
||||
OtpCode: otpCode
|
||||
};
|
||||
return this.http.post(this.api('/api/v1/auth/ValidateOtp'), body);
|
||||
UserId: userId,
|
||||
OtpCode: otpCode
|
||||
};
|
||||
return this.http.post<ValidateOtpResponse>(this.api('/api/v1/auth/ValidateOtp'), body).pipe(map((response: ValidateOtpResponse) => {
|
||||
if (response && response.accessToken) {
|
||||
this.accessToken = response.accessToken;
|
||||
this.safeSetToken(response.accessToken);
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
getApiKey(): string{
|
||||
getAccessToken(): string | null {
|
||||
return this.accessToken ?? this.safeGetToken();
|
||||
}
|
||||
|
||||
logout(): Observable<void> {
|
||||
this.accessToken = null;
|
||||
this.safeRemoveToken();
|
||||
this.router.navigate(['/login']);
|
||||
return of();
|
||||
}
|
||||
|
||||
refreshToken(): Observable<RefreshTokenResponse> {
|
||||
return this.http.post<RefreshTokenResponse>(this.api('/api/v1/auth/RefreshToken'), {});
|
||||
}
|
||||
|
||||
getApiKey(): string {
|
||||
return environment.apiKey;
|
||||
}
|
||||
|
||||
isLoggedIn(): boolean {
|
||||
return this.safeGetToken() != null;
|
||||
}
|
||||
}
|
||||
@@ -1,40 +1,51 @@
|
||||
<div class="otp-container">
|
||||
<h2>🔐 Email Verification</h2>
|
||||
<div class="verify-wrapper">
|
||||
<div class="verify-card">
|
||||
|
||||
<!-- Step 1: Enter Email -->
|
||||
@if (!isOtpSent()) {
|
||||
<form [formGroup]="emailForm" (ngSubmit)="sendOtp()">
|
||||
<h2 class="title">🔐 OTP Verification</h2>
|
||||
|
||||
<!-- Step 1: Enter Email -->
|
||||
@if (!isOtpSent()) {
|
||||
<form [formGroup]="emailForm" (ngSubmit)="sendOtp()" class="form-section">
|
||||
<label>Email Address</label>
|
||||
<input type="email" formControlName="email" placeholder="Enter your email" />
|
||||
<button type="submit" [disabled]="emailForm.invalid">Send OTP</button>
|
||||
</form>
|
||||
}
|
||||
}
|
||||
|
||||
<!-- Step 2: Enter OTP -->
|
||||
@if (isOtpSent() && !isVerified()) {
|
||||
<div>
|
||||
<p>OTP sent to <b>{{ emailForm.value.email }}</b></p>
|
||||
<!-- Step 2: OTP Input -->
|
||||
@if (isOtpSent() && !isVerified()) {
|
||||
<div class="otp-section">
|
||||
<p class="info-text">
|
||||
OTP sent to <b>{{ emailForm.value.email }}</b>
|
||||
</p>
|
||||
|
||||
<form [formGroup]="otpForm" (ngSubmit)="verifyOtp()">
|
||||
<label>Enter 6-digit OTP</label>
|
||||
<input type="text" maxlength="6" formControlName="otp" placeholder="123456" />
|
||||
<button type="submit" [disabled]="otpForm.invalid">Verify OTP</button>
|
||||
<button type="submit" [disabled]="otpForm.invalid">
|
||||
Verify OTP
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<button (click)="resendOtp()" [disabled]="countdown() > 0">
|
||||
Resend OTP @if (countdown() > 0) {
|
||||
<span>({{ countdown() }}s)</span>
|
||||
<button class="resend-btn" (click)="resendOtp()" [disabled]="countdown() > 0">
|
||||
Resend OTP
|
||||
@if (countdown() > 0) {
|
||||
<span>({{ countdown() }}s)</span>
|
||||
}
|
||||
</button>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
<!-- Step 3: Success -->
|
||||
@if (isVerified()) {
|
||||
<!-- Step 3: Success -->
|
||||
<!-- @if (isVerified()) {
|
||||
<div>
|
||||
<p class="success">✅ Your email has been verified successfully!</p>
|
||||
<p class="success-msg">{{ message() }}✅</p>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
<p class="message">{{ message() }}</p>
|
||||
@if (isError()) {
|
||||
<p class="error-message">{{ message() }}</p>
|
||||
} -->
|
||||
|
||||
</div>
|
||||
</div>
|
||||
@@ -1,47 +1,139 @@
|
||||
.otp-container {
|
||||
max-width: 400px;
|
||||
margin: 60px auto;
|
||||
padding: 30px;
|
||||
background: #fff;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
/* Background wrapper */
|
||||
.verify-wrapper {
|
||||
width: 100%;
|
||||
height: 100vh;
|
||||
background: #0e0e0e;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
/* Card */
|
||||
.verify-card {
|
||||
width: 420px;
|
||||
|
||||
/* NEW: More contrast from background */
|
||||
background: var(--bg-gradient-onyx);
|
||||
|
||||
border-radius: 18px;
|
||||
padding: 35px 40px;
|
||||
|
||||
/* NEW: Clean gold glow */
|
||||
box-shadow:
|
||||
0 0 12px rgba(227, 179, 65, 0.25),
|
||||
0 10px 35px rgba(0, 0, 0, 0.65);
|
||||
|
||||
/* NEW: Gold border highlight */
|
||||
border: 1px solid rgba(227, 179, 65, 0.15);
|
||||
|
||||
/* Slight glass effect */
|
||||
backdrop-filter: blur(6px);
|
||||
}
|
||||
|
||||
|
||||
/* Title */
|
||||
.title {
|
||||
text-align: center;
|
||||
color: #fff;
|
||||
font-size: 24px;
|
||||
font-weight: 600;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
/* Labels */
|
||||
label {
|
||||
color: #d6d6d6;
|
||||
font-size: 14px;
|
||||
margin-bottom: 6px;
|
||||
display: block;
|
||||
}
|
||||
|
||||
/* Inputs */
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 14px;
|
||||
background: #111;
|
||||
border: 1px solid #333;
|
||||
color: #fff;
|
||||
border-radius: 10px;
|
||||
font-size: 15px;
|
||||
margin-bottom: 18px;
|
||||
outline: none;
|
||||
transition: 0.25s;
|
||||
|
||||
&:focus {
|
||||
border-color: #e3b341;
|
||||
box-shadow: 0 0 5px rgba(227, 179, 65, 0.45);
|
||||
}
|
||||
|
||||
&::placeholder {
|
||||
color: #777;
|
||||
}
|
||||
}
|
||||
|
||||
/* Primary buttons */
|
||||
button {
|
||||
width: 100%;
|
||||
padding: 14px;
|
||||
background: #e3b341;
|
||||
border: none;
|
||||
color: #111;
|
||||
font-size: 16px;
|
||||
border-radius: 10px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: 0.25s;
|
||||
text-align: center;
|
||||
|
||||
h2 {
|
||||
margin-bottom: 20px;
|
||||
&:disabled {
|
||||
background: #555;
|
||||
color: #aaa;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
|
||||
input {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
margin: 10px 0;
|
||||
font-size: 16px;
|
||||
&:not(:disabled):hover {
|
||||
background: #f2c85c;
|
||||
}
|
||||
}
|
||||
|
||||
/* Resend OTP button */
|
||||
.resend-btn {
|
||||
margin-top: 12px;
|
||||
background: transparent !important;
|
||||
color: #e3b341 !important;
|
||||
border: 1px solid #e3b341;
|
||||
font-size: 14px;
|
||||
|
||||
&:hover:not(:disabled) {
|
||||
background: rgba(227, 179, 65, 0.1);
|
||||
}
|
||||
|
||||
button {
|
||||
width: 100%;
|
||||
margin-top: 10px;
|
||||
padding: 10px;
|
||||
background-color: #007bff;
|
||||
border: none;
|
||||
color: white;
|
||||
font-size: 16px;
|
||||
border-radius: 8px;
|
||||
cursor: pointer;
|
||||
|
||||
&:disabled {
|
||||
background: #aaa;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
&:disabled {
|
||||
border-color: #555;
|
||||
color: #777;
|
||||
}
|
||||
}
|
||||
|
||||
.success {
|
||||
color: green;
|
||||
font-weight: 600;
|
||||
}
|
||||
/* Success message */
|
||||
.success-msg {
|
||||
margin-top: 10px;
|
||||
font-size: 16px;
|
||||
color: #78ff8c;
|
||||
text-align: center;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.message {
|
||||
margin-top: 15px;
|
||||
color: #555;
|
||||
}
|
||||
/* Error / general message */
|
||||
.error-message {
|
||||
margin-top: 12px;
|
||||
color: #ff6b6b;
|
||||
text-align: center;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.info-text {
|
||||
color: #ccc;
|
||||
font-size: 14px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
@@ -1,23 +1,29 @@
|
||||
import { Component, signal } from '@angular/core';
|
||||
import { Component, inject, OnInit, signal } from '@angular/core';
|
||||
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
|
||||
import { AuthService } from '../auth.service';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
|
||||
@Component({
|
||||
selector: 'app-otp',
|
||||
standalone: false,
|
||||
templateUrl: './otp.component.html',
|
||||
styleUrls: ['./otp.component.scss']
|
||||
})
|
||||
export class OtpComponent {
|
||||
export class OtpComponent implements OnInit {
|
||||
emailForm: FormGroup;
|
||||
otpForm: FormGroup;
|
||||
isOtpSent = signal(false);
|
||||
isVerified = signal(false);
|
||||
isError = signal(false);
|
||||
message = signal('');
|
||||
countdown = signal(0);
|
||||
timer: any;
|
||||
timer: NodeJS.Timeout | undefined;
|
||||
returnUrl = '/';
|
||||
fb: FormBuilder = inject(FormBuilder);
|
||||
authService: AuthService = inject(AuthService);
|
||||
router: Router = inject(Router);
|
||||
route: ActivatedRoute = inject(ActivatedRoute);
|
||||
|
||||
constructor(private fb: FormBuilder, private authService: AuthService) {
|
||||
constructor() {
|
||||
this.emailForm = this.fb.group({
|
||||
email: ['', [Validators.required, Validators.email]],
|
||||
});
|
||||
@@ -25,6 +31,15 @@ export class OtpComponent {
|
||||
this.otpForm = this.fb.group({
|
||||
otp: ['', [Validators.required, Validators.pattern(/^[0-9]{6}$/)]],
|
||||
});
|
||||
|
||||
if(this.authService.isLoggedIn()){
|
||||
this.router.navigateByUrl('/');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
ngOnInit() {
|
||||
this.returnUrl = this.route.snapshot.queryParamMap.get('returnUrl') || '/';
|
||||
}
|
||||
|
||||
sendOtp() {
|
||||
@@ -58,15 +73,16 @@ export class OtpComponent {
|
||||
const { otp: otpCode } = this.otpForm.value;
|
||||
|
||||
this.authService.verifyOtp(userId, otpCode).subscribe({
|
||||
next: (res) => {
|
||||
next: () => {
|
||||
this.isVerified.set(true);
|
||||
this.message.set(res.message || 'OTP verified successfully ✅');
|
||||
this.router.navigateByUrl(this.returnUrl); // Navigate to dashboard or desired route after successful verification
|
||||
},
|
||||
error: (err) => {
|
||||
this.isError.set(true);
|
||||
if (err.status === 401 && err.error?.message) {
|
||||
this.message.set(err.error.message); // "OTP Expired" or "Invalid OTP"
|
||||
console.log(err.error.message); // "OTP Expired" or "Invalid OTP"
|
||||
} else {
|
||||
this.message.set('Something went wrong. Please try again.');
|
||||
console.log('Something went wrong. Please try again.');
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user