WIP
This commit is contained in:
@@ -0,0 +1,400 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||
import { BehaviorSubject, Observable, of } from 'rxjs';
|
||||
import { map, catchError } from 'rxjs/operators';
|
||||
import { ApiConfigService } from '../../core/services/api-config.service';
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
username: string;
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
createdAt: string;
|
||||
branchIds: string[];
|
||||
token: string;
|
||||
mfaVerified?: boolean;
|
||||
}
|
||||
|
||||
export interface LoginCredentials {
|
||||
email: string;
|
||||
password: string;
|
||||
mfaCode?: string;
|
||||
}
|
||||
|
||||
export interface LoginResponse {
|
||||
isAuthenticated: boolean;
|
||||
requiresMfa: boolean;
|
||||
token?: string;
|
||||
expires?: string;
|
||||
user?: UserDto;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface TokenCreateResponse {
|
||||
token?: string;
|
||||
mfaToken?: string;
|
||||
access?: AccessDto[];
|
||||
isAuthenticated?: boolean;
|
||||
isAuthorized?: boolean;
|
||||
isChangePassword?: boolean;
|
||||
message?: string;
|
||||
username?: string;
|
||||
email?: string;
|
||||
concurrentTabs?: number;
|
||||
mfaType?: number;
|
||||
mfaHint?: string;
|
||||
Token?: string;
|
||||
MfaToken?: string;
|
||||
Access?: AccessDto[];
|
||||
IsAuthenticated?: boolean;
|
||||
IsAuthorized?: boolean;
|
||||
IsChangePassword?: boolean;
|
||||
Message?: string;
|
||||
Username?: string;
|
||||
Email?: string;
|
||||
ConcurrentTabs?: number;
|
||||
MfaType?: number;
|
||||
MfaHint?: string;
|
||||
}
|
||||
|
||||
export interface AccessDto {
|
||||
branchName?: string;
|
||||
redxBranch?: string;
|
||||
BranchName?: string;
|
||||
RedxBranch?: string;
|
||||
}
|
||||
|
||||
export interface UserDto {
|
||||
id: string;
|
||||
username: string;
|
||||
email: string;
|
||||
firstName: string;
|
||||
lastName: string;
|
||||
createdAt: string;
|
||||
branchIds: string[];
|
||||
}
|
||||
|
||||
export enum LoginResultType {
|
||||
Success = 'Success',
|
||||
MfaRequired = 'MfaRequired',
|
||||
InvalidCredentials = 'InvalidCredentials',
|
||||
Error = 'Error'
|
||||
}
|
||||
|
||||
export interface LoginResult {
|
||||
result: LoginResultType;
|
||||
responseData?: User;
|
||||
message?: string;
|
||||
}
|
||||
|
||||
export interface TokenVerificationResult {
|
||||
isValid: boolean;
|
||||
user?: User;
|
||||
message?: string;
|
||||
expiresAt?: Date;
|
||||
requiresMfa?: boolean;
|
||||
}
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
export class AuthService {
|
||||
private currentUserSubject = new BehaviorSubject<User | null>(null);
|
||||
public currentUser$ = this.currentUserSubject.asObservable();
|
||||
private redirectUrl: string = '/dashboard';
|
||||
constructor(
|
||||
private http: HttpClient,
|
||||
private apiConfig: ApiConfigService
|
||||
) { }
|
||||
|
||||
login(credentials: LoginCredentials): Observable<LoginResult> {
|
||||
return this.authenticateUser(credentials);
|
||||
}
|
||||
|
||||
private authenticateUser(credentials: LoginCredentials): Observable<LoginResult> {
|
||||
const loginUrl = `${this.apiConfig.tokenUrl}/Create`;
|
||||
return this.http.get<TokenCreateResponse>(loginUrl, {
|
||||
headers: this.buildTokenCreateHeaders(credentials)
|
||||
}).pipe(
|
||||
map((response: TokenCreateResponse) => this.mapTokenCreateResponse(response, credentials)),
|
||||
catchError((error) => {
|
||||
console.error('Login error:', error);
|
||||
return of({
|
||||
result: LoginResultType.Error,
|
||||
message: error.error?.message || 'An error occurred during login'
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
private buildTokenCreateHeaders(credentials: LoginCredentials): HttpHeaders {
|
||||
let headers = new HttpHeaders({
|
||||
Authorization: `Basic ${btoa(`${credentials.email}:${credentials.password}`)}`
|
||||
});
|
||||
|
||||
if (credentials.mfaCode) {
|
||||
headers = headers.set('mfaCode', credentials.mfaCode);
|
||||
}
|
||||
|
||||
return headers;
|
||||
}
|
||||
|
||||
private mapTokenCreateResponse(response: TokenCreateResponse, credentials: LoginCredentials): LoginResult {
|
||||
const token = response.token || response.Token || '';
|
||||
const message = response.message || response.Message || '';
|
||||
const isAuthenticated = response.isAuthenticated ?? response.IsAuthenticated ?? false;
|
||||
const isAuthorized = response.isAuthorized ?? response.IsAuthorized ?? false;
|
||||
|
||||
if (isAuthenticated && isAuthorized && token) {
|
||||
return {
|
||||
result: LoginResultType.Success,
|
||||
responseData: this.mapUserFromTokenCreateResponse(response, credentials, token)
|
||||
};
|
||||
}
|
||||
|
||||
if (isAuthenticated && !token && this.isMfaRequired(message)) {
|
||||
return {
|
||||
result: LoginResultType.MfaRequired,
|
||||
responseData: this.mapUserFromTokenCreateResponse(
|
||||
response,
|
||||
credentials,
|
||||
response.mfaToken || response.MfaToken || ''
|
||||
),
|
||||
message
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
result: isAuthenticated ? LoginResultType.Error : LoginResultType.InvalidCredentials,
|
||||
message: message || 'Invalid email or password'
|
||||
};
|
||||
}
|
||||
|
||||
private mapUserFromTokenCreateResponse(
|
||||
response: TokenCreateResponse,
|
||||
credentials: LoginCredentials,
|
||||
token: string
|
||||
): User {
|
||||
const username = response.username || response.Username || credentials.email;
|
||||
const email = response.email || response.Email || credentials.email;
|
||||
const access = response.access || response.Access || [];
|
||||
|
||||
return {
|
||||
id: username || email,
|
||||
username,
|
||||
email,
|
||||
firstName: '',
|
||||
lastName: '',
|
||||
createdAt: new Date().toISOString(),
|
||||
branchIds: access
|
||||
.map(item => item.redxBranch || item.RedxBranch || item.branchName || item.BranchName || '')
|
||||
.filter(branchId => !!branchId),
|
||||
token,
|
||||
mfaVerified: !!credentials.mfaCode
|
||||
};
|
||||
}
|
||||
|
||||
private isMfaRequired(message: string): boolean {
|
||||
return message.toLowerCase().includes('mfa verification required');
|
||||
}
|
||||
|
||||
logout(): void {
|
||||
this.currentUserSubject.next(null);
|
||||
this.redirectUrl = '/dashboard';
|
||||
// Clear any stored tokens, etc.
|
||||
localStorage.removeItem('currentUser');
|
||||
}
|
||||
|
||||
getCurrentUser(): User | null {
|
||||
return this.currentUserSubject.value;
|
||||
}
|
||||
|
||||
getToken(): string | null {
|
||||
const user = this.currentUserSubject.value;
|
||||
return user?.token || null;
|
||||
}
|
||||
|
||||
isAuthenticated(): boolean {
|
||||
return this.currentUserSubject.value !== null;
|
||||
}
|
||||
|
||||
setCurrentUser(user: User): void {
|
||||
this.currentUserSubject.next(user);
|
||||
// Store user in localStorage for persistence
|
||||
localStorage.setItem('currentUser', JSON.stringify(user));
|
||||
}
|
||||
|
||||
setRedirectUrl(url: string): void {
|
||||
this.redirectUrl = url;
|
||||
}
|
||||
|
||||
getRedirectUrl(): string {
|
||||
return this.redirectUrl;
|
||||
}
|
||||
|
||||
// Initialize user from localStorage on app start
|
||||
initializeAuth(): void {
|
||||
const storedUser = localStorage.getItem('currentUser');
|
||||
if (storedUser) {
|
||||
try {
|
||||
const user = JSON.parse(storedUser);
|
||||
this.currentUserSubject.next(user);
|
||||
} catch (error) {
|
||||
console.error('Error parsing stored user:', error);
|
||||
localStorage.removeItem('currentUser');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify JWT token from email link (local verification)
|
||||
* @param token The JWT token to verify
|
||||
* @returns Observable with verification result
|
||||
*/
|
||||
verifySecretLinkToken(token: string): Observable<TokenVerificationResult> {
|
||||
try {
|
||||
// Parse JWT token locally
|
||||
const tokenData = this.parseJwtToken(token);
|
||||
|
||||
if (!tokenData) {
|
||||
return of({
|
||||
isValid: false,
|
||||
message: 'Invalid token format'
|
||||
});
|
||||
}
|
||||
|
||||
// Check if token is expired
|
||||
if (this.isTokenExpired(token)) {
|
||||
return of({
|
||||
isValid: false,
|
||||
message: 'This link has expired. Please request a new one.'
|
||||
});
|
||||
}
|
||||
console.log('tokenData', tokenData);
|
||||
|
||||
// Extract user data from token
|
||||
const user: User = {
|
||||
id: tokenData.userId || tokenData.sub || tokenData.id,
|
||||
username: tokenData.username || tokenData.preferred_username || '',
|
||||
email: tokenData.email || tokenData.email_address || '',
|
||||
firstName: tokenData.firstName || tokenData.given_name || tokenData.first_name || '',
|
||||
lastName: tokenData.lastName || tokenData.family_name || tokenData.last_name || '',
|
||||
createdAt: tokenData.createdAt || tokenData.created_at || new Date().toISOString(),
|
||||
branchIds: tokenData.branchIds || tokenData.branch_ids || [],
|
||||
token: token,
|
||||
mfaVerified: false // Token users still need MFA verification
|
||||
};
|
||||
|
||||
return of({
|
||||
isValid: true,
|
||||
user: user,
|
||||
message: 'Token verified successfully. MFA required.',
|
||||
expiresAt: tokenData.exp ? new Date(tokenData.exp * 1000) : undefined,
|
||||
requiresMfa: true
|
||||
});
|
||||
|
||||
} catch (error) {
|
||||
console.error('Token verification error:', error);
|
||||
return of({
|
||||
isValid: false,
|
||||
message: 'Invalid or corrupted token'
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a token is expired locally (basic check)
|
||||
* @param token JWT token to check
|
||||
* @returns boolean indicating if token is expired
|
||||
*/
|
||||
isTokenExpired(token: string): boolean {
|
||||
try {
|
||||
const payload = JSON.parse(atob(token.split('.')[1]));
|
||||
const currentTime = Math.floor(Date.now() / 1000);
|
||||
return payload.exp < currentTime;
|
||||
} catch (error) {
|
||||
console.error('Error parsing token:', error);
|
||||
return true; // Consider invalid tokens as expired
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse JWT token and extract payload
|
||||
* @param token JWT token to parse
|
||||
* @returns parsed token data or null if invalid
|
||||
*/
|
||||
private parseJwtToken(token: string): any | null {
|
||||
try {
|
||||
// Split token into parts
|
||||
const parts = token.split('.');
|
||||
if (parts.length !== 3) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Decode the payload (middle part)
|
||||
const payload = parts[1];
|
||||
const decodedPayload = atob(payload.replace(/-/g, '+').replace(/_/g, '/'));
|
||||
return JSON.parse(decodedPayload);
|
||||
} catch (error) {
|
||||
console.error('Error parsing JWT token:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Verify MFA code for token-based authentication
|
||||
* @param mfaCode MFA code entered by user
|
||||
* @param user User data from token verification
|
||||
* @returns Observable with MFA verification result
|
||||
*/
|
||||
verifyMfaForToken(mfaCode: string, user: User): Observable<LoginResult> {
|
||||
// For token-based users, we can either:
|
||||
// 1. Verify MFA locally (if MFA code is embedded in token)
|
||||
// 2. Make a server call to verify MFA
|
||||
|
||||
// For now, we'll simulate MFA verification
|
||||
// In a real implementation, you might want to verify against a server
|
||||
//TODO: Implement MFA verification
|
||||
|
||||
const loginUrl = `${this.apiConfig.authUrl}/mfa/token-login`;
|
||||
return this.http.post<LoginResponse>(loginUrl, {
|
||||
mfaToken: user.token,
|
||||
mfaCode: mfaCode
|
||||
}).pipe(
|
||||
map((response: LoginResponse) => {
|
||||
|
||||
return {
|
||||
result: LoginResultType.Success,
|
||||
responseData: {
|
||||
...user,
|
||||
mfaVerified: true
|
||||
},
|
||||
message: 'MFA verification successful'
|
||||
};
|
||||
}),
|
||||
catchError((error) => {
|
||||
console.error('MFA verification error:', error);
|
||||
return of({
|
||||
result: LoginResultType.Error,
|
||||
message: error.error?.message || 'An error occurred during MFA verification'
|
||||
});
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract token from URL parameters
|
||||
* @param url URL containing token parameter
|
||||
* @returns token string or null if not found
|
||||
*/
|
||||
extractTokenFromUrl(url: string): string | null {
|
||||
try {
|
||||
const urlObj = new URL(url);
|
||||
return urlObj.searchParams.get('token');
|
||||
} catch (error) {
|
||||
console.error('Error parsing URL:', error);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user