line-gestao-frontend/src/app/services/auth.service.ts

244 lines
6.4 KiB
TypeScript

import { Injectable } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { environment } from '../../environments/environment';
import { BehaviorSubject } from 'rxjs';
import { tap } from 'rxjs/operators';
import { MveAuditService } from './mve-audit.service';
export interface RegisterPayload {
name: string;
email: string;
phone: string;
password: string;
confirmPassword: string;
}
export interface LoginPayload {
email: string;
password: string;
}
export interface LoginOptions {
rememberMe?: boolean;
}
export interface LoginResponse {
token?: string;
accessToken?: string;
}
export interface AuthUserProfile {
id: string;
nome: string;
email: string;
tenantId: string;
roles: string[];
}
@Injectable({ providedIn: 'root' })
export class AuthService {
private baseUrl = `${environment.apiUrl}/auth`;
private userProfileSubject = new BehaviorSubject<AuthUserProfile | null>(null);
readonly userProfile$ = this.userProfileSubject.asObservable();
private readonly tokenStorageKey = 'token';
private readonly tokenExpiresAtKey = 'tokenExpiresAt';
private readonly rememberMeHours = 6;
constructor(
private http: HttpClient,
private readonly mveAuditService: MveAuditService
) {
this.syncUserProfileFromToken();
}
register(payload: RegisterPayload) {
return this.http.post<{ token: string }>(`${this.baseUrl}/register`, payload)
.pipe(tap(r => this.setToken(r.token)));
}
login(payload: LoginPayload, options?: LoginOptions) {
return this.http.post<LoginResponse>(`${this.baseUrl}/login`, payload)
.pipe(
tap((r) => {
const token = this.resolveLoginToken(r);
if (!token) return;
this.setToken(token, options?.rememberMe ?? false);
})
);
}
logout() {
if (typeof window === 'undefined') {
this.mveAuditService.clearCache();
this.userProfileSubject.next(null);
return;
}
this.mveAuditService.clearCache();
this.clearTokenStorage(localStorage);
this.clearTokenStorage(sessionStorage);
this.userProfileSubject.next(null);
}
setToken(token: string, rememberMe = false) {
if (typeof window === 'undefined') return;
this.mveAuditService.clearCache();
this.clearTokenStorage(localStorage);
this.clearTokenStorage(sessionStorage);
if (rememberMe) {
const expiresAt = Date.now() + this.rememberMeHours * 60 * 60 * 1000;
localStorage.setItem(this.tokenStorageKey, token);
localStorage.setItem(this.tokenExpiresAtKey, String(expiresAt));
} else {
sessionStorage.setItem(this.tokenStorageKey, token);
}
this.syncUserProfileFromToken();
}
get token(): string | null {
if (typeof window === 'undefined') return null;
this.cleanupExpiredRememberSession();
const sessionToken = sessionStorage.getItem(this.tokenStorageKey);
if (sessionToken) return sessionToken;
return localStorage.getItem(this.tokenStorageKey);
}
isLoggedIn(): boolean {
return !!this.token;
}
get currentUserProfile(): AuthUserProfile | null {
return this.userProfileSubject.value;
}
syncUserProfileFromToken() {
this.userProfileSubject.next(this.buildProfileFromToken());
}
updateUserProfile(profile: Pick<AuthUserProfile, 'nome' | 'email'>) {
const current = this.userProfileSubject.value;
if (!current) return;
this.userProfileSubject.next({
...current,
nome: profile.nome.trim(),
email: profile.email.trim().toLowerCase(),
});
}
getTokenPayload(): Record<string, any> | null {
const token = this.token;
if (!token) return null;
try {
const payload = token.split('.')[1];
const base64 = payload.replace(/-/g, '+').replace(/_/g, '/');
const json = decodeURIComponent(
atob(base64)
.split('')
.map(c => '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2))
.join('')
);
return JSON.parse(json);
} catch {
return null;
}
}
getRoles(): string[] {
const payload = this.getTokenPayload();
if (!payload) return [];
return this.extractRoles(payload);
}
private extractRoles(payload: Record<string, any>): string[] {
const possibleKeys = [
'role',
'roles',
'http://schemas.microsoft.com/ws/2008/06/identity/claims/role',
];
let roles: string[] = [];
for (const key of possibleKeys) {
const value = payload[key];
if (!value) continue;
if (Array.isArray(value)) roles = roles.concat(value);
else roles.push(String(value));
}
return roles.map(r => r.toLowerCase());
}
private buildProfileFromToken(): AuthUserProfile | null {
const payload = this.getTokenPayload();
if (!payload) return null;
const id = String(
payload['sub'] ??
payload['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier'] ??
''
).trim();
const nome = String(payload['name'] ?? '').trim();
const email = String(
payload['email'] ??
payload['http://schemas.xmlsoap.org/ws/2005/05/identity/claims/emailaddress'] ??
''
).trim().toLowerCase();
const tenantId = String(
payload['tenantId'] ??
payload['tenant'] ??
payload['TenantId'] ??
''
).trim();
if (!id || !tenantId) return null;
return {
id,
nome,
email,
tenantId,
roles: this.extractRoles(payload),
};
}
hasRole(role: string): boolean {
const target = (role || '').toLowerCase();
if (!target) return false;
return this.getRoles().includes(target);
}
private cleanupExpiredRememberSession() {
const token = localStorage.getItem(this.tokenStorageKey);
if (!token) return;
const expiresAtRaw = localStorage.getItem(this.tokenExpiresAtKey);
if (!expiresAtRaw) {
this.clearTokenStorage(localStorage);
return;
}
const expiresAt = Number(expiresAtRaw);
if (!Number.isFinite(expiresAt)) {
this.clearTokenStorage(localStorage);
return;
}
if (Date.now() > expiresAt) {
this.clearTokenStorage(localStorage);
}
}
private clearTokenStorage(storage: Storage) {
storage.removeItem(this.tokenStorageKey);
storage.removeItem(this.tokenExpiresAtKey);
}
private resolveLoginToken(response: LoginResponse | null | undefined): string | null {
const raw = response?.token ?? response?.accessToken ?? null;
const token = (raw ?? '').toString().trim();
return token || null;
}
}