Skip to content

Архитектура приложения

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:

typescript
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 для управления состоянием:

typescript
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 эндпоинты (БЕЗ обёртки)

Аутентификационные эндпоинты возвращают данные напрямую:

typescript
// POST /auth/sign-in
{ accessToken: string, refreshToken: string }

// GET /auth/me
{ id: string, email: string, firstName?: string, lastName?: string }

Остальные эндпоинты (С обёрткой)

Все остальные эндпоинты возвращают данные в стандартной обёртке:

typescript
// GET /users
{ success: true, data: IUser[] }

// POST /assistants/:id
{ success: true, data: IAssistant }

API Service

Базовый сервис для работы с API. Автоматически извлекает data из обёртки:

typescript
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 токен ко всем запросам:

typescript
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:

typescript
export const errorInterceptor: HttpInterceptorFn = (req, next) => {
	return next(req).pipe(
		catchError((error: HttpErrorResponse) => {
			if (error.status === 401) {
				// Logout logic
			}
			return throwError(() => error);
		})
	);
};

Аутентификация

Flow

  1. Пользователь вводит credentials
  2. Отправка запроса на /auth/sign-in
  3. Получение JWT токенов (access + refresh)
  4. Сохранение токенов в localStorage
  5. Перенаправление в админ-панель

AuthService

typescript
@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:

typescript
@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
BroadcastsServiceEmail broadcasts, аудитории, получатели, email-события
IntegrationsServiceУправление интеграциями
PhonesServiceУправление SIP телефонами
HistoryServiceИстория звонков и чатов
AnalyticsServiceАналитика и статистика
WebsocketsServiceWebSocket коммуникация
ThemeServiceУправление темой (light/dark)
LanguageServiceУправление языком (i18n)
HealthCheckServiceМониторинг здоровья системы

Компоненты

Все компоненты являются standalone (без NgModules):

typescript
@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:

typescript
imports: [MatCardModule, MatButtonModule, MatIconModule, MatTableModule, MatDialogModule];

Темизация

Темы настраиваются в styles.scss:

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:

typescript
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 сборка

bash
npm run build:prod

Генерируются два варианта:

  • dist/admin/browser — клиентская часть
  • dist/admin/server — серверная часть для SSR

Docker

dockerfile
FROM node:22-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --production
COPY dist ./dist
CMD ["node", "dist/admin/server/server.mjs"]

Следующие шаги

SaaS Admin Documentation