All files / app/services auth.service.ts

37.13% Statements 101/272
100% Branches 4/4
50% Functions 4/8
37.13% Lines 101/272

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 2731x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x           1x 1x 1x 1x 1x 1x 1x 1x                                                                                                               1x 1x 1x 1x 1x 1x 1x     1x 1x 1x 1x 1x 1x 1x 1x 82x 82x 82x 82x 82x 82x 82x 82x 82x 82x 82x 82x                             82x 82x 82x 82x 1x 1x 1x 1x 1x 1x                                                                                                                                                                                           1x 1x 1x 1x 1x 39x 39x 1x 1x 1x 1x 1x     1x  
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { inject, Injectable, Injector } from '@angular/core';
import { UserClass } from '@classes/user.class';
import { environment } from '@environments/environment';
import { BehaviorSubject, Observable, of } from 'rxjs';
import { catchError, map, tap } from 'rxjs/operators';
import { GtmService } from './gtm.service';
import { NetworkService } from './network.service';
 
export interface User {
	id: string;
	email: string;
	'first-name': string;
	// Add other user properties
}
 
export interface LoginResponse {
	data: {
		id: string;
		type: string;
		attributes: User;
	};
	meta?: {
		token: string;
		refresh_token: string;
	};
}
 
@Injectable({
	providedIn: 'root',
})
export class AuthService {
	public currentUserSubject = new BehaviorSubject<any | null>(null);
	public currentUser$ = this.currentUserSubject.asObservable();
 
	// ✅ Normal injections
	private http = inject(HttpClient);
	private gtm = inject(GtmService);
 
	// ✅ Lazy inject to avoid circular dependency with NetworkService
	private injector = inject(Injector);
	private _network?: NetworkService;
	private get network(): NetworkService {
		if (!this._network) {
			this._network = this.injector.get(NetworkService);
		}
		return this._network;
	}
 
	constructor() {}
 
	/**
	 * ✅ Login user with email and password
	 * Backend automatically sets cookies via Origin header.
	 */
	login(email: string, password: string): Observable<any> {
		return this.http
			.post<LoginResponse>(
				`${environment.apiUrl}/session`,
				{
					data: {
						type: 'users',
						attributes: { email, password },
					},
				},
				{
					withCredentials: true, // ✅ Crucial for cookies
					observe: 'response',
					headers: new HttpHeaders({
						Accept: 'application/vnd.api+json',
						'Content-Type': 'application/vnd.api+json',
					}),
				},
			)
			.pipe(
				map((response: any) => {
					console.log('Response headers:', response.headers);
					console.log('Set-Cookie header:', response.headers.get('Set-Cookie'));

					let user: UserClass = new UserClass();
					if (response.headers.has('access-token')) {
						const accessToken = response.headers.get('access-token');
						if (accessToken) {
							localStorage.setItem('token', accessToken);
						}
					}
					const userData = response.body;
					if (userData) {
						user.fromJson(response.body);
					}

					// ✅ Safe lazy access to network service
					this.network.user = user;
					this.network.isAdmin = user.isAdmin();
					this.network.headersWithHeaders = new HttpHeaders({
						Accept: 'application/vnd.api+json',
						'Content-Type': 'application/vnd.api+json',
						Origin: environment.frontendUrl,
						Authorization: `Bearer ${response.headers.get('access-token')}`,
					});

					this.gtm.login(user, this.network.headersWithHeaders);

					user = response.body?.data?.attributes;
					if (user) {
						this.currentUserSubject.next(user);
					}
					return user!;
				}),
			);
	}
 
	/**
	 * ✅ Logout user (clears cookies on backend)
	 */
	logout(): Observable<void> {
		return this.http.delete<void>(`${environment.apiUrl}/session`, { withCredentials: true }).pipe(
			tap(() => {
				this.currentUserSubject.next(null);
				sessionStorage.removeItem('access_token');
			}),
		);
	}
 
	/**
	 * ✅ Verify if user session exists
	 */
	verifySession(): Observable<UserClass | null> {
		return this.http
			.get<LoginResponse>(`${environment.apiUrl}/session/verify`, {
				withCredentials: true, // ✅ Required for cookie-based sessions
				observe: 'response',
				headers: new HttpHeaders({
					Accept: 'application/vnd.api+json',
					'Content-Type': 'application/vnd.api+json',
					Origin: environment.frontendUrl,
				}),
			})
			.pipe(
				map((response: any) => {
					const user = new UserClass();
					if (response.body) {
						user.fromJson(response.body);
					}

					// ✅ Safe lazy access again
					this.network.is_logged_in = true;
					this.network.user = user;
					this.network.isAdmin = user.isAdmin?.() ?? false;

					if (user) {
						this.currentUserSubject.next(user);
					}
					return user || null;
				}),
				catchError(() => of(null)), // graceful fallback
			);
	}
 
	/**
	 * ✅ Register a new user
	 * Matches payload & response from backend
	 */
	register(registerData: any): Promise<UserClass> {
		let countryCode: string | null = null;
		let phoneNumber: string | null = null;

		if (registerData?.phone) {
			countryCode = registerData.phone.countryCode;
			phoneNumber = registerData.phone.internationalNumber;
		}

		const payload = {
			data: {
				type: 'users',
				attributes: {
					email: registerData.email,
					password: registerData.password,
					phone: phoneNumber,
					first_name: registerData.first_name,
					last_name: registerData.last_name,
					country_code: countryCode,
					sign_up_ip: this.network?.ip || '', // fallback if IP tracked from NetworkService
					sms_opt_in: registerData.smsOptIn ?? false,
					email_opt_in: registerData.emailOptIn ?? false,
					wizard_version: '', // optional if not using a wizard version flow
				},
			},
		};

		return new Promise<UserClass>((resolve, reject) => {
			this.http
				.post<LoginResponse>(`${environment.apiUrl}/users`, payload, {
					withCredentials: true,
					observe: 'response',
					headers: new HttpHeaders({
						Accept: 'application/vnd.api+json',
						'Content-Type': 'application/vnd.api+json',
					}),
				})
				.subscribe({
					next: (response: any) => {
						console.log('✅ Registration Response:', response);

						// ✅ Save token if present in headers
						if (response.headers.has('access-token')) {
							const accessToken = response.headers.get('access-token');
							if (accessToken) {
								localStorage.setItem('token', accessToken);
							}
						}

						// ✅ Convert API response to UserClass
						const user = new UserClass();
						if (response.body) {
							user.fromJson(response.body);
						}

						// ✅ Sync with NetworkService
						this.network.user = user;
						this.network.isAdmin = user.isAdmin?.() ?? false;

						// ✅ GTM tracking event
						this.gtm.register(user);

						// ✅ Broadcast logged-in user
						this.currentUserSubject.next(user);

						resolve(user);
					},
					error: (error) => {
						let message = '';

						switch (error.status || 0) {
							case 401:
								message = 'Please check your form data.';
								break;
							case 422:
								if (error.error?.errors?.[0]?.detail === 'taken') {
									reject({
										email: {
											taken: 'This email address is already taken',
										},
									});
									return;
								}
								message = 'You already have a user account. Please sign in instead.';
								break;
							default:
								message = 'Server is not available. Please try again later.';
						}

						reject(message);
					},
				});
		});
	}
 
	/**
	 * ✅ Get current user
	 */
	getCurrentUser(): User | null {
		return this.currentUserSubject.value;
	}
 
	/**
	 * ✅ Check if user is logged in
	 */
	isLoggedIn(): boolean {
		return this.currentUserSubject.value !== null;
	}
}