Аутентификация
Система аутентификации в SaaS Admin основана на JWT токенах с поддержкой refresh tokens.
Обзор
Аутентификация включает в себя:
- ✅ Вход по email и паролю
- ✅ Регистрация новых пользователей
- ✅ JWT access и refresh токены
- ✅ Автоматическое добавление токена к запросам
- ✅ Защита маршрутов через guards
- ✅ Автоматический logout при 401
Архитектура
┌─────────────┐
│ User │
└──────┬──────┘
│
▼
┌─────────────────┐ ┌──────────────┐
│ Login Page │────▶│ AuthService │
└─────────────────┘ └──────┬───────┘
│
▼
┌─────────────┐
│ API │
│ /admin/auth│
└──────┬──────┘
│
▼
┌─────────────────┐
│ StorageService │
│ (localStorage) │
│ - access_token │
│ - refresh_token │
└─────────────────┘Функциональность
Вход в систему
Страница: /auth/login
- Пользователь вводит email и пароль
- Отправка запроса
POST /auth/sign-in - Получение access и refresh токенов
- Сохранение токенов в localStorage
- Редирект на
/admin/dashboard
Пример кода:
@Component({
selector: "app-login",
standalone: true,
imports: [ReactiveFormsModule, MatFormFieldModule, MatInputModule],
templateUrl: "./login.component.html"
})
export class LoginComponent {
private readonly authService = inject(AuthService);
private readonly router = inject(Router);
loginForm = new FormGroup({
email: new FormControl("", [Validators.required, Validators.email]),
password: new FormControl("", [Validators.required])
});
onSubmit() {
if (this.loginForm.valid) {
const { email, password } = this.loginForm.value;
this.authService.login(email!, password!).subscribe({
next: () => {
this.router.navigate(["/admin/dashboard"]);
},
error: (error) => {
console.error("Login failed:", error);
}
});
}
}
}Регистрация
Страница: /auth/register
- Пользователь заполняет форму регистрации
- Отправка запроса
POST /auth/sign-up - Автоматический вход после успешной регистрации
Выход из системы
Выход происходит при:
- Нажатии кнопки "Выход"
- Получении 401 ошибки
- Истечении токена
logout() {
this.authService.logout();
// Токены удаляются
// Редирект на /auth/login
}AuthService API
AuthService використовує HttpClient напряму для auth ендпоінтів, ApiService для admin-операцій, StorageService для зберігання токенів, UniversalService для SSR-сумісності та EnvService для baseUrl.
Методы
login(email: string, password: string): Observable<undefined>
Виконує вхід користувача. Використовує HttpClient напряму.
this.authService.login("user@example.com", "password123").subscribe(() => {
console.log("Успешный вход");
});logout(): void
Виконує вихід, очищує токени через StorageService і робить редірект.
this.authService.logout();me(): Observable<IUser>
Отримує інформацію про поточного користувача. Використовує HttpClient напряму.
this.authService.me().subscribe((user) => {
console.log(user.email);
});refreshTokens(): Observable<ILoginResponse>
Оновлює access та refresh токени. Використовує HttpClient напряму.
this.authService.refreshTokens().subscribe(() => {
// Токени автоматично оновлюються
});updateMe(data: { firstName?: string; lastName?: string }): Observable<IUser>
Оновлює профіль поточного користувача. Використовує ApiService.
getAdminUsers(params?): Observable<IPaginatedResponse<IUser>>
Отримує список адмін-користувачів з пагінацією. Використовує ApiService.
createAdminUser(data): Observable<IUser>
Створює нового адмін-користувача. Використовує ApiService.
deleteAdminUser(id: string)
Видаляє адмін-користувача. Використовує ApiService.
getTokenClaims(): JwtPayload | null
Декодує JWT токен та повертає claims.
const claims = this.authService.getTokenClaims();
// claims містить: sub, email, exp, iat і т.д.isAuthenticated: boolean
Перевірка авторизації. Повертає false на сервері (SSR).
if (this.authService.isAuthenticated) {
// Користувач авторизований
}isAuthenticated$: Observable<boolean>
Observable для реактивної перевірки авторизації.
this.authService.isAuthenticated$.subscribe((isAuth) => {
this.showAdminPanel = isAuth;
});Guards
authGuard
Защищает маршруты, требующие авторизации.
Использование:
const routes: Routes = [
{
path: "admin",
canActivate: [authGuard],
loadComponent: () => import("./admin.component")
}
];Логика:
export const authGuard: CanActivateFn = () => {
const authService = inject(AuthService);
const router = inject(Router);
if (!authService.isAuthenticated) {
return router.createUrlTree(["/auth/login"]);
}
return true;
};noAuthGuard
Редиректит авторизованных пользователей (для страниц login/register).
Использование:
const routes: Routes = [
{
path: "auth/login",
canActivate: [noAuthGuard],
loadComponent: () => import("./login.component")
}
];Interceptors
Auth Interceptor
Автоматически додає JWT токен до всіх запитів через заголовок x-admin-token, використовуючи StorageService (через AuthService.getAccessToken() з ключем STORAGE_KEYS.ACCESS_TOKEN). Також обробляє 401 помилки з автоматичним оновленням токенів.
const AUTH_URLS = ["/auth/sign-in", "/auth/refresh"];
function addToken(req: Parameters<HttpInterceptorFn>[0], token: string | null) {
if (!token) return req;
return req.clone({ setHeaders: { "x-admin-token": token } });
}
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const authService = inject(AuthService);
req = addToken(req, authService.getAccessToken());
return next(req).pipe(
catchError((error) => {
if (error.status !== 401 || isAuthRequest(req.url)) {
return throwError(() => error);
}
if (!isRefreshing) {
isRefreshing = true;
return authService.refreshTokens().pipe(
switchMap(() => {
isRefreshing = false;
return next(addToken(req, authService.getAccessToken()));
}),
catchError((refreshError) => {
isRefreshing = false;
authService.logout();
return throwError(() => refreshError);
})
);
}
// Якщо refresh вже виконується — чекаємо завершення
return refreshDone$.pipe(
filter(Boolean),
take(1),
switchMap(() => next(addToken(req, authService.getAccessToken())))
);
})
);
};Хранение токенов
Токени зберігаються через StorageService, який є обгорткою над localStorage з SSR-безпечними перевірками через UniversalService. Ключі визначені в STORAGE_KEYS:
// Ключі (shared/constants/storage-keys.constant.ts)
export const STORAGE_KEYS = {
ACCESS_TOKEN: "access_token",
REFRESH_TOKEN: "refresh_token"
// ...
} as const;
// Збереження (через AuthService.setTokens)
this.storage.set(STORAGE_KEYS.ACCESS_TOKEN, accessToken);
this.storage.set(STORAGE_KEYS.REFRESH_TOKEN, refreshToken);
// Отримання
const token = this.storage.getString(STORAGE_KEYS.ACCESS_TOKEN);
// Видалення (через AuthService.clearTokens)
this.storage.remove(STORAGE_KEYS.ACCESS_TOKEN);
this.storage.remove(STORAGE_KEYS.REFRESH_TOKEN);SSR
StorageService використовує UniversalService.isBrowser для перевірки оточення перед роботою з localStorage, що необхідно для коректної роботи SSR.
JWT Claims
Токен можно декодировать для получения claims:
import { jwtDecode } from "jwt-decode";
const claims = this.authService.getTokenClaims();
// claims содержит: userId, email, exp, iat и т.д.Безопасность
Best Practices
✅ Что делается:
- Токены в localStorage (не в cookies)
- HTTPS обязателен для production
- Токены автоматически прикрепляются к запросам
- Автоматический logout при 401
❌ Что НЕ делается:
- XSS защита — ответственность разработчика
- CSRF — не применимо при использовании JWT без cookies
Рекомендации
- Всегда используйте HTTPS в production
- Устанавливайте короткое время жизни access токена
- Используйте refresh токен для обновления
- Валидируйте токены на backend'е
API Endpoints
| Метод | Endpoint | Описание |
|---|---|---|
| POST | /auth/sign-in | Вход в систему |
| POST | /auth/sign-up | Регистрация |
| GET | /auth/me | Получить текущего пользователя |
| POST | /auth/refresh | Обновить токен (если реализовано) |
Следующие шаги
- Интеграции — управление интеграциями
- Access Tokens — токены для API
- API Reference — документация AuthService