This commit is contained in:
Chris Chen
2026-05-25 17:32:18 -07:00
parent 9b28fbcfb6
commit d5648315a0
262 changed files with 32074 additions and 0 deletions
+400
View File
@@ -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;
}
}
}
@@ -0,0 +1,182 @@
import { Injectable } from '@angular/core';
import { SVGIcon } from '@progress/kendo-angular-icons';
import { EscrowStatus, CbAssigneeRole } from '../models/enums.model';
import { Subject } from 'rxjs';
@Injectable({
providedIn: 'root'
})
export class UiUtilsService {
public setPageTitleSubject = new Subject<string>();
public setPageTitle$ = this.setPageTitleSubject.asObservable();
// Icon properties - these should be injected or provided by a parent component
// For now, we'll make them optional and let the calling component provide them
checkCircleIcon?: SVGIcon;
clockIcon?: SVGIcon;
alertCircleIcon?: SVGIcon;
userIcon?: SVGIcon;
constructor() { }
/**
* Get CSS class for escrow status
*/
getStatusClass(status: EscrowStatus): string {
switch (status) {
case EscrowStatus.Open:
return 'status-active';
case EscrowStatus.Closed:
return 'status-completed';
case EscrowStatus.Cancelled:
return 'status-cancelled';
default:
return '';
}
}
/**
* Get display label for escrow status
*/
getEscrowStatusLabel(status: EscrowStatus): string {
switch (status) {
case EscrowStatus.Open:
return 'Open';
case EscrowStatus.Closed:
return 'Closed';
case EscrowStatus.Cancelled:
return 'Cancelled';
default:
return '';
}
}
/**
* Get CSS class for priority level
*/
getPriorityClass(priority: string): string {
switch (priority) {
case 'high':
return 'priority-high';
case 'medium':
return 'priority-medium';
case 'low':
return 'priority-low';
default:
return '';
}
}
/**
* Get icon for escrow status
*/
getStatusIcon(status: EscrowStatus): SVGIcon | undefined {
switch (status) {
case EscrowStatus.Open:
return this.checkCircleIcon;
case EscrowStatus.Closed:
return this.checkCircleIcon;
case EscrowStatus.Cancelled:
return this.alertCircleIcon;
default:
return this.clockIcon;
}
}
/**
* Get CSS class for assignee role
*/
getRoleClass(role: CbAssigneeRole): string {
switch (role) {
case CbAssigneeRole.Buyer:
return 'role-buyer';
case CbAssigneeRole.Seller:
return 'role-seller';
case CbAssigneeRole.BuyerRealEstateAgent:
case CbAssigneeRole.SellerRealEstateAgent:
return 'role-agent';
case CbAssigneeRole.EscrowOfficer:
case CbAssigneeRole.EscrowAssignee:
return 'role-escrow';
case CbAssigneeRole.LoanBroker:
case CbAssigneeRole.Lender:
return 'role-lender';
case CbAssigneeRole.SellerTransactionCoordinator:
case CbAssigneeRole.BuyerTransactionCoordinator:
return 'role-coordinator';
case CbAssigneeRole.None:
return 'role-default';
default:
return 'role-default';
}
}
/**
* Get icon for assignee role
*/
getRoleIcon(role: CbAssigneeRole): SVGIcon | undefined {
switch (role) {
case CbAssigneeRole.Buyer:
case CbAssigneeRole.Seller:
case CbAssigneeRole.BuyerRealEstateAgent:
case CbAssigneeRole.SellerRealEstateAgent:
case CbAssigneeRole.EscrowOfficer:
case CbAssigneeRole.EscrowAssignee:
case CbAssigneeRole.LoanBroker:
case CbAssigneeRole.Lender:
case CbAssigneeRole.SellerTransactionCoordinator:
case CbAssigneeRole.BuyerTransactionCoordinator:
return this.userIcon;
case CbAssigneeRole.None:
return this.userIcon;
default:
return this.userIcon;
}
}
/**
* Get display label for assignee role
*/
getRoleLabel(role: CbAssigneeRole): string {
switch (role) {
case CbAssigneeRole.Buyer:
return 'Buyer';
case CbAssigneeRole.Seller:
return 'Seller';
case CbAssigneeRole.BuyerRealEstateAgent:
return 'Buyer Agent';
case CbAssigneeRole.SellerRealEstateAgent:
return 'Seller Agent';
case CbAssigneeRole.EscrowOfficer:
return 'Escrow Officer';
case CbAssigneeRole.EscrowAssignee:
return 'Escrow Assignee';
case CbAssigneeRole.LoanBroker:
return 'Loan Broker';
case CbAssigneeRole.Lender:
return 'Lender';
case CbAssigneeRole.SellerTransactionCoordinator:
return 'Seller Coordinator';
case CbAssigneeRole.BuyerTransactionCoordinator:
return 'Buyer Coordinator';
case CbAssigneeRole.None:
return 'None';
default:
return 'Unknown';
}
}
/**
* Set icons for the service (should be called by components that use this service)
*/
setIcons(icons: {
checkCircleIcon?: SVGIcon;
clockIcon?: SVGIcon;
alertCircleIcon?: SVGIcon;
userIcon?: SVGIcon;
}): void {
this.checkCircleIcon = icons.checkCircleIcon;
this.clockIcon = icons.clockIcon;
this.alertCircleIcon = icons.alertCircleIcon;
this.userIcon = icons.userIcon;
}
}