237 lines
6.2 KiB
TypeScript
237 lines
6.2 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';
|
|
|
|
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) {
|
|
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.userProfileSubject.next(null);
|
|
return;
|
|
}
|
|
|
|
this.clearTokenStorage(localStorage);
|
|
this.clearTokenStorage(sessionStorage);
|
|
this.userProfileSubject.next(null);
|
|
}
|
|
|
|
setToken(token: string, rememberMe = false) {
|
|
if (typeof window === 'undefined') return;
|
|
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;
|
|
}
|
|
}
|