Архитектура приложения
SaaS Admin построен на Angular 21 с использованием современных подходов и best practices.
Общая архитектура
Приложение использует модульную архитектуру с четким разделением ответственности:
src/app/
├── core/ # Ядро приложения
├── admin/ # Админ секция
└── shared/ # Shared компонентыCore
Содержит основную функциональность приложения:
- Services — бизнес-логика и работа с API
- Guards — защита маршрутов
- Interceptors — обработка HTTP запросов
- Interfaces — TypeScript типы и интерфейсы
- Routes — определение роутов
- Config — конфигурация приложения
- Utils — утилитарные функции
Admin
Содержит админскую часть приложения:
- Layout — общий layout с навигацией
- Pages — страницы функциональности (features)
Shared
Содержит переиспользуемые компоненты (35+):
- Components — UI компоненты (data-table, charts, dialogs, editors, status badges и т.д.)
- Classes — общие классы
- Constants — константы
- Pipes — Angular pipes
- Utils — утилиты
Роутинг
Приложение использует Angular Router с lazy loading:
export const CORE_ROUTES: Routes = [
{
path: 'auth',
children: [
{ path: 'login', loadComponent: ... }
]
},
{
path: 'admin',
canActivate: [authGuard],
loadComponent: ...,
children: [
{ path: 'dashboard', loadComponent: ... },
{ path: 'analytics', loadComponent: ... },
{ path: 'users', children: [...] },
{ path: 'companies', children: [...] },
{ path: 'assistants', children: [...] },
{ path: 'chats', children: [...] },
// Billing
{ path: 'tariffs', children: [...] },
{ path: 'subscriptions', children: [...] },
{ path: 'invoices', children: [...] },
{ path: 'payments', children: [...] },
{ path: 'billing-events', children: [...] },
{ path: 'refunds', children: [...] },
{ path: 'coupons', children: [...] },
// Broadcasts
{ path: 'broadcasts', children: [...] },
// System
{ path: 'integrations', children: [...] },
{ path: 'phones', children: [...] },
{ path: 'workflows', children: [...] },
{ path: 'history', children: [...] },
{ path: 'settings', children: [
{ path: 'account', loadComponent: ... },
{ path: 'access-tokens', loadComponent: ... }
]}
]
}
];Защита маршрутов
Используются два guard'а:
- authGuard — проверяет авторизацию для доступа к админ-панели
- noAuthGuard — редирект для авторизованных пользователей (login)
State Management
Приложение использует @ngneat/elf для управления состоянием:
import { createStore } from "@ngneat/elf";
import { withEntities } from "@ngneat/elf-entities";
const usersStore = createStore({ name: "users" }, withEntities<IUser>());Преимущества Elf
- Легковесный (меньше boilerplate, чем NgRx)
- Типобезопасный
- Поддержка DevTools
- Персистентность состояния
HTTP Layer
Формат API Ответов
API использует два разных формата для разных типов эндпоинтов:
Auth эндпоинты (БЕЗ обёртки)
Аутентификационные эндпоинты возвращают данные напрямую:
// POST /auth/sign-in
{ accessToken: string, refreshToken: string }
// GET /auth/me
{ id: string, email: string, firstName?: string, lastName?: string }Остальные эндпоинты (С обёрткой)
Все остальные эндпоинты возвращают данные в стандартной обёртке:
// GET /users
{ success: true, data: IUser[] }
// POST /assistants/:id
{ success: true, data: IAssistant }API Service
Базовый сервис для работы с API. Автоматически извлекает data из обёртки:
interface IApiWrapper<T> {
success: boolean;
data: T;
}
@Injectable({ providedIn: "root" })
export class ApiService {
private readonly http = inject(HttpClient);
get<T>(url: string) {
return this.http.get<IApiWrapper<T>>(url).pipe(map((response) => response.data));
}
post<T>(url: string, data: unknown) {
return this.http.post<IApiWrapper<T>>(url, data).pipe(map((response) => response.data));
}
patch<T>(url: string, data: unknown) {
return this.http.patch<IApiWrapper<T>>(url, data).pipe(map((response) => response.data));
}
delete<T>(url: string) {
return this.http.delete<IApiWrapper<T>>(url).pipe(map((response) => response.data));
}
// Raw методы — для эндпоинтов без admin prefix
getRaw<T>(endpoint: string, params?) {
return this.http.get<IApiWrapper<T>>(`${apiUrl}${endpoint}`, { params });
}
postRaw<T>(endpoint: string, body) {
return this.http.post<IApiWrapper<T>>(`${apiUrl}${endpoint}`, body);
}
patchRaw<T>(endpoint: string, body) {
return this.http.patch<IApiWrapper<T>>(`${apiUrl}${endpoint}`, body);
}
deleteRaw<T>(endpoint: string) {
return this.http.delete<IApiWrapper<T>>(`${apiUrl}${endpoint}`);
}
}Interceptors
Auth Interceptor
Автоматически добавляет JWT токен ко всем запросам:
export const authInterceptor: HttpInterceptorFn = (req, next) => {
const token = localStorage.getItem("access_token");
if (token) {
req = req.clone({
setHeaders: { Authorization: `Bearer ${token}` }
});
}
return next(req);
};Error Interceptor
Обрабатывает ошибки и выполняет logout при 401:
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
return next(req).pipe(
catchError((error: HttpErrorResponse) => {
if (error.status === 401) {
// Logout logic
}
return throwError(() => error);
})
);
};Аутентификация
Flow
- Пользователь вводит credentials
- Отправка запроса на
/auth/sign-in - Получение JWT токенов (access + refresh)
- Сохранение токенов в localStorage
- Перенаправление в админ-панель
AuthService
@Injectable({ providedIn: "root" })
export class AuthService {
private readonly api = inject(ApiService);
login(credentials: ILoginRequest) {
return this.api.post<IAuthResponse>("/auth/sign-in", credentials).pipe(
tap((response) => {
localStorage.setItem("access_token", response.accessToken);
localStorage.setItem("refresh_token", response.refreshToken);
})
);
}
logout() {
localStorage.removeItem("access_token");
localStorage.removeItem("refresh_token");
this.router.navigate(["/auth/login"]);
}
isAuthenticated() {
return !!localStorage.getItem("access_token");
}
}Структура сервисов
Все доменные сервисы следуют единому паттерну и инжектят ApiService:
@Injectable({ providedIn: "root" })
export class UsersService {
private readonly api = inject(ApiService);
getAll() {
return this.api.get<IUser[]>("/users");
}
getById(id: string) {
return this.api.get<IUser>(`/users/${id}`);
}
create(data: ICreateUserRequest) {
return this.api.post<IUser>("/users", data);
}
update(id: string, data: IUpdateUserRequest) {
return this.api.patch<IUser>(`/users/${id}`, data);
}
delete(id: string) {
return this.api.delete<null>(`/users/${id}`);
}
}Основные сервисы
| Сервис | Описание |
|---|---|
AuthService | Аутентификация, JWT токены, refresh |
UsersService | Управление пользователями |
CompaniesService | Управление компаниями |
AssistantsService | Управление голосовыми и текстовыми ассистентами |
ChatsService | Управление чатами и сообщениями |
BillingService | Тарифы, подписки, счета, платежи, возвраты, купоны, события биллинга |
WorkflowsService | Мониторинг Temporal workflows |
BroadcastsService | Email broadcasts, аудитории, получатели, email-события |
IntegrationsService | Управление интеграциями |
PhonesService | Управление SIP телефонами |
HistoryService | История звонков и чатов |
AnalyticsService | Аналитика и статистика |
WebsocketsService | WebSocket коммуникация |
ThemeService | Управление темой (light/dark) |
LanguageService | Управление языком (i18n) |
HealthCheckService | Мониторинг здоровья системы |
Компоненты
Все компоненты являются standalone (без NgModules):
@Component({
selector: "app-user-detail",
standalone: true,
imports: [CommonModule, MatCardModule, MatButtonModule],
templateUrl: "./user-detail.component.html",
styleUrl: "./user-detail.component.scss",
changeDetection: ChangeDetectionStrategy.OnPush
})
export class UserDetailComponent {
private readonly route = inject(ActivatedRoute);
private readonly service = inject(UsersService);
}Angular Material
Приложение использует Angular Material 21 с Material Design 3:
imports: [MatCardModule, MatButtonModule, MatIconModule, MatTableModule, MatDialogModule];Темизация
Темы настраиваются в styles.scss:
@use "@angular/material" as mat;
$theme: mat.define-theme(
(
color: (
theme-type: light,
primary: mat.$violet-palette
)
)
);
html {
@include mat.all-component-themes($theme);
}Server-Side Rendering
Приложение поддерживает SSR через Angular SSR + Express:
export function app() {
const server = express();
const serverDistFolder = dirname(fileURLToPath(import.meta.url));
const browserDistFolder = resolve(serverDistFolder, "../browser");
server.get("*.*", express.static(browserDistFolder));
server.get("*", (req, res) => {
// SSR logic
});
return server;
}SSR Configuration
- Protected routes (
/admin/**) используютRenderMode.Client— без SSR для аутентифицированных страниц - Auth pages (
/auth/**) используютRenderMode.Server
Deployment
Production сборка
npm run build:prodГенерируются два варианта:
- dist/admin/browser — клиентская часть
- dist/admin/server — серверная часть для SSR
Docker
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY dist ./dist
CMD ["node", "dist/admin/server/server.mjs"]Следующие шаги
- Паттерны — архитектурные решения
- Правила кода — правила написания кода
- Качество кода — линтинг и форматирование