init
This commit is contained in:
@@ -9,336 +9,301 @@ import { BehaviorSubject, Observable, throwError } from 'rxjs';
|
||||
import { catchError, finalize, tap } from 'rxjs/operators';
|
||||
import { LOCAL_STORAGE_KEYS } from '../../../assets/constants';
|
||||
import {
|
||||
IAuthResponse,
|
||||
ISignupRequestPayload,
|
||||
IUserLoginInfo,
|
||||
LoginCredentials,
|
||||
Maybe,
|
||||
TRoles,
|
||||
User,
|
||||
IAuthAccountResponse,
|
||||
IAuthResponse,
|
||||
ISignupRequestPayload,
|
||||
IUserLoginInfo,
|
||||
LoginCredentials,
|
||||
Maybe,
|
||||
TRoles,
|
||||
} from '../models';
|
||||
import { CaptchaService } from './captcha.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root',
|
||||
providedIn: 'root',
|
||||
})
|
||||
export class AuthService {
|
||||
constructor() {
|
||||
this.initializeAuth();
|
||||
constructor() {
|
||||
this.initializeAuth();
|
||||
}
|
||||
|
||||
readonly modulesLoginRoutes = {
|
||||
// ADMIN: ADMIN_API_ROUTES.login(),
|
||||
// SCHOOL: SCHOOLS_API_ROUTES.login(),
|
||||
// TEACHER: TEACHERS_API_ROUTES.login(),
|
||||
} as Record<TRoles, string>;
|
||||
|
||||
readonly modulesSignupRoutes = {
|
||||
// TEACHER: TEACHERS_API_ROUTES.signup(),
|
||||
} as Record<TRoles, string>;
|
||||
|
||||
readonly modulesGetInfoRoutes = {
|
||||
// SCHOOL: SCHOOLS_API_ROUTES.me(),
|
||||
} as Record<TRoles, string>;
|
||||
|
||||
readonly modulesChangeInfoRoutes = {
|
||||
// SCHOOL: SCHOOLS_API_ROUTES.editLoginInfo(),
|
||||
} as Record<TRoles, string>;
|
||||
|
||||
private readonly http = inject(HttpClient);
|
||||
private readonly router = inject(Router);
|
||||
private captchaService = inject(CaptchaService);
|
||||
|
||||
private readonly currentAccountSubject = new BehaviorSubject<Maybe<IAuthAccountResponse>>(null);
|
||||
private readonly isLoadingSubject = new BehaviorSubject<boolean>(false);
|
||||
|
||||
readonly currentUser$ = this.currentAccountSubject.asObservable();
|
||||
readonly isLoading$ = this.isLoadingSubject.asObservable();
|
||||
|
||||
// Signals for reactive state management
|
||||
// readonly currentUser = signal<Maybe<User>>(null);
|
||||
readonly currentAccount = signal<Maybe<IAuthAccountResponse>>(null);
|
||||
readonly isLoading = signal<boolean>(false);
|
||||
readonly token = signal<Maybe<string>>(null);
|
||||
readonly refreshToken = signal<Maybe<string>>(null);
|
||||
readonly isAuthenticated = computed(() => {
|
||||
return Boolean(this.token());
|
||||
});
|
||||
readonly userRole = computed(() => this.currentAccount()?.type);
|
||||
|
||||
private initializeAuth(): void {
|
||||
const token = localStorage.getItem(LOCAL_STORAGE_KEYS.AUTH_TOKEN);
|
||||
|
||||
const accountData = localStorage.getItem(LOCAL_STORAGE_KEYS.USER_DATA);
|
||||
|
||||
if (token && accountData) {
|
||||
try {
|
||||
this.token.set(token);
|
||||
const account = JSON.parse(accountData) as IAuthAccountResponse;
|
||||
this.setCurrentUser(account);
|
||||
} catch (error) {
|
||||
console.error('Error parsing stored user data:', error);
|
||||
this.logout();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
login(credentials: LoginCredentials): Observable<IAuthResponse> {
|
||||
const { username, password, captcha } = credentials;
|
||||
this.isLoading.set(true);
|
||||
this.isLoadingSubject.next(true);
|
||||
const baseHeaders = this.captchaService.buildCaptchaHeaders({}, captcha);
|
||||
|
||||
return this.http
|
||||
.post<IAuthResponse>(
|
||||
'/api/v1/auth/login',
|
||||
{ username, password },
|
||||
{
|
||||
headers: baseHeaders,
|
||||
},
|
||||
)
|
||||
.pipe(
|
||||
tap((response) => {
|
||||
this.handleAuthSuccess(response);
|
||||
return response;
|
||||
}),
|
||||
catchError((error) => {
|
||||
console.error('Login error:', error);
|
||||
this.isLoadingSubject.next(false);
|
||||
throw error;
|
||||
}),
|
||||
finalize(() => {
|
||||
this.isLoading.set(false);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
signup(credentials: ISignupRequestPayload, role: TRoles): Observable<IAuthResponse> {
|
||||
this.isLoading.set(true);
|
||||
this.isLoadingSubject.next(true);
|
||||
// const baseHeaders = this.captchaService.buildCaptchaHeaders({}, captcha);
|
||||
|
||||
return this.http
|
||||
.post<IAuthResponse>(
|
||||
this.modulesSignupRoutes[role],
|
||||
credentials,
|
||||
// {
|
||||
// headers: baseHeaders,
|
||||
// },
|
||||
)
|
||||
.pipe(
|
||||
tap((response) => {
|
||||
this.handleAuthSuccess(response);
|
||||
return response;
|
||||
}),
|
||||
catchError((error) => {
|
||||
console.error('Signup error:', error);
|
||||
this.isLoadingSubject.next(false);
|
||||
throw error;
|
||||
}),
|
||||
finalize(() => {
|
||||
this.isLoading.set(false);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
logout(): void {
|
||||
localStorage.removeItem(LOCAL_STORAGE_KEYS.AUTH_TOKEN);
|
||||
localStorage.removeItem(LOCAL_STORAGE_KEYS.USER_DATA);
|
||||
localStorage.removeItem(LOCAL_STORAGE_KEYS.REFRESH_TOKEN);
|
||||
|
||||
this.setCurrentUser(null);
|
||||
this.router.navigate(['/auth']);
|
||||
}
|
||||
|
||||
doRefreshToken(): Observable<IAuthResponse> {
|
||||
const refreshToken = this.getRefreshToken();
|
||||
if (!refreshToken) {
|
||||
this.logout();
|
||||
return throwError(() => new Error('No refresh token available'));
|
||||
}
|
||||
|
||||
readonly modulesLoginRoutes = {
|
||||
// ADMIN: ADMIN_API_ROUTES.login(),
|
||||
// SCHOOL: SCHOOLS_API_ROUTES.login(),
|
||||
// TEACHER: TEACHERS_API_ROUTES.login(),
|
||||
} as Record<TRoles, string>;
|
||||
|
||||
readonly modulesSignupRoutes = {
|
||||
// TEACHER: TEACHERS_API_ROUTES.signup(),
|
||||
} as Record<TRoles, string>;
|
||||
|
||||
readonly modulesGetInfoRoutes = {
|
||||
// SCHOOL: SCHOOLS_API_ROUTES.me(),
|
||||
} as Record<TRoles, string>;
|
||||
|
||||
readonly modulesChangeInfoRoutes = {
|
||||
// SCHOOL: SCHOOLS_API_ROUTES.editLoginInfo(),
|
||||
} as Record<TRoles, string>;
|
||||
|
||||
private readonly http = inject(HttpClient);
|
||||
private readonly router = inject(Router);
|
||||
private captchaService = inject(CaptchaService);
|
||||
|
||||
private readonly currentUserSubject = new BehaviorSubject<Maybe<User>>(
|
||||
null,
|
||||
return this.http.post<IAuthResponse>('/api/auth/refresh', { refreshToken }).pipe(
|
||||
tap((response) => {
|
||||
this.handleAuthSuccess(response);
|
||||
}),
|
||||
catchError((error) => {
|
||||
console.error('Token refresh error:', error);
|
||||
this.logout();
|
||||
return throwError(() => error);
|
||||
}),
|
||||
);
|
||||
private readonly isLoadingSubject = new BehaviorSubject<boolean>(false);
|
||||
}
|
||||
|
||||
readonly currentUser$ = this.currentUserSubject.asObservable();
|
||||
readonly isLoading$ = this.isLoadingSubject.asObservable();
|
||||
|
||||
// Signals for reactive state management
|
||||
// readonly currentUser = signal<Maybe<User>>(null);
|
||||
readonly currentUser = signal<Maybe<User>>(null);
|
||||
readonly isLoading = signal<boolean>(false);
|
||||
readonly token = signal<Maybe<string>>(null);
|
||||
readonly isAuthenticated = computed(() => {
|
||||
return Boolean(this.token());
|
||||
});
|
||||
readonly userRole = computed(() => this.currentUser()?.role);
|
||||
|
||||
private initializeAuth(): void {
|
||||
const token = localStorage.getItem(LOCAL_STORAGE_KEYS.AUTH_TOKEN);
|
||||
|
||||
const userData = localStorage.getItem(LOCAL_STORAGE_KEYS.USER_DATA);
|
||||
|
||||
if (token && userData) {
|
||||
try {
|
||||
this.token.set(token);
|
||||
const user = JSON.parse(userData) as User;
|
||||
this.setCurrentUser(user);
|
||||
} catch (error) {
|
||||
console.error('Error parsing stored user data:', error);
|
||||
this.logout();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
login(
|
||||
credentials: LoginCredentials,
|
||||
role: TRoles,
|
||||
): Observable<IAuthResponse> {
|
||||
const { username, password, captcha } = credentials;
|
||||
this.isLoading.set(true);
|
||||
this.isLoadingSubject.next(true);
|
||||
const baseHeaders = this.captchaService.buildCaptchaHeaders(
|
||||
{},
|
||||
captcha,
|
||||
);
|
||||
|
||||
return this.http
|
||||
.post<IAuthResponse>(
|
||||
this.modulesLoginRoutes[role],
|
||||
{ username, password },
|
||||
{
|
||||
headers: baseHeaders,
|
||||
},
|
||||
)
|
||||
.pipe(
|
||||
tap((response) => {
|
||||
this.handleAuthSuccess(response);
|
||||
return response;
|
||||
}),
|
||||
catchError((error) => {
|
||||
console.error('Login error:', error);
|
||||
this.isLoadingSubject.next(false);
|
||||
throw error;
|
||||
}),
|
||||
finalize(() => {
|
||||
this.isLoading.set(false);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
signup(
|
||||
credentials: ISignupRequestPayload,
|
||||
role: TRoles,
|
||||
): Observable<IAuthResponse> {
|
||||
this.isLoading.set(true);
|
||||
this.isLoadingSubject.next(true);
|
||||
// const baseHeaders = this.captchaService.buildCaptchaHeaders({}, captcha);
|
||||
|
||||
return this.http
|
||||
.post<IAuthResponse>(
|
||||
this.modulesSignupRoutes[role],
|
||||
credentials,
|
||||
// {
|
||||
// headers: baseHeaders,
|
||||
// },
|
||||
)
|
||||
.pipe(
|
||||
tap((response) => {
|
||||
this.handleAuthSuccess(response);
|
||||
return response;
|
||||
}),
|
||||
catchError((error) => {
|
||||
console.error('Signup error:', error);
|
||||
this.isLoadingSubject.next(false);
|
||||
throw error;
|
||||
}),
|
||||
finalize(() => {
|
||||
this.isLoading.set(false);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
logout(): void {
|
||||
localStorage.removeItem(LOCAL_STORAGE_KEYS.AUTH_TOKEN);
|
||||
localStorage.removeItem(LOCAL_STORAGE_KEYS.USER_DATA);
|
||||
localStorage.removeItem(LOCAL_STORAGE_KEYS.REFRESH_TOKEN);
|
||||
|
||||
this.setCurrentUser(null);
|
||||
this.router.navigate(['/auth']);
|
||||
}
|
||||
|
||||
refreshToken(): Observable<IAuthResponse> {
|
||||
const refreshToken = this.getRefreshToken();
|
||||
if (!refreshToken) {
|
||||
this.logout();
|
||||
return throwError(() => new Error('No refresh token available'));
|
||||
}
|
||||
|
||||
return this.http
|
||||
.post<IAuthResponse>('/api/auth/refresh', { refreshToken })
|
||||
.pipe(
|
||||
tap((response) => {
|
||||
this.handleAuthSuccess(response);
|
||||
}),
|
||||
catchError((error) => {
|
||||
console.error('Token refresh error:', error);
|
||||
this.logout();
|
||||
return throwError(() => error);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
getInfo(role: TRoles): Observable<Partial<IUserLoginInfo>> {
|
||||
// if (this.modulesGetInfoRoutes[role]) {
|
||||
// return this.http.get<any>(this.modulesGetInfoRoutes[role]).pipe(
|
||||
// map((data) => {
|
||||
// switch (role) {
|
||||
// case 'SCHOOL':
|
||||
// return this.prepareUserInfoBasedOnRole(role, data as ISchoolMeResponse);
|
||||
// default:
|
||||
// return data as Partial<IUserLoginInfo>;
|
||||
// }
|
||||
// }),
|
||||
// catchError((err) => {
|
||||
// console.error('GetInfo error:', err);
|
||||
// return throwError(() => err);
|
||||
// }),
|
||||
// );
|
||||
// }
|
||||
|
||||
return throwError(() => new Error('متاسفانه مشکلی پیش آمده'));
|
||||
}
|
||||
changeInfo(
|
||||
credentials: IUserLoginInfo,
|
||||
role: TRoles,
|
||||
): Observable<IAuthResponse> {
|
||||
return this.http.post<IAuthResponse>(
|
||||
this.modulesChangeInfoRoutes[role],
|
||||
credentials,
|
||||
);
|
||||
}
|
||||
|
||||
// prepareUserInfoBasedOnRole(role: TRoles, info: ISchoolMeResponse): Partial<IUserLoginInfo> {
|
||||
// if (role === 'SCHOOL') {
|
||||
// return {
|
||||
// username: info.username,
|
||||
// email: info.managerInfo.email,
|
||||
// mobile: info.managerInfo.mobile,
|
||||
// };
|
||||
// }
|
||||
// return {};
|
||||
getInfo(role: TRoles): Observable<Partial<IUserLoginInfo>> {
|
||||
// if (this.modulesGetInfoRoutes[role]) {
|
||||
// return this.http.get<any>(this.modulesGetInfoRoutes[role]).pipe(
|
||||
// map((data) => {
|
||||
// switch (role) {
|
||||
// case 'SCHOOL':
|
||||
// return this.prepareUserInfoBasedOnRole(role, data as ISchoolMeResponse);
|
||||
// default:
|
||||
// return data as Partial<IUserLoginInfo>;
|
||||
// }
|
||||
// }),
|
||||
// catchError((err) => {
|
||||
// console.error('GetInfo error:', err);
|
||||
// return throwError(() => err);
|
||||
// }),
|
||||
// );
|
||||
// }
|
||||
|
||||
/**
|
||||
* Get current access token
|
||||
*/
|
||||
getToken(): Maybe<string> {
|
||||
return localStorage.getItem(LOCAL_STORAGE_KEYS.AUTH_TOKEN);
|
||||
return throwError(() => new Error('متاسفانه مشکلی پیش آمده'));
|
||||
}
|
||||
changeInfo(credentials: IUserLoginInfo, role: TRoles): Observable<IAuthResponse> {
|
||||
return this.http.post<IAuthResponse>(this.modulesChangeInfoRoutes[role], credentials);
|
||||
}
|
||||
|
||||
// prepareUserInfoBasedOnRole(role: TRoles, info: ISchoolMeResponse): Partial<IUserLoginInfo> {
|
||||
// if (role === 'SCHOOL') {
|
||||
// return {
|
||||
// username: info.username,
|
||||
// email: info.managerInfo.email,
|
||||
// mobile: info.managerInfo.mobile,
|
||||
// };
|
||||
// }
|
||||
// return {};
|
||||
// }
|
||||
|
||||
/**
|
||||
* Get current access token
|
||||
*/
|
||||
getToken(): Maybe<string> {
|
||||
return localStorage.getItem(LOCAL_STORAGE_KEYS.AUTH_TOKEN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current refresh token
|
||||
*/
|
||||
getRefreshToken(): Maybe<string> {
|
||||
return localStorage.getItem(LOCAL_STORAGE_KEYS.REFRESH_TOKEN);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if token is expired (optional - requires JWT parsing)
|
||||
*/
|
||||
isTokenExpired(): boolean {
|
||||
const token = this.getToken();
|
||||
if (!token) return true;
|
||||
|
||||
try {
|
||||
// Parse JWT token to check expiration
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if refresh token is expired
|
||||
*/
|
||||
isRefreshTokenExpired(): boolean {
|
||||
const refreshToken = this.getRefreshToken();
|
||||
if (!refreshToken) return true;
|
||||
|
||||
try {
|
||||
// Parse JWT refresh token to check expiration
|
||||
const payload = JSON.parse(atob(refreshToken.split('.')[1]));
|
||||
const currentTime = Math.floor(Date.now() / 1000);
|
||||
return payload.exp < currentTime;
|
||||
} catch (error) {
|
||||
console.error('Error parsing refresh token:', error);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
hasRole(role: TRoles): boolean {
|
||||
return this.currentAccount()?.type === role;
|
||||
}
|
||||
|
||||
hasAnyRole(roles: TRoles[]): boolean {
|
||||
const currentRole = this.currentAccount()?.type;
|
||||
return currentRole ? roles.includes(currentRole) : false;
|
||||
}
|
||||
|
||||
hasPermission(permission: string): boolean {
|
||||
const user = this.currentAccount();
|
||||
if (!user) return false;
|
||||
|
||||
return true;
|
||||
// return user.permissions.some(
|
||||
// (p) => p.name === permission || `${p.resource}:${p.action}` === permission,
|
||||
// );
|
||||
}
|
||||
|
||||
canAccess(requiredRoles?: TRoles[], requiredPermissions?: string[]): boolean {
|
||||
if (!this.isAuthenticated()) return false;
|
||||
|
||||
if (requiredRoles && requiredRoles.length > 0) {
|
||||
if (!this.hasAnyRole(requiredRoles)) return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current refresh token
|
||||
*/
|
||||
getRefreshToken(): Maybe<string> {
|
||||
return localStorage.getItem(LOCAL_STORAGE_KEYS.REFRESH_TOKEN);
|
||||
if (requiredPermissions && requiredPermissions.length > 0) {
|
||||
return requiredPermissions.every((permission) => this.hasPermission(permission));
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if token is expired (optional - requires JWT parsing)
|
||||
*/
|
||||
isTokenExpired(): boolean {
|
||||
const token = this.getToken();
|
||||
if (!token) return true;
|
||||
return true;
|
||||
}
|
||||
|
||||
try {
|
||||
// Parse JWT token to check expiration
|
||||
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;
|
||||
}
|
||||
}
|
||||
private handleAuthSuccess(response: IAuthResponse): void {
|
||||
localStorage.setItem(LOCAL_STORAGE_KEYS.AUTH_TOKEN, response.accessToken);
|
||||
this.token.set(response.accessToken);
|
||||
this.refreshToken.set(response.refreshToken);
|
||||
localStorage.setItem(LOCAL_STORAGE_KEYS.REFRESH_TOKEN, response.refreshToken);
|
||||
|
||||
/**
|
||||
* Check if refresh token is expired
|
||||
*/
|
||||
isRefreshTokenExpired(): boolean {
|
||||
const refreshToken = this.getRefreshToken();
|
||||
if (!refreshToken) return true;
|
||||
this.setCurrentUser(response.account);
|
||||
this.isLoadingSubject.next(false);
|
||||
|
||||
try {
|
||||
// Parse JWT refresh token to check expiration
|
||||
const payload = JSON.parse(atob(refreshToken.split('.')[1]));
|
||||
const currentTime = Math.floor(Date.now() / 1000);
|
||||
return payload.exp < currentTime;
|
||||
} catch (error) {
|
||||
console.error('Error parsing refresh token:', error);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Navigate based on user role
|
||||
// this.navigateByRole(response.user.role);
|
||||
}
|
||||
|
||||
hasRole(role: TRoles): boolean {
|
||||
return this.currentUser()?.role === role;
|
||||
}
|
||||
|
||||
hasAnyRole(roles: TRoles[]): boolean {
|
||||
const currentRole = this.currentUser()?.role;
|
||||
return currentRole ? roles.includes(currentRole) : false;
|
||||
}
|
||||
|
||||
hasPermission(permission: string): boolean {
|
||||
const user = this.currentUser();
|
||||
if (!user) return false;
|
||||
|
||||
return true;
|
||||
// return user.permissions.some(
|
||||
// (p) => p.name === permission || `${p.resource}:${p.action}` === permission,
|
||||
// );
|
||||
}
|
||||
|
||||
canAccess(
|
||||
requiredRoles?: TRoles[],
|
||||
requiredPermissions?: string[],
|
||||
): boolean {
|
||||
if (!this.isAuthenticated()) return false;
|
||||
|
||||
if (requiredRoles && requiredRoles.length > 0) {
|
||||
if (!this.hasAnyRole(requiredRoles)) return false;
|
||||
}
|
||||
|
||||
if (requiredPermissions && requiredPermissions.length > 0) {
|
||||
return requiredPermissions.every((permission) =>
|
||||
this.hasPermission(permission),
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private handleAuthSuccess(response: IAuthResponse): void {
|
||||
localStorage.setItem(LOCAL_STORAGE_KEYS.AUTH_TOKEN, response.token);
|
||||
this.token.set(response.token);
|
||||
// localStorage.setItem('refresh_token', response.refreshToken);
|
||||
const user = {
|
||||
fullName: response.fullName,
|
||||
role: response.role as TRoles,
|
||||
};
|
||||
|
||||
localStorage.setItem(
|
||||
LOCAL_STORAGE_KEYS.USER_DATA,
|
||||
JSON.stringify(user),
|
||||
);
|
||||
|
||||
this.setCurrentUser(user);
|
||||
// this.isLoading.set(false);
|
||||
this.currentUser.set({
|
||||
fullName: response.fullName,
|
||||
role: response.role as TRoles,
|
||||
});
|
||||
this.isLoadingSubject.next(false);
|
||||
|
||||
// Navigate based on user role
|
||||
// this.navigateByRole(response.user.role);
|
||||
}
|
||||
|
||||
private setCurrentUser(user: Maybe<User>): void {
|
||||
this.currentUser.set(user);
|
||||
this.currentUserSubject.next(user);
|
||||
}
|
||||
private setCurrentUser(account: Maybe<IAuthAccountResponse>): void {
|
||||
this.currentAccount.set(account);
|
||||
this.currentAccountSubject.next(account);
|
||||
localStorage.setItem(LOCAL_STORAGE_KEYS.USER_DATA, JSON.stringify(account));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user