Files
library/apps/frontend/src/app/services/auth.service.ts
T
hikari 34c7ca8ba2 feat: implement user profiles and settings
Add comprehensive user profile system allowing users to showcase
their activity and customize their profiles.

Database Changes:
- Added profile fields to User model: slug, displayName, bio, profilePublic
- Added index on slug field for efficient lookups

API Changes:
- Added GET /users/me endpoint to fetch current user
- Added PUT /users/me endpoint to update user settings
- Added GET /users/profile/:identifier endpoint for public profiles
- Updated UserService with profile methods and statistics
- Modified AuthService to include profile fields in user responses

Frontend Changes:
- Created ProfileComponent to display user profiles with stats
- Created SettingsComponent for profile customization
- Added profile and settings routes
- Updated header dropdown menu with profile links
- Enhanced UserService with profile methods
- Added updateUser method to AuthService

Features:
- Custom profile slugs for clean URLs
- Display names separate from usernames
- User bios (up to 500 characters)
- Public/private profile toggle
- Activity statistics (suggestions, likes, comments, acceptance rate)
- Badge display (Staff, Mod, VIP, Discord Member)
- Beautiful witch-themed styling

Closes #45
2026-02-19 17:27:35 -08:00

133 lines
3.5 KiB
TypeScript

/**
* @copyright 2026 NHCarrigan
* @license Naomi's Public License
* @author Naomi Carrigan
*/
import { Injectable, signal, inject } from '@angular/core';
import { Router } from '@angular/router';
import { Observable, tap, catchError, switchMap, throwError, of } from 'rxjs';
import { ApiService } from './api.service';
import { AuthResponse, User } from '@library/shared-types';
import { environment } from '../../environments/environment';
import { HttpClient } from '@angular/common/http';
@Injectable({
providedIn: 'root'
})
export class AuthService {
private api = inject(ApiService);
private router = inject(Router);
private http = inject(HttpClient);
private currentUser = signal<User | null>(null);
public readonly user = this.currentUser.asReadonly();
private refreshing = false;
private refreshInterval?: ReturnType<typeof setInterval>;
login(): void {
// Redirect to API login endpoint
window.location.href = `${environment.apiUrl}/auth/login`;
}
getCurrentUser(): Observable<AuthResponse> {
return this.api.get<AuthResponse>('/auth/me').pipe(
tap(response => {
this.currentUser.set(response.user);
this.startRefreshTimer();
}),
catchError(error => {
if (error.status === 401) {
return this.refreshToken().pipe(
switchMap(() => this.api.get<AuthResponse>('/auth/me')),
tap(response => {
this.currentUser.set(response.user);
this.startRefreshTimer();
}),
catchError(() => {
this.currentUser.set(null);
this.stopRefreshTimer();
return throwError(() => error);
})
);
}
return throwError(() => error);
})
);
}
refreshToken(): Observable<AuthResponse> {
if (this.refreshing) {
return of({ user: this.currentUser()!, accessToken: '' });
}
this.refreshing = true;
return this.http.post<AuthResponse>(
`${environment.apiUrl}/auth/refresh`,
{},
{ withCredentials: true }
).pipe(
tap(response => {
this.currentUser.set(response.user);
this.refreshing = false;
this.startRefreshTimer();
}),
catchError(error => {
this.refreshing = false;
this.currentUser.set(null);
this.stopRefreshTimer();
return throwError(() => error);
})
);
}
private startRefreshTimer(): void {
this.stopRefreshTimer();
// Refresh token every 13 minutes (before 15-minute expiry)
const refreshIntervalMs = 13 * 60 * 1000;
this.refreshInterval = setInterval(() => {
this.refreshToken().subscribe({
error: (err) => {
console.error('Background token refresh failed:', err);
this.stopRefreshTimer();
}
});
}, refreshIntervalMs);
}
private stopRefreshTimer(): void {
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
this.refreshInterval = undefined;
}
}
logout(): Observable<{ message: string }> {
return this.api.post<{ message: string }>('/auth/logout', {}).pipe(
tap(() => {
this.currentUser.set(null);
this.api.clearCsrfToken();
this.stopRefreshTimer();
this.router.navigate(['/']);
})
);
}
clearUser(): void {
this.currentUser.set(null);
this.stopRefreshTimer();
}
isAuthenticated(): boolean {
return this.user() !== null;
}
updateUser(user: User): void {
this.currentUser.set(user);
}
isAdmin(): boolean {
return this.user()?.isAdmin === true;
}
}