Refactor Protected and Public Routes for Consistent Loading UI; Enhance HomePage with User Company and Branch Details; Update LoginPage Layout; Introduce User Relations in Auth Services
This commit is contained in:
@@ -14,7 +14,7 @@ function ProtectedRoute({ children }: ProtectedRouteProps) {
|
||||
// Показываем загрузку пока проверяем авторизацию
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="flex justify-center items-center min-h-screen">
|
||||
<div className="text-xl">Проверка авторизации...</div>
|
||||
</div>
|
||||
);
|
||||
@@ -30,4 +30,3 @@ function ProtectedRoute({ children }: ProtectedRouteProps) {
|
||||
}
|
||||
|
||||
export default ProtectedRoute;
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@ function PublicRoute({ children }: PublicRouteProps) {
|
||||
// Показываем загрузку пока проверяем авторизацию
|
||||
if (isLoading) {
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center">
|
||||
<div className="flex justify-center items-center min-h-screen">
|
||||
<div className="text-xl">Загрузка...</div>
|
||||
</div>
|
||||
);
|
||||
@@ -31,4 +31,3 @@ function PublicRoute({ children }: PublicRouteProps) {
|
||||
}
|
||||
|
||||
export default PublicRoute;
|
||||
|
||||
|
||||
@@ -18,4 +18,3 @@ export const api = ky.create({
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -12,4 +12,3 @@ export const queryClient = new QueryClient({
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -12,24 +12,68 @@ function HomePage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50 py-8">
|
||||
<div className="max-w-4xl mx-auto px-4">
|
||||
<div className="bg-white rounded-lg shadow-md p-8">
|
||||
<h1 className="text-3xl font-bold mb-6">Главная страница</h1>
|
||||
<div className="py-8 min-h-screen bg-gray-50">
|
||||
<div className="px-4 mx-auto max-w-4xl">
|
||||
<div className="p-8 bg-white rounded-lg shadow-md">
|
||||
<h1 className="mb-6 text-3xl font-bold">Главная страница</h1>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="bg-blue-50 border border-blue-200 rounded-lg p-4">
|
||||
<h2 className="text-xl font-semibold mb-2">
|
||||
<div className="p-4 bg-blue-50 rounded-lg border border-blue-200">
|
||||
<h2 className="mb-2 text-xl font-semibold">
|
||||
Добро пожаловать, {user?.fullName}!
|
||||
</h2>
|
||||
<p className="text-gray-600">Email: {user?.email}</p>
|
||||
<p className="text-gray-600">Роль: {user?.role}</p>
|
||||
</div>
|
||||
|
||||
{user?.currentCompany && (
|
||||
<div className="p-4 bg-green-50 rounded-lg border border-green-200">
|
||||
<h3 className="mb-2 text-lg font-semibold">Компания</h3>
|
||||
<p className="font-medium text-gray-700">
|
||||
{user.currentCompany.name}
|
||||
</p>
|
||||
{user.currentCompany.description && (
|
||||
<p className="mt-1 text-sm text-gray-600">
|
||||
{user.currentCompany.description}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{user?.currentBranch && (
|
||||
<div className="p-4 bg-purple-50 rounded-lg border border-purple-200">
|
||||
<h3 className="mb-2 text-lg font-semibold">Филиал</h3>
|
||||
<p className="font-medium text-gray-700">
|
||||
{user.currentBranch.name}
|
||||
</p>
|
||||
{user.currentBranch.address && (
|
||||
<p className="mt-1 text-sm text-gray-600">
|
||||
Адрес: {user.currentBranch.address}
|
||||
</p>
|
||||
)}
|
||||
{(user.currentBranch.city || user.currentBranch.country) && (
|
||||
<p className="text-sm text-gray-600">
|
||||
{[user.currentBranch.city, user.currentBranch.country]
|
||||
.filter(Boolean)
|
||||
.join(", ")}
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!user?.currentBranch && !user?.currentCompany && (
|
||||
<div className="p-4 bg-yellow-50 rounded-lg border border-yellow-200">
|
||||
<p className="text-yellow-800">
|
||||
Вы не привязаны ни к одному филиалу. Обратитесь к
|
||||
администратору.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={handleLogout}
|
||||
disabled={logoutMutation.isPending}
|
||||
className="px-4 py-2 bg-red-600 text-white rounded-md hover:bg-red-700 disabled:opacity-50"
|
||||
className="px-4 py-2 text-white bg-red-600 rounded-md hover:bg-red-700 disabled:opacity-50"
|
||||
>
|
||||
{logoutMutation.isPending ? "Выход..." : "Выйти"}
|
||||
</button>
|
||||
|
||||
@@ -22,8 +22,8 @@ function LoginPage() {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex items-center justify-center bg-gray-50">
|
||||
<div className="max-w-md w-full space-y-8 p-8 bg-white rounded-lg shadow-md">
|
||||
<div className="flex justify-center items-center min-h-screen bg-gray-50">
|
||||
<div className="p-8 space-y-8 w-full max-w-md bg-white rounded-lg shadow-md">
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold text-center text-gray-900">
|
||||
Вход в аккаунт
|
||||
@@ -45,7 +45,7 @@ function LoginPage() {
|
||||
required
|
||||
value={email}
|
||||
onChange={(e) => setEmail(e.target.value)}
|
||||
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
className="block px-3 py-2 mt-1 w-full rounded-md border border-gray-300 shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="your@email.com"
|
||||
/>
|
||||
</div>
|
||||
@@ -63,14 +63,14 @@ function LoginPage() {
|
||||
required
|
||||
value={password}
|
||||
onChange={(e) => setPassword(e.target.value)}
|
||||
className="mt-1 block w-full px-3 py-2 border border-gray-300 rounded-md shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
className="block px-3 py-2 mt-1 w-full rounded-md border border-gray-300 shadow-sm focus:outline-none focus:ring-blue-500 focus:border-blue-500"
|
||||
placeholder="••••••••"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{loginMutation.isError && (
|
||||
<div className="text-red-600 text-sm text-center">
|
||||
<div className="text-sm text-center text-red-600">
|
||||
Неверный email или пароль
|
||||
</div>
|
||||
)}
|
||||
@@ -78,14 +78,17 @@ function LoginPage() {
|
||||
<button
|
||||
type="submit"
|
||||
disabled={loginMutation.isPending}
|
||||
className="w-full flex justify-center py-2 px-4 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
className="flex justify-center px-4 py-2 w-full text-sm font-medium text-white bg-blue-600 rounded-md border border-transparent shadow-sm hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
>
|
||||
{loginMutation.isPending ? "Вход..." : "Войти"}
|
||||
</button>
|
||||
|
||||
<div className="text-center text-sm text-gray-600">
|
||||
<div className="text-sm text-center text-gray-600">
|
||||
Нет аккаунта?{" "}
|
||||
<Link to="/register" className="text-blue-600 hover:text-blue-700 font-medium">
|
||||
<Link
|
||||
to="/register"
|
||||
className="font-medium text-blue-600 hover:text-blue-700"
|
||||
>
|
||||
Зарегистрироваться
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -1,11 +1,27 @@
|
||||
export type RoleName = "admin" | "director" | "manager";
|
||||
|
||||
export interface Branch {
|
||||
id: string;
|
||||
name: string;
|
||||
address: string | null;
|
||||
city: string | null;
|
||||
country: string | null;
|
||||
}
|
||||
|
||||
export interface Company {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
}
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
email: string;
|
||||
fullName: string;
|
||||
role: RoleName;
|
||||
createdAt: string;
|
||||
currentBranch?: Branch;
|
||||
currentCompany?: Company;
|
||||
}
|
||||
|
||||
export interface LoginData {
|
||||
@@ -35,4 +51,3 @@ export interface RegisterResponse {
|
||||
export interface MeResponse {
|
||||
user: User;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,376 @@
|
||||
# Компании и филиалы - Документация
|
||||
|
||||
## Обзор
|
||||
|
||||
Добавлена функциональность для управления компаниями и филиалами с поддержкой привязки пользователей к нескольким филиалам.
|
||||
|
||||
## Структура базы данных
|
||||
|
||||
### Таблицы
|
||||
|
||||
#### `companies` - Компании
|
||||
- `id` - UUID, первичный ключ
|
||||
- `name` - Название компании (обязательное)
|
||||
- `description` - Описание компании
|
||||
- `createdAt` - Дата создания
|
||||
- `updatedAt` - Дата обновления
|
||||
|
||||
#### `branches` - Филиалы
|
||||
- `id` - UUID, первичный ключ
|
||||
- `companyId` - UUID, внешний ключ на компанию (каскадное удаление)
|
||||
- `name` - Название филиала (обязательное)
|
||||
- `address` - Адрес филиала
|
||||
- `city` - Город
|
||||
- `country` - Страна
|
||||
- `createdAt` - Дата создания
|
||||
- `updatedAt` - Дата обновления
|
||||
|
||||
#### `user_branches` - Связь пользователей и филиалов (Many-to-Many)
|
||||
- `userId` - UUID, внешний ключ на пользователя (каскадное удаление)
|
||||
- `branchId` - UUID, внешний ключ на филиал (каскадное удаление)
|
||||
- `createdAt` - Дата создания
|
||||
- Составной первичный ключ: `(userId, branchId)`
|
||||
|
||||
### Изменения в таблице `users`
|
||||
Добавлено поле:
|
||||
- `currentBranchId` - UUID, внешний ключ на выбранный филиал (nullable)
|
||||
|
||||
## API Эндпоинты
|
||||
|
||||
Все эндпоинты требуют авторизации через Bearer токен.
|
||||
|
||||
### Компании (`/companies`)
|
||||
|
||||
#### `GET /companies`
|
||||
Получить список всех компаний с их филиалами.
|
||||
|
||||
**Ответ:**
|
||||
```json
|
||||
{
|
||||
"companies": [
|
||||
{
|
||||
"id": "uuid",
|
||||
"name": "Компания 1",
|
||||
"description": "Описание",
|
||||
"branches": [...],
|
||||
"createdAt": "2024-01-01T00:00:00Z",
|
||||
"updatedAt": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### `GET /companies/:id`
|
||||
Получить компанию по ID.
|
||||
|
||||
**Параметры:**
|
||||
- `id` - UUID компании
|
||||
|
||||
**Ответ:**
|
||||
```json
|
||||
{
|
||||
"company": {
|
||||
"id": "uuid",
|
||||
"name": "Компания 1",
|
||||
"description": "Описание",
|
||||
"branches": [...],
|
||||
"createdAt": "2024-01-01T00:00:00Z",
|
||||
"updatedAt": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### `POST /companies`
|
||||
Создать новую компанию.
|
||||
|
||||
**Body:**
|
||||
```json
|
||||
{
|
||||
"name": "Новая компания",
|
||||
"description": "Описание (опционально)"
|
||||
}
|
||||
```
|
||||
|
||||
**Ответ:**
|
||||
```json
|
||||
{
|
||||
"company": {
|
||||
"id": "uuid",
|
||||
"name": "Новая компания",
|
||||
"description": "Описание",
|
||||
"createdAt": "2024-01-01T00:00:00Z",
|
||||
"updatedAt": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### `PATCH /companies/:id`
|
||||
Обновить компанию.
|
||||
|
||||
**Body:**
|
||||
```json
|
||||
{
|
||||
"name": "Обновленное название (опционально)",
|
||||
"description": "Обновленное описание (опционально)"
|
||||
}
|
||||
```
|
||||
|
||||
#### `DELETE /companies/:id`
|
||||
Удалить компанию (каскадно удалит все филиалы).
|
||||
|
||||
**Ответ:**
|
||||
```json
|
||||
{
|
||||
"message": "Company deleted successfully"
|
||||
}
|
||||
```
|
||||
|
||||
### Филиалы (`/branches`)
|
||||
|
||||
#### `GET /branches/my`
|
||||
Получить филиалы текущего пользователя.
|
||||
|
||||
**Ответ:**
|
||||
```json
|
||||
{
|
||||
"branches": [
|
||||
{
|
||||
"id": "uuid",
|
||||
"companyId": "uuid",
|
||||
"name": "Филиал 1",
|
||||
"address": "Адрес",
|
||||
"city": "Город",
|
||||
"country": "Страна",
|
||||
"company": {...},
|
||||
"createdAt": "2024-01-01T00:00:00Z",
|
||||
"updatedAt": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### `GET /branches/:id`
|
||||
Получить филиал по ID.
|
||||
|
||||
**Ответ:**
|
||||
```json
|
||||
{
|
||||
"branch": {
|
||||
"id": "uuid",
|
||||
"companyId": "uuid",
|
||||
"name": "Филиал 1",
|
||||
"address": "Адрес",
|
||||
"city": "Город",
|
||||
"country": "Страна",
|
||||
"company": {...},
|
||||
"createdAt": "2024-01-01T00:00:00Z",
|
||||
"updatedAt": "2024-01-01T00:00:00Z"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
#### `GET /branches/:id/users`
|
||||
Получить пользователей филиала.
|
||||
|
||||
**Ответ:**
|
||||
```json
|
||||
{
|
||||
"users": [
|
||||
{
|
||||
"id": "uuid",
|
||||
"email": "user@example.com",
|
||||
"fullName": "Иван Иванов",
|
||||
"role": "manager",
|
||||
"currentBranchId": "uuid"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
#### `POST /branches`
|
||||
Создать новый филиал.
|
||||
|
||||
**Body:**
|
||||
```json
|
||||
{
|
||||
"companyId": "uuid",
|
||||
"name": "Новый филиал",
|
||||
"address": "Адрес (опционально)",
|
||||
"city": "Город (опционально)",
|
||||
"country": "Страна (опционально)"
|
||||
}
|
||||
```
|
||||
|
||||
#### `PATCH /branches/:id`
|
||||
Обновить филиал.
|
||||
|
||||
**Body:**
|
||||
```json
|
||||
{
|
||||
"name": "Обновленное название (опционально)",
|
||||
"address": "Обновленный адрес (опционально)",
|
||||
"city": "Обновленный город (опционально)",
|
||||
"country": "Обновленная страна (опционально)"
|
||||
}
|
||||
```
|
||||
|
||||
#### `DELETE /branches/:id`
|
||||
Удалить филиал.
|
||||
|
||||
#### `POST /branches/:id/users`
|
||||
Привязать пользователя к филиалу.
|
||||
|
||||
**Body:**
|
||||
```json
|
||||
{
|
||||
"userId": "uuid"
|
||||
}
|
||||
```
|
||||
|
||||
**Ответ:**
|
||||
```json
|
||||
{
|
||||
"message": "User assigned to branch successfully"
|
||||
}
|
||||
```
|
||||
|
||||
#### `DELETE /branches/:id/users/:userId`
|
||||
Отвязать пользователя от филиала.
|
||||
|
||||
**Ответ:**
|
||||
```json
|
||||
{
|
||||
"message": "User removed from branch successfully"
|
||||
}
|
||||
```
|
||||
|
||||
#### `POST /branches/:id/select`
|
||||
Установить филиал как текущий для авторизованного пользователя.
|
||||
|
||||
**Ответ:**
|
||||
```json
|
||||
{
|
||||
"message": "Current branch updated successfully",
|
||||
"currentBranchId": "uuid"
|
||||
}
|
||||
```
|
||||
|
||||
**Ошибки:**
|
||||
- `400` - Пользователь не привязан к этому филиалу
|
||||
|
||||
## Использование
|
||||
|
||||
### Создание компании и филиалов
|
||||
|
||||
```typescript
|
||||
// 1. Создать компанию
|
||||
POST /companies
|
||||
{
|
||||
"name": "ООО Рога и Копыта",
|
||||
"description": "Крупная IT компания"
|
||||
}
|
||||
|
||||
// 2. Создать филиалы
|
||||
POST /branches
|
||||
{
|
||||
"companyId": "company-uuid",
|
||||
"name": "Московский офис",
|
||||
"city": "Москва",
|
||||
"country": "Россия"
|
||||
}
|
||||
|
||||
POST /branches
|
||||
{
|
||||
"companyId": "company-uuid",
|
||||
"name": "Питерский офис",
|
||||
"city": "Санкт-Петербург",
|
||||
"country": "Россия"
|
||||
}
|
||||
```
|
||||
|
||||
### Привязка пользователя к филиалам
|
||||
|
||||
```typescript
|
||||
// Привязать пользователя к московскому офису
|
||||
POST /branches/{moscow-branch-id}/users
|
||||
{
|
||||
"userId": "user-uuid"
|
||||
}
|
||||
|
||||
// Привязать пользователя к питерскому офису
|
||||
POST /branches/{spb-branch-id}/users
|
||||
{
|
||||
"userId": "user-uuid"
|
||||
}
|
||||
```
|
||||
|
||||
### Выбор текущего филиала
|
||||
|
||||
```typescript
|
||||
// Установить московский офис как текущий
|
||||
POST /branches/{moscow-branch-id}/select
|
||||
|
||||
// Теперь в объекте пользователя currentBranchId будет указывать на московский офис
|
||||
```
|
||||
|
||||
### Получение филиалов пользователя
|
||||
|
||||
```typescript
|
||||
// Получить все филиалы, к которым привязан пользователь
|
||||
GET /branches/my
|
||||
```
|
||||
|
||||
## Миграция базы данных
|
||||
|
||||
После добавления схем необходимо выполнить миграцию:
|
||||
|
||||
```bash
|
||||
cd server
|
||||
bun run drizzle-kit generate
|
||||
bun run drizzle-kit migrate
|
||||
```
|
||||
|
||||
## Типы TypeScript
|
||||
|
||||
### Company
|
||||
```typescript
|
||||
type Company = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
};
|
||||
```
|
||||
|
||||
### Branch
|
||||
```typescript
|
||||
type Branch = {
|
||||
id: string;
|
||||
companyId: string;
|
||||
name: string;
|
||||
address: string | null;
|
||||
city: string | null;
|
||||
country: string | null;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
};
|
||||
```
|
||||
|
||||
### UserBranch
|
||||
```typescript
|
||||
type UserBranch = {
|
||||
userId: string;
|
||||
branchId: string;
|
||||
createdAt: Date;
|
||||
};
|
||||
```
|
||||
|
||||
## Примечания
|
||||
|
||||
1. **Каскадное удаление**: При удалении компании автоматически удаляются все её филиалы
|
||||
2. **Каскадное удаление связей**: При удалении пользователя или филиала автоматически удаляются все связи в таблице `user_branches`
|
||||
3. **Валидация**: Пользователь может установить текущим только тот филиал, к которому он привязан
|
||||
4. **Many-to-Many**: Пользователь может быть привязан к нескольким филиалам одновременно
|
||||
5. **Текущий филиал**: Поле `currentBranchId` в таблице пользователей позволяет отслеживать, в каком филиале пользователь работает в данный момент
|
||||
|
||||
@@ -4,6 +4,7 @@ import {
|
||||
loginService,
|
||||
registerService,
|
||||
sessionService,
|
||||
userService,
|
||||
} from "../services/auth";
|
||||
import type { LoginData, RegisterData } from "../services/auth/types";
|
||||
|
||||
@@ -43,7 +44,17 @@ export const authController = new Elysia({ prefix: "/auth" })
|
||||
.use(authMiddleware)
|
||||
// GET /me
|
||||
.get("/me", async ({ currentUser }) => {
|
||||
return { user: currentUser };
|
||||
// Получить полную информацию о пользователе с филиалом и компанией
|
||||
const userWithRelations = await userService.findByIdWithRelations(
|
||||
currentUser.id
|
||||
);
|
||||
|
||||
if (!userWithRelations) {
|
||||
return { user: currentUser };
|
||||
}
|
||||
|
||||
const userResponse = userService.sanitizeWithRelations(userWithRelations);
|
||||
return { user: userResponse };
|
||||
})
|
||||
// POST /logout
|
||||
.post("/logout", async ({ authSession }) => {
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
import { Elysia, t } from "elysia";
|
||||
import { authMiddleware } from "../middlewares/auth";
|
||||
import { branchService } from "../services/branch";
|
||||
import { companyService } from "../services/company";
|
||||
|
||||
export const branchController = new Elysia({ prefix: "/branches" })
|
||||
// Все роуты требуют авторизации
|
||||
.use(authMiddleware)
|
||||
// GET /branches - получить филиалы пользователя
|
||||
.get("/my", async ({ currentUser }) => {
|
||||
const branches = await branchService.findByUserId(currentUser.id);
|
||||
return { branches };
|
||||
})
|
||||
// GET /branches/:id - получить филиал по ID
|
||||
.get("/:id", async ({ params, status }) => {
|
||||
const { id } = params;
|
||||
|
||||
const branch = await branchService.findById(id);
|
||||
|
||||
if (!branch) {
|
||||
return status(404, "Branch not found");
|
||||
}
|
||||
|
||||
return { branch };
|
||||
})
|
||||
// GET /branches/:id/users - получить пользователей филиала
|
||||
.get("/:id/users", async ({ params, status }) => {
|
||||
const { id } = params;
|
||||
|
||||
// Проверить существование филиала
|
||||
const branch = await branchService.findById(id);
|
||||
|
||||
if (!branch) {
|
||||
return status(404, "Branch not found");
|
||||
}
|
||||
|
||||
const users = await branchService.getUsersByBranchId(id);
|
||||
|
||||
return { users };
|
||||
})
|
||||
// POST /branches - создать филиал
|
||||
.post(
|
||||
"/",
|
||||
async ({ body, status }) => {
|
||||
const { companyId, name, address, city, country } = body as {
|
||||
companyId: string;
|
||||
name: string;
|
||||
address?: string;
|
||||
city?: string;
|
||||
country?: string;
|
||||
};
|
||||
|
||||
// Проверить существование компании
|
||||
const company = await companyService.findById(companyId);
|
||||
|
||||
if (!company) {
|
||||
return status(404, "Company not found");
|
||||
}
|
||||
|
||||
const branch = await branchService.create({
|
||||
companyId,
|
||||
name,
|
||||
address,
|
||||
city,
|
||||
country,
|
||||
});
|
||||
|
||||
return { branch };
|
||||
},
|
||||
{
|
||||
body: t.Object({
|
||||
companyId: t.String({ format: "uuid" }),
|
||||
name: t.String({ minLength: 1, maxLength: 255 }),
|
||||
address: t.Optional(t.String({ maxLength: 500 })),
|
||||
city: t.Optional(t.String({ maxLength: 100 })),
|
||||
country: t.Optional(t.String({ maxLength: 100 })),
|
||||
}),
|
||||
}
|
||||
)
|
||||
// PATCH /branches/:id - обновить филиал
|
||||
.patch(
|
||||
"/:id",
|
||||
async ({ params, body, status }) => {
|
||||
const { id } = params;
|
||||
const { name, address, city, country } = body as {
|
||||
name?: string;
|
||||
address?: string;
|
||||
city?: string;
|
||||
country?: string;
|
||||
};
|
||||
|
||||
// Проверить существование филиала
|
||||
const branch = await branchService.findById(id);
|
||||
|
||||
if (!branch) {
|
||||
return status(404, "Branch not found");
|
||||
}
|
||||
|
||||
const updatedBranch = await branchService.update(id, {
|
||||
name,
|
||||
address,
|
||||
city,
|
||||
country,
|
||||
});
|
||||
|
||||
return { branch: updatedBranch };
|
||||
},
|
||||
{
|
||||
body: t.Object({
|
||||
name: t.Optional(t.String({ minLength: 1, maxLength: 255 })),
|
||||
address: t.Optional(t.String({ maxLength: 500 })),
|
||||
city: t.Optional(t.String({ maxLength: 100 })),
|
||||
country: t.Optional(t.String({ maxLength: 100 })),
|
||||
}),
|
||||
}
|
||||
)
|
||||
// DELETE /branches/:id - удалить филиал
|
||||
.delete("/:id", async ({ params, status }) => {
|
||||
const { id } = params;
|
||||
|
||||
// Проверить существование филиала
|
||||
const branch = await branchService.findById(id);
|
||||
|
||||
if (!branch) {
|
||||
return status(404, "Branch not found");
|
||||
}
|
||||
|
||||
await branchService.delete(id);
|
||||
|
||||
return { message: "Branch deleted successfully" };
|
||||
})
|
||||
// POST /branches/:id/users - привязать пользователя к филиалу
|
||||
.post(
|
||||
"/:id/users",
|
||||
async ({ params, body, status }) => {
|
||||
const { id } = params;
|
||||
const { userId } = body as { userId: string };
|
||||
|
||||
// Проверить существование филиала
|
||||
const branch = await branchService.findById(id);
|
||||
|
||||
if (!branch) {
|
||||
return status(404, "Branch not found");
|
||||
}
|
||||
|
||||
try {
|
||||
await branchService.assignUserToBranch(userId, id);
|
||||
return { message: "User assigned to branch successfully" };
|
||||
} catch (error) {
|
||||
return status(409, "User is already assigned to this branch");
|
||||
}
|
||||
},
|
||||
{
|
||||
body: t.Object({
|
||||
userId: t.String({ format: "uuid" }),
|
||||
}),
|
||||
}
|
||||
)
|
||||
// DELETE /branches/:id/users/:userId - отвязать пользователя от филиала
|
||||
.delete("/:id/users/:userId", async ({ params, status }) => {
|
||||
const { id, userId } = params;
|
||||
|
||||
// Проверить существование филиала
|
||||
const branch = await branchService.findById(id);
|
||||
|
||||
if (!branch) {
|
||||
return status(404, "Branch not found");
|
||||
}
|
||||
|
||||
await branchService.removeUserFromBranch(userId, id);
|
||||
|
||||
return { message: "User removed from branch successfully" };
|
||||
})
|
||||
// POST /branches/:id/select - установить филиал как текущий для пользователя
|
||||
.post("/:id/select", async ({ params, currentUser, status }) => {
|
||||
const { id } = params;
|
||||
|
||||
try {
|
||||
const updatedUser = await branchService.setUserCurrentBranch(
|
||||
currentUser.id,
|
||||
id
|
||||
);
|
||||
|
||||
return {
|
||||
message: "Current branch updated successfully",
|
||||
currentBranchId: updatedUser.currentBranchId,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
return status(400, error.message);
|
||||
}
|
||||
return status(500, "Failed to update current branch");
|
||||
}
|
||||
});
|
||||
@@ -0,0 +1,93 @@
|
||||
import { Elysia, t } from "elysia";
|
||||
import { authMiddleware } from "../middlewares/auth";
|
||||
import { companyService } from "../services/company";
|
||||
|
||||
export const companyController = new Elysia({ prefix: "/companies" })
|
||||
// Все роуты требуют авторизации
|
||||
.use(authMiddleware)
|
||||
// GET /companies - получить все компании
|
||||
.get("/", async () => {
|
||||
const companies = await companyService.findAll();
|
||||
return { companies };
|
||||
})
|
||||
// GET /companies/:id - получить компанию по ID
|
||||
.get("/:id", async ({ params, status }) => {
|
||||
const { id } = params;
|
||||
|
||||
const company = await companyService.findById(id);
|
||||
|
||||
if (!company) {
|
||||
return status(404, "Company not found");
|
||||
}
|
||||
|
||||
return { company };
|
||||
})
|
||||
// POST /companies - создать компанию
|
||||
.post(
|
||||
"/",
|
||||
async ({ body }) => {
|
||||
const { name, description } = body as {
|
||||
name: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
const company = await companyService.create({
|
||||
name,
|
||||
description,
|
||||
});
|
||||
|
||||
return { company };
|
||||
},
|
||||
{
|
||||
body: t.Object({
|
||||
name: t.String({ minLength: 1, maxLength: 255 }),
|
||||
description: t.Optional(t.String({ maxLength: 1000 })),
|
||||
}),
|
||||
}
|
||||
)
|
||||
// PATCH /companies/:id - обновить компанию
|
||||
.patch(
|
||||
"/:id",
|
||||
async ({ params, body, status }) => {
|
||||
const { id } = params;
|
||||
const { name, description } = body as {
|
||||
name?: string;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
// Проверить существование компании
|
||||
const company = await companyService.findById(id);
|
||||
|
||||
if (!company) {
|
||||
return status(404, "Company not found");
|
||||
}
|
||||
|
||||
const updatedCompany = await companyService.update(id, {
|
||||
name,
|
||||
description,
|
||||
});
|
||||
|
||||
return { company: updatedCompany };
|
||||
},
|
||||
{
|
||||
body: t.Object({
|
||||
name: t.Optional(t.String({ minLength: 1, maxLength: 255 })),
|
||||
description: t.Optional(t.String({ maxLength: 1000 })),
|
||||
}),
|
||||
}
|
||||
)
|
||||
// DELETE /companies/:id - удалить компанию
|
||||
.delete("/:id", async ({ params, status }) => {
|
||||
const { id } = params;
|
||||
|
||||
// Проверить существование компании
|
||||
const company = await companyService.findById(id);
|
||||
|
||||
if (!company) {
|
||||
return status(404, "Company not found");
|
||||
}
|
||||
|
||||
await companyService.delete(id);
|
||||
|
||||
return { message: "Company deleted successfully" };
|
||||
});
|
||||
@@ -0,0 +1,208 @@
|
||||
import { Elysia, t } from "elysia";
|
||||
import { authMiddleware } from "../middlewares/auth";
|
||||
import { eq } from "drizzle-orm";
|
||||
import db from "../db";
|
||||
import { apps } from "../db/schema/apps";
|
||||
import { serverSessionService } from "../services/serverSession";
|
||||
|
||||
export const sessionController = new Elysia({ prefix: "/sessions" })
|
||||
// Все роуты требуют авторизации
|
||||
.use(authMiddleware)
|
||||
// GET /sessions - получить список сессий пользователя
|
||||
.get("/", async ({ currentUser, query }) => {
|
||||
const { status, mode } = query as {
|
||||
status?: "starting" | "started" | "ending" | "ended";
|
||||
mode?: "stream" | "local";
|
||||
};
|
||||
|
||||
const sessions = await serverSessionService.findByUserId(currentUser.id, {
|
||||
status,
|
||||
mode,
|
||||
});
|
||||
|
||||
return { sessions };
|
||||
})
|
||||
// GET /sessions/:id - получить информацию о конкретной сессии
|
||||
.get("/:id", async ({ params, currentUser, status }) => {
|
||||
const { id } = params;
|
||||
|
||||
const session = await serverSessionService.findByIdForUser(
|
||||
id,
|
||||
currentUser.id
|
||||
);
|
||||
|
||||
if (!session) {
|
||||
return status(404, "Session not found");
|
||||
}
|
||||
|
||||
return { session };
|
||||
})
|
||||
// POST /sessions - создать новую сессию
|
||||
.post(
|
||||
"/",
|
||||
async ({ body, currentUser, status }) => {
|
||||
const { appId, mode, serverId } = body as {
|
||||
appId: string;
|
||||
mode: "stream" | "local";
|
||||
serverId?: string;
|
||||
};
|
||||
|
||||
// Проверить, что приложение существует
|
||||
const app = await db.query.apps.findFirst({
|
||||
where: eq(apps.id, appId),
|
||||
});
|
||||
|
||||
if (!app) {
|
||||
return status(404, "App not found");
|
||||
}
|
||||
|
||||
// Проверить, что пользователь не имеет активных сессий этого приложения
|
||||
const hasActive = await serverSessionService.hasActiveSession(
|
||||
currentUser.id,
|
||||
appId
|
||||
);
|
||||
|
||||
if (hasActive) {
|
||||
return status(409, "User already has an active session for this app");
|
||||
}
|
||||
|
||||
// Создать сессию
|
||||
try {
|
||||
const newSession = await serverSessionService.create({
|
||||
appId,
|
||||
userId: currentUser.id,
|
||||
mode,
|
||||
serverId,
|
||||
});
|
||||
|
||||
return { session: newSession };
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
return status(503, error.message);
|
||||
}
|
||||
return status(500, "Failed to create session");
|
||||
}
|
||||
},
|
||||
{
|
||||
body: t.Object({
|
||||
appId: t.String({ format: "uuid" }),
|
||||
mode: t.Union([t.Literal("stream"), t.Literal("local")]),
|
||||
serverId: t.Optional(t.String({ format: "uuid" })),
|
||||
}),
|
||||
}
|
||||
)
|
||||
// PATCH /sessions/:id - обновить статус сессии
|
||||
.patch(
|
||||
"/:id",
|
||||
async ({ params, body, currentUser, status }) => {
|
||||
const { id } = params;
|
||||
const {
|
||||
status: sessionStatus,
|
||||
appPid,
|
||||
cirrusPid,
|
||||
endAt,
|
||||
} = body as {
|
||||
status?: "starting" | "started" | "ending" | "ended";
|
||||
appPid?: number;
|
||||
cirrusPid?: number;
|
||||
endAt?: string;
|
||||
};
|
||||
|
||||
// Проверить, что сессия существует и принадлежит пользователю
|
||||
const session = await serverSessionService.findByIdForUser(
|
||||
id,
|
||||
currentUser.id
|
||||
);
|
||||
|
||||
if (!session) {
|
||||
return status(404, "Session not found");
|
||||
}
|
||||
|
||||
// Обновить сессию
|
||||
const updatedSession = await serverSessionService.update(id, {
|
||||
status: sessionStatus,
|
||||
appPid,
|
||||
cirrusPid,
|
||||
endAt: endAt ? new Date(endAt) : undefined,
|
||||
});
|
||||
|
||||
return { session: updatedSession };
|
||||
},
|
||||
{
|
||||
body: t.Object({
|
||||
status: t.Optional(
|
||||
t.Union([
|
||||
t.Literal("starting"),
|
||||
t.Literal("started"),
|
||||
t.Literal("ending"),
|
||||
t.Literal("ended"),
|
||||
])
|
||||
),
|
||||
appPid: t.Optional(t.Number()),
|
||||
cirrusPid: t.Optional(t.Number()),
|
||||
endAt: t.Optional(t.String({ format: "date-time" })),
|
||||
}),
|
||||
}
|
||||
)
|
||||
// DELETE /sessions/:id - удалить (завершить) сессию
|
||||
.delete("/:id", async ({ params, currentUser, status }) => {
|
||||
const { id } = params;
|
||||
|
||||
// Проверить, что сессия существует и принадлежит пользователю
|
||||
const session = await serverSessionService.findByIdForUser(
|
||||
id,
|
||||
currentUser.id
|
||||
);
|
||||
|
||||
if (!session) {
|
||||
return status(404, "Session not found");
|
||||
}
|
||||
|
||||
// Если сессия активна, изменить статус на "ending"
|
||||
if (session.status === "started" || session.status === "starting") {
|
||||
await serverSessionService.end(id);
|
||||
return { message: "Session is ending" };
|
||||
}
|
||||
|
||||
// Если сессия уже завершена или завершается
|
||||
return { message: "Session already ended or ending" };
|
||||
})
|
||||
// POST /sessions/:id/extend - продлить сессию
|
||||
.post(
|
||||
"/:id/extend",
|
||||
async ({ params, body, currentUser, status }) => {
|
||||
const { id } = params;
|
||||
const { minutes } = body as { minutes: number };
|
||||
|
||||
// Проверить, что сессия существует и принадлежит пользователю
|
||||
const session = await serverSessionService.findByIdForUser(
|
||||
id,
|
||||
currentUser.id
|
||||
);
|
||||
|
||||
if (!session) {
|
||||
return status(404, "Session not found");
|
||||
}
|
||||
|
||||
// Проверить, что сессия активна
|
||||
if (session.status !== "started") {
|
||||
return status(400, "Can only extend active sessions");
|
||||
}
|
||||
|
||||
// Продлить сессию
|
||||
try {
|
||||
const updatedSession = await serverSessionService.extend(id, minutes);
|
||||
return { session: updatedSession };
|
||||
} catch (error) {
|
||||
if (error instanceof Error) {
|
||||
return status(400, error.message);
|
||||
}
|
||||
return status(500, "Failed to extend session");
|
||||
}
|
||||
},
|
||||
{
|
||||
body: t.Object({
|
||||
minutes: t.Number({ minimum: 1, maximum: 120 }),
|
||||
}),
|
||||
}
|
||||
);
|
||||
@@ -0,0 +1,24 @@
|
||||
import { pgTable, uuid, varchar, timestamp } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
|
||||
import { companies } from "./companies";
|
||||
|
||||
export const branches = pgTable("branches", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
companyId: uuid("company_id")
|
||||
.notNull()
|
||||
.references(() => companies.id, { onDelete: "cascade" }),
|
||||
name: varchar("name", { length: 255 }).notNull(),
|
||||
address: varchar("address", { length: 500 }),
|
||||
city: varchar("city", { length: 100 }),
|
||||
country: varchar("country", { length: 100 }),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||||
});
|
||||
|
||||
// Zod schemas for validation
|
||||
export const insertBranchSchema = createInsertSchema(branches);
|
||||
export const selectBranchSchema = createSelectSchema(branches);
|
||||
|
||||
// Type exports
|
||||
export type Branch = typeof branches.$inferSelect;
|
||||
export type NewBranch = typeof branches.$inferInsert;
|
||||
@@ -0,0 +1,18 @@
|
||||
import { pgTable, uuid, varchar, timestamp } from "drizzle-orm/pg-core";
|
||||
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
|
||||
|
||||
export const companies = pgTable("companies", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
name: varchar("name", { length: 255 }).notNull(),
|
||||
description: varchar("description", { length: 1000 }),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||||
});
|
||||
|
||||
// Zod schemas for validation
|
||||
export const insertCompanySchema = createInsertSchema(companies);
|
||||
export const selectCompanySchema = createSelectSchema(companies);
|
||||
|
||||
// Type exports
|
||||
export type Company = typeof companies.$inferSelect;
|
||||
export type NewCompany = typeof companies.$inferInsert;
|
||||
@@ -6,4 +6,3 @@ export const roleNameEnum = pgEnum("role_name", [
|
||||
"director",
|
||||
"manager",
|
||||
]);
|
||||
|
||||
|
||||
@@ -1,9 +1,29 @@
|
||||
// Export all schemas
|
||||
export * from "./enums";
|
||||
export * from "./streamServers";
|
||||
export * from "./localServers";
|
||||
export * from "./servers";
|
||||
export * from "./apps";
|
||||
export * from "./companies";
|
||||
export * from "./branches";
|
||||
export * from "./users";
|
||||
export * from "./userBranches";
|
||||
export * from "./serverSessions";
|
||||
export * from "./authSessions";
|
||||
export * from "./protectedRoutes";
|
||||
|
||||
// Relations (defined here to avoid circular dependencies)
|
||||
import { relations } from "drizzle-orm";
|
||||
import { companies } from "./companies";
|
||||
import { branches } from "./branches";
|
||||
import { userBranches } from "./userBranches";
|
||||
|
||||
export const companiesRelations = relations(companies, ({ many }) => ({
|
||||
branches: many(branches),
|
||||
}));
|
||||
|
||||
export const branchesRelations = relations(branches, ({ one, many }) => ({
|
||||
company: one(companies, {
|
||||
fields: [branches.companyId],
|
||||
references: [companies.id],
|
||||
}),
|
||||
userBranches: many(userBranches),
|
||||
}));
|
||||
@@ -1,27 +0,0 @@
|
||||
import { pgTable, uuid, varchar } from "drizzle-orm/pg-core";
|
||||
import { relations } from "drizzle-orm";
|
||||
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
|
||||
import { serverSessions } from "./serverSessions";
|
||||
|
||||
export const localServers = pgTable("local_servers", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
localIp: varchar("local_ip", { length: 45 }).notNull(), // IPv6 can be up to 45 chars
|
||||
hostname: varchar("hostname", { length: 255 }).notNull(),
|
||||
location: varchar("location", { length: 10 }).notNull(), // ru1, uae1
|
||||
type: varchar("type", { length: 10 }).notNull(), // demo, prod
|
||||
});
|
||||
|
||||
// Zod schemas for validation
|
||||
export const insertLocalServerSchema = createInsertSchema(localServers);
|
||||
export const selectLocalServerSchema = createSelectSchema(localServers);
|
||||
|
||||
// Relations
|
||||
export const localServersRelations = relations(localServers, ({ many }) => ({
|
||||
serverSessions: many(serverSessions, {
|
||||
relationName: "session_local_server",
|
||||
}),
|
||||
}));
|
||||
|
||||
// Type exports
|
||||
export type LocalServer = typeof localServers.$inferSelect;
|
||||
export type NewLocalServer = typeof localServers.$inferInsert;
|
||||
@@ -4,14 +4,8 @@ import { roleNameEnum } from "./enums";
|
||||
|
||||
export const protectedRoutes = pgTable("protected_routes", {
|
||||
path: varchar("path", { length: 255 }).primaryKey(), // /auth/register-user
|
||||
methods: varchar("methods", { length: 50 })
|
||||
.array()
|
||||
.notNull()
|
||||
.default([]), // Массив: ["GET", "POST", "PUT", "DELETE", "PATCH"]
|
||||
roles: roleNameEnum("roles")
|
||||
.array()
|
||||
.notNull()
|
||||
.default([]), // Массив: ["admin", "director", "manager"]
|
||||
methods: varchar("methods", { length: 50 }).array().notNull().default([]), // Массив: ["GET", "POST", "PUT", "DELETE", "PATCH"]
|
||||
roles: roleNameEnum("roles").array().notNull().default([]), // Массив: ["admin", "director", "manager"]
|
||||
description: text("description"), // Описание маршрута
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.defaultNow()
|
||||
@@ -28,4 +22,3 @@ export const selectProtectedRouteSchema = createSelectSchema(protectedRoutes);
|
||||
// Type exports
|
||||
export type ProtectedRoute = typeof protectedRoutes.$inferSelect;
|
||||
export type NewProtectedRoute = typeof protectedRoutes.$inferInsert;
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { pgTable, uuid, integer, timestamp, pgEnum } from "drizzle-orm/pg-core";
|
||||
import { streamServers } from "./streamServers";
|
||||
import { localServers } from "./localServers";
|
||||
import { servers } from "./servers";
|
||||
import { apps } from "./apps";
|
||||
import { users } from "./users";
|
||||
import { relations } from "drizzle-orm";
|
||||
@@ -8,10 +7,18 @@ import { createInsertSchema, createSelectSchema } from "drizzle-zod";
|
||||
|
||||
// Enums
|
||||
export const sessionModeEnum = pgEnum("session_mode", ["stream", "local"]);
|
||||
export const sessionStatusEnum = pgEnum("session_status", [
|
||||
"starting",
|
||||
"started",
|
||||
"ending",
|
||||
"ended",
|
||||
]);
|
||||
|
||||
export const serverSessions = pgTable("server_sessions", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
serverId: uuid("server_id").notNull(),
|
||||
serverId: uuid("server_id")
|
||||
.notNull()
|
||||
.references(() => servers.id),
|
||||
appId: uuid("app_id")
|
||||
.notNull()
|
||||
.references(() => apps.id),
|
||||
@@ -23,6 +30,7 @@ export const serverSessions = pgTable("server_sessions", {
|
||||
appPid: integer("app_pid"),
|
||||
cirrusPid: integer("cirrus_pid"),
|
||||
mode: sessionModeEnum("mode").notNull(), // stream, local
|
||||
status: sessionStatusEnum("status").notNull(), // starting, started, ending, ended
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
||||
});
|
||||
@@ -37,17 +45,9 @@ export const serverSessionsRelations = relations(serverSessions, ({ one }) => ({
|
||||
fields: [serverSessions.userId],
|
||||
references: [users.id],
|
||||
}),
|
||||
// Полиморфная реляция для serverId
|
||||
// В зависимости от mode, serverId может ссылаться на stream_servers или local_servers
|
||||
streamServer: one(streamServers, {
|
||||
server: one(servers, {
|
||||
fields: [serverSessions.serverId],
|
||||
references: [streamServers.id],
|
||||
relationName: "session_stream_server",
|
||||
}),
|
||||
localServer: one(localServers, {
|
||||
fields: [serverSessions.serverId],
|
||||
references: [localServers.id],
|
||||
relationName: "session_local_server",
|
||||
references: [servers.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
import { pgTable, uuid, varchar, pgEnum } from "drizzle-orm/pg-core";
|
||||
import { relations } from "drizzle-orm";
|
||||
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
|
||||
import { serverSessions } from "./serverSessions";
|
||||
|
||||
// Enums
|
||||
export const serverLocationEnum = pgEnum("server_location", ["ru1", "uae1"]);
|
||||
export const serverTypeEnum = pgEnum("server_type", ["stream", "local"]);
|
||||
export const serverTierEnum = pgEnum("server_tier", ["demo", "prod"]);
|
||||
|
||||
export const servers = pgTable("servers", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
localIp: varchar("local_ip", { length: 45 }).notNull(), // IPv6 can be up to 45 chars
|
||||
hostname: varchar("hostname").notNull(),
|
||||
type: serverTypeEnum("type").notNull(), // stream, local
|
||||
location: serverLocationEnum("location"), // ru1, uae1 (только для stream)
|
||||
tier: serverTierEnum("tier"), // demo, prod (только для stream)
|
||||
});
|
||||
|
||||
// Zod schemas for validation
|
||||
export const insertServerSchema = createInsertSchema(servers);
|
||||
export const selectServerSchema = createSelectSchema(servers);
|
||||
|
||||
// Relations
|
||||
export const serversRelations = relations(servers, ({ many }) => ({
|
||||
serverSessions: many(serverSessions),
|
||||
}));
|
||||
|
||||
// Type exports
|
||||
export type Server = typeof servers.$inferSelect;
|
||||
export type NewServer = typeof servers.$inferInsert;
|
||||
@@ -1,31 +0,0 @@
|
||||
import { pgTable, uuid, varchar, pgEnum } from "drizzle-orm/pg-core";
|
||||
import { relations } from "drizzle-orm";
|
||||
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
|
||||
import { serverSessions } from "./serverSessions";
|
||||
|
||||
// Enums
|
||||
export const serverLocationEnum = pgEnum("server_location", ["ru1", "uae1"]);
|
||||
export const serverTypeEnum = pgEnum("server_type", ["demo", "prod"]);
|
||||
|
||||
export const streamServers = pgTable("stream_servers", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
localIp: varchar("local_ip", { length: 45 }).notNull(), // IPv6 can be up to 45 chars
|
||||
hostname: varchar("hostname", { length: 255 }).notNull(),
|
||||
location: serverLocationEnum("location").notNull(),
|
||||
type: serverTypeEnum("type").notNull(),
|
||||
});
|
||||
|
||||
// Zod schemas for validation
|
||||
export const insertStreamServerSchema = createInsertSchema(streamServers);
|
||||
export const selectStreamServerSchema = createSelectSchema(streamServers);
|
||||
|
||||
// Relations
|
||||
export const streamServersRelations = relations(streamServers, ({ many }) => ({
|
||||
serverSessions: many(serverSessions, {
|
||||
relationName: "session_stream_server",
|
||||
}),
|
||||
}));
|
||||
|
||||
// Type exports
|
||||
export type StreamServer = typeof streamServers.$inferSelect;
|
||||
export type NewStreamServer = typeof streamServers.$inferInsert;
|
||||
@@ -0,0 +1,42 @@
|
||||
import { pgTable, uuid, timestamp, primaryKey } from "drizzle-orm/pg-core";
|
||||
import { relations } from "drizzle-orm";
|
||||
import { createInsertSchema, createSelectSchema } from "drizzle-zod";
|
||||
import { users } from "./users";
|
||||
import { branches } from "./branches";
|
||||
|
||||
// Junction table для связи many-to-many между пользователями и филиалами
|
||||
export const userBranches = pgTable(
|
||||
"user_branches",
|
||||
{
|
||||
userId: uuid("user_id")
|
||||
.notNull()
|
||||
.references(() => users.id, { onDelete: "cascade" }),
|
||||
branchId: uuid("branch_id")
|
||||
.notNull()
|
||||
.references(() => branches.id, { onDelete: "cascade" }),
|
||||
createdAt: timestamp("created_at").defaultNow().notNull(),
|
||||
},
|
||||
(table) => ({
|
||||
pk: primaryKey({ columns: [table.userId, table.branchId] }),
|
||||
})
|
||||
);
|
||||
|
||||
// Relations
|
||||
export const userBranchesRelations = relations(userBranches, ({ one }) => ({
|
||||
user: one(users, {
|
||||
fields: [userBranches.userId],
|
||||
references: [users.id],
|
||||
}),
|
||||
branch: one(branches, {
|
||||
fields: [userBranches.branchId],
|
||||
references: [branches.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
// Zod schemas for validation
|
||||
export const insertUserBranchSchema = createInsertSchema(userBranches);
|
||||
export const selectUserBranchSchema = createSelectSchema(userBranches);
|
||||
|
||||
// Type exports
|
||||
export type UserBranch = typeof userBranches.$inferSelect;
|
||||
export type NewUserBranch = typeof userBranches.$inferInsert;
|
||||
@@ -4,6 +4,8 @@ import { createInsertSchema, createSelectSchema } from "drizzle-zod";
|
||||
import { roleNameEnum } from "./enums";
|
||||
import { serverSessions } from "./serverSessions";
|
||||
import { authSessions } from "./authSessions";
|
||||
import { userBranches } from "./userBranches";
|
||||
import { branches } from "./branches";
|
||||
|
||||
export const users = pgTable("users", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
@@ -11,6 +13,7 @@ export const users = pgTable("users", {
|
||||
password: varchar("password", { length: 255 }).notNull(), // scrypt hash
|
||||
fullName: varchar("full_name", { length: 255 }).notNull(), // ФИО
|
||||
role: roleNameEnum("role").notNull().default("manager"),
|
||||
currentBranchId: uuid("current_branch_id").references(() => branches.id), // Текущий выбранный филиал
|
||||
createdAt: timestamp("created_at", { withTimezone: true })
|
||||
.defaultNow()
|
||||
.notNull(),
|
||||
@@ -24,9 +27,14 @@ export const insertUserSchema = createInsertSchema(users);
|
||||
export const selectUserSchema = createSelectSchema(users);
|
||||
|
||||
// Relations
|
||||
export const usersRelations = relations(users, ({ many }) => ({
|
||||
export const usersRelations = relations(users, ({ one, many }) => ({
|
||||
serverSessions: many(serverSessions),
|
||||
authSessions: many(authSessions),
|
||||
userBranches: many(userBranches),
|
||||
currentBranch: one(branches, {
|
||||
fields: [users.currentBranchId],
|
||||
references: [branches.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
// Type exports
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { Elysia } from "elysia";
|
||||
import cors from "@elysiajs/cors";
|
||||
import { authController } from "./controllers/auth";
|
||||
import { sessionController } from "./controllers/session";
|
||||
import { companyController } from "./controllers/company";
|
||||
import { branchController } from "./controllers/branch";
|
||||
|
||||
const app = new Elysia();
|
||||
|
||||
@@ -12,6 +15,9 @@ app.use(
|
||||
);
|
||||
|
||||
app.use(authController);
|
||||
app.use(sessionController);
|
||||
app.use(companyController);
|
||||
app.use(branchController);
|
||||
|
||||
app.listen(3000);
|
||||
|
||||
|
||||
@@ -32,7 +32,10 @@ export const loginService = {
|
||||
}
|
||||
|
||||
// Создать новую сессию
|
||||
const { sessionId, accessToken } = await sessionService.create(user.id, metadata);
|
||||
const { sessionId, accessToken } = await sessionService.create(
|
||||
user.id,
|
||||
metadata
|
||||
);
|
||||
|
||||
// Вычислить дату истечения токена
|
||||
const expiresAt = new Date();
|
||||
@@ -48,4 +51,3 @@ export const loginService = {
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -30,9 +30,7 @@ export const registerService = {
|
||||
// Определить роль для нового пользователя
|
||||
// Только администраторы могут указывать кастомную роль
|
||||
const role =
|
||||
callerRole === "admin" && data.role
|
||||
? data.role
|
||||
: DEFAULT_ROLE_NAME;
|
||||
callerRole === "admin" && data.role ? data.role : DEFAULT_ROLE_NAME;
|
||||
|
||||
// Создать пользователя
|
||||
const newUser = await userService.create({
|
||||
@@ -45,4 +43,3 @@ export const registerService = {
|
||||
return userService.sanitize(newUser);
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -2,12 +2,28 @@
|
||||
|
||||
export type RoleName = "admin" | "director" | "manager";
|
||||
|
||||
export type BranchResponse = {
|
||||
id: string;
|
||||
name: string;
|
||||
address: string | null;
|
||||
city: string | null;
|
||||
country: string | null;
|
||||
};
|
||||
|
||||
export type CompanyResponse = {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string | null;
|
||||
};
|
||||
|
||||
export type UserResponse = {
|
||||
id: string;
|
||||
email: string;
|
||||
fullName: string;
|
||||
role: RoleName;
|
||||
createdAt: Date;
|
||||
currentBranch?: BranchResponse;
|
||||
currentCompany?: CompanyResponse;
|
||||
};
|
||||
|
||||
export type LoginResult = {
|
||||
@@ -35,4 +51,3 @@ export type SessionMetadata = {
|
||||
userAgent: string | null;
|
||||
ipAddress: string | null;
|
||||
};
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ export const userService = {
|
||||
/**
|
||||
* Убрать пароль из объекта пользователя
|
||||
*/
|
||||
sanitize(user: User): UserResponse {
|
||||
sanitize(user: User, includeRelations: boolean = false): UserResponse {
|
||||
return {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
@@ -80,4 +80,55 @@ export const userService = {
|
||||
createdAt: user.createdAt,
|
||||
};
|
||||
},
|
||||
|
||||
/**
|
||||
* Получить пользователя по ID с полной информацией (филиал и компания)
|
||||
*/
|
||||
async findByIdWithRelations(userId: string): Promise<User | null> {
|
||||
const user = await db.query.users.findFirst({
|
||||
where: eq(users.id, userId),
|
||||
with: {
|
||||
currentBranch: {
|
||||
with: {
|
||||
company: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return user || null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Убрать пароль из объекта пользователя и добавить информацию о филиале и компании
|
||||
*/
|
||||
sanitizeWithRelations(user: any): UserResponse {
|
||||
const response: UserResponse = {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
fullName: user.fullName,
|
||||
role: user.role,
|
||||
createdAt: user.createdAt,
|
||||
};
|
||||
|
||||
if (user.currentBranch) {
|
||||
response.currentBranch = {
|
||||
id: user.currentBranch.id,
|
||||
name: user.currentBranch.name,
|
||||
address: user.currentBranch.address,
|
||||
city: user.currentBranch.city,
|
||||
country: user.currentBranch.country,
|
||||
};
|
||||
|
||||
if (user.currentBranch.company) {
|
||||
response.currentCompany = {
|
||||
id: user.currentBranch.company.id,
|
||||
name: user.currentBranch.company.name,
|
||||
description: user.currentBranch.company.description,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return response;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,208 @@
|
||||
import { eq, and } from "drizzle-orm";
|
||||
import db from "../../db";
|
||||
import { branches, userBranches, users } from "../../db/schema";
|
||||
|
||||
export interface CreateBranchParams {
|
||||
companyId: string;
|
||||
name: string;
|
||||
address?: string;
|
||||
city?: string;
|
||||
country?: string;
|
||||
}
|
||||
|
||||
export interface UpdateBranchParams {
|
||||
name?: string;
|
||||
address?: string;
|
||||
city?: string;
|
||||
country?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Сервис для работы с филиалами
|
||||
*/
|
||||
export const branchService = {
|
||||
/**
|
||||
* Создать филиал
|
||||
*/
|
||||
async create(params: CreateBranchParams) {
|
||||
const [branch] = await db
|
||||
.insert(branches)
|
||||
.values({
|
||||
companyId: params.companyId,
|
||||
name: params.name,
|
||||
address: params.address,
|
||||
city: params.city,
|
||||
country: params.country,
|
||||
})
|
||||
.returning();
|
||||
|
||||
return branch;
|
||||
},
|
||||
|
||||
/**
|
||||
* Получить все филиалы компании
|
||||
*/
|
||||
async findByCompanyId(companyId: string) {
|
||||
const companyBranches = await db.query.branches.findMany({
|
||||
where: eq(branches.companyId, companyId),
|
||||
with: {
|
||||
company: true,
|
||||
},
|
||||
orderBy: (branches, { asc }) => [asc(branches.name)],
|
||||
});
|
||||
|
||||
return companyBranches;
|
||||
},
|
||||
|
||||
/**
|
||||
* Найти филиал по ID
|
||||
*/
|
||||
async findById(branchId: string) {
|
||||
const branch = await db.query.branches.findFirst({
|
||||
where: eq(branches.id, branchId),
|
||||
with: {
|
||||
company: true,
|
||||
},
|
||||
});
|
||||
|
||||
return branch || null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Получить филиалы пользователя
|
||||
*/
|
||||
async findByUserId(userId: string) {
|
||||
const userBranchRecords = await db.query.userBranches.findMany({
|
||||
where: eq(userBranches.userId, userId),
|
||||
with: {
|
||||
branch: {
|
||||
with: {
|
||||
company: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return userBranchRecords.map((record) => record.branch);
|
||||
},
|
||||
|
||||
/**
|
||||
* Обновить филиал
|
||||
*/
|
||||
async update(branchId: string, params: UpdateBranchParams) {
|
||||
const updateData: any = {
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
if (params.name) {
|
||||
updateData.name = params.name;
|
||||
}
|
||||
|
||||
if (params.address !== undefined) {
|
||||
updateData.address = params.address;
|
||||
}
|
||||
|
||||
if (params.city !== undefined) {
|
||||
updateData.city = params.city;
|
||||
}
|
||||
|
||||
if (params.country !== undefined) {
|
||||
updateData.country = params.country;
|
||||
}
|
||||
|
||||
const [updatedBranch] = await db
|
||||
.update(branches)
|
||||
.set(updateData)
|
||||
.where(eq(branches.id, branchId))
|
||||
.returning();
|
||||
|
||||
return updatedBranch;
|
||||
},
|
||||
|
||||
/**
|
||||
* Удалить филиал
|
||||
*/
|
||||
async delete(branchId: string) {
|
||||
await db.delete(branches).where(eq(branches.id, branchId));
|
||||
},
|
||||
|
||||
/**
|
||||
* Привязать пользователя к филиалу
|
||||
*/
|
||||
async assignUserToBranch(userId: string, branchId: string) {
|
||||
const [userBranch] = await db
|
||||
.insert(userBranches)
|
||||
.values({
|
||||
userId,
|
||||
branchId,
|
||||
})
|
||||
.returning();
|
||||
|
||||
return userBranch;
|
||||
},
|
||||
|
||||
/**
|
||||
* Отвязать пользователя от филиала
|
||||
*/
|
||||
async removeUserFromBranch(userId: string, branchId: string) {
|
||||
await db
|
||||
.delete(userBranches)
|
||||
.where(
|
||||
and(
|
||||
eq(userBranches.userId, userId),
|
||||
eq(userBranches.branchId, branchId)
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Установить текущий филиал для пользователя
|
||||
*/
|
||||
async setUserCurrentBranch(userId: string, branchId: string) {
|
||||
// Проверить, что пользователь привязан к этому филиалу
|
||||
const userBranch = await db.query.userBranches.findFirst({
|
||||
where: and(
|
||||
eq(userBranches.userId, userId),
|
||||
eq(userBranches.branchId, branchId)
|
||||
),
|
||||
});
|
||||
|
||||
if (!userBranch) {
|
||||
throw new Error("User is not assigned to this branch");
|
||||
}
|
||||
|
||||
// Обновить текущий филиал пользователя
|
||||
const [updatedUser] = await db
|
||||
.update(users)
|
||||
.set({
|
||||
currentBranchId: branchId,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(users.id, userId))
|
||||
.returning();
|
||||
|
||||
return updatedUser;
|
||||
},
|
||||
|
||||
/**
|
||||
* Получить пользователей филиала
|
||||
*/
|
||||
async getUsersByBranchId(branchId: string) {
|
||||
const branchUsers = await db.query.userBranches.findMany({
|
||||
where: eq(userBranches.branchId, branchId),
|
||||
with: {
|
||||
user: {
|
||||
columns: {
|
||||
id: true,
|
||||
email: true,
|
||||
fullName: true,
|
||||
role: true,
|
||||
currentBranchId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
return branchUsers.map((record) => record.user);
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,93 @@
|
||||
import { eq } from "drizzle-orm";
|
||||
import db from "../../db";
|
||||
import { companies, branches, userBranches } from "../../db/schema";
|
||||
|
||||
export interface CreateCompanyParams {
|
||||
name: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
export interface UpdateCompanyParams {
|
||||
name?: string;
|
||||
description?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* Сервис для работы с компаниями
|
||||
*/
|
||||
export const companyService = {
|
||||
/**
|
||||
* Создать компанию
|
||||
*/
|
||||
async create(params: CreateCompanyParams) {
|
||||
const [company] = await db
|
||||
.insert(companies)
|
||||
.values({
|
||||
name: params.name,
|
||||
description: params.description,
|
||||
})
|
||||
.returning();
|
||||
|
||||
return company;
|
||||
},
|
||||
|
||||
/**
|
||||
* Получить все компании
|
||||
*/
|
||||
async findAll() {
|
||||
const allCompanies = await db.query.companies.findMany({
|
||||
with: {
|
||||
branches: true,
|
||||
},
|
||||
orderBy: (companies, { asc }) => [asc(companies.name)],
|
||||
});
|
||||
|
||||
return allCompanies;
|
||||
},
|
||||
|
||||
/**
|
||||
* Найти компанию по ID
|
||||
*/
|
||||
async findById(companyId: string) {
|
||||
const company = await db.query.companies.findFirst({
|
||||
where: eq(companies.id, companyId),
|
||||
with: {
|
||||
branches: true,
|
||||
},
|
||||
});
|
||||
|
||||
return company || null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Обновить компанию
|
||||
*/
|
||||
async update(companyId: string, params: UpdateCompanyParams) {
|
||||
const updateData: any = {
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
if (params.name) {
|
||||
updateData.name = params.name;
|
||||
}
|
||||
|
||||
if (params.description !== undefined) {
|
||||
updateData.description = params.description;
|
||||
}
|
||||
|
||||
const [updatedCompany] = await db
|
||||
.update(companies)
|
||||
.set(updateData)
|
||||
.where(eq(companies.id, companyId))
|
||||
.returning();
|
||||
|
||||
return updatedCompany;
|
||||
},
|
||||
|
||||
/**
|
||||
* Удалить компанию (каскадно удалит все филиалы)
|
||||
*/
|
||||
async delete(companyId: string) {
|
||||
await db.delete(companies).where(eq(companies.id, companyId));
|
||||
},
|
||||
};
|
||||
@@ -0,0 +1,266 @@
|
||||
import { eq, and, or } from "drizzle-orm";
|
||||
import db from "../../db";
|
||||
import { serverSessions } from "../../db/schema/serverSessions";
|
||||
import { servers } from "../../db/schema/servers";
|
||||
import { apps } from "../../db/schema/apps";
|
||||
|
||||
export type SessionMode = "stream" | "local";
|
||||
export type SessionStatus = "starting" | "started" | "ending" | "ended";
|
||||
|
||||
export interface CreateSessionParams {
|
||||
appId: string;
|
||||
userId: string;
|
||||
mode: SessionMode;
|
||||
serverId?: string;
|
||||
}
|
||||
|
||||
export interface UpdateSessionParams {
|
||||
status?: SessionStatus;
|
||||
appPid?: number;
|
||||
cirrusPid?: number;
|
||||
endAt?: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* Сервис для работы с игровыми/streaming сессиями
|
||||
*/
|
||||
export const serverSessionService = {
|
||||
/**
|
||||
* Найти сессию по ID
|
||||
*/
|
||||
async findById(sessionId: string) {
|
||||
const session = await db.query.serverSessions.findFirst({
|
||||
where: eq(serverSessions.id, sessionId),
|
||||
with: {
|
||||
app: true,
|
||||
user: {
|
||||
columns: {
|
||||
id: true,
|
||||
email: true,
|
||||
role: true,
|
||||
},
|
||||
},
|
||||
server: true,
|
||||
},
|
||||
});
|
||||
|
||||
return session || null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Найти сессию по ID для конкретного пользователя
|
||||
*/
|
||||
async findByIdForUser(sessionId: string, userId: string) {
|
||||
const session = await db.query.serverSessions.findFirst({
|
||||
where: and(
|
||||
eq(serverSessions.id, sessionId),
|
||||
eq(serverSessions.userId, userId)
|
||||
),
|
||||
with: {
|
||||
app: true,
|
||||
user: {
|
||||
columns: {
|
||||
id: true,
|
||||
email: true,
|
||||
role: true,
|
||||
},
|
||||
},
|
||||
server: true,
|
||||
},
|
||||
});
|
||||
|
||||
return session || null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Получить все сессии пользователя
|
||||
*/
|
||||
async findByUserId(
|
||||
userId: string,
|
||||
filters?: {
|
||||
status?: SessionStatus;
|
||||
mode?: SessionMode;
|
||||
}
|
||||
) {
|
||||
const conditions = [eq(serverSessions.userId, userId)];
|
||||
|
||||
if (filters?.status) {
|
||||
conditions.push(eq(serverSessions.status, filters.status));
|
||||
}
|
||||
|
||||
if (filters?.mode) {
|
||||
conditions.push(eq(serverSessions.mode, filters.mode));
|
||||
}
|
||||
|
||||
const sessions = await db
|
||||
.select({
|
||||
id: serverSessions.id,
|
||||
serverId: serverSessions.serverId,
|
||||
appId: serverSessions.appId,
|
||||
userId: serverSessions.userId,
|
||||
startAt: serverSessions.startAt,
|
||||
endAt: serverSessions.endAt,
|
||||
appPid: serverSessions.appPid,
|
||||
cirrusPid: serverSessions.cirrusPid,
|
||||
mode: serverSessions.mode,
|
||||
status: serverSessions.status,
|
||||
createdAt: serverSessions.createdAt,
|
||||
updatedAt: serverSessions.updatedAt,
|
||||
app: {
|
||||
id: apps.id,
|
||||
name: apps.name,
|
||||
title: apps.title,
|
||||
},
|
||||
})
|
||||
.from(serverSessions)
|
||||
.leftJoin(apps, eq(serverSessions.appId, apps.id))
|
||||
.where(and(...conditions))
|
||||
.orderBy(serverSessions.createdAt);
|
||||
|
||||
return sessions;
|
||||
},
|
||||
|
||||
/**
|
||||
* Проверить, есть ли у пользователя активная сессия для данного приложения
|
||||
*/
|
||||
async hasActiveSession(userId: string, appId: string) {
|
||||
const session = await db.query.serverSessions.findFirst({
|
||||
where: and(
|
||||
eq(serverSessions.userId, userId),
|
||||
eq(serverSessions.appId, appId),
|
||||
or(
|
||||
eq(serverSessions.status, "starting"),
|
||||
eq(serverSessions.status, "started")
|
||||
)
|
||||
),
|
||||
});
|
||||
|
||||
return !!session;
|
||||
},
|
||||
|
||||
/**
|
||||
* Выбрать доступный сервер
|
||||
*/
|
||||
async selectAvailableServer(mode: SessionMode): Promise<string | undefined> {
|
||||
if (mode === "stream") {
|
||||
const server = await db.query.servers.findFirst({
|
||||
where: and(eq(servers.type, "stream"), eq(servers.tier, "prod")),
|
||||
});
|
||||
return server?.id;
|
||||
} else {
|
||||
const server = await db.query.servers.findFirst({
|
||||
where: eq(servers.type, "local"),
|
||||
});
|
||||
return server?.id;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Создать новую сессию
|
||||
*/
|
||||
async create(params: CreateSessionParams) {
|
||||
const { appId, userId, mode, serverId } = params;
|
||||
|
||||
// Выбрать сервер (если не указан)
|
||||
let selectedServerId = serverId;
|
||||
if (!selectedServerId) {
|
||||
selectedServerId = await this.selectAvailableServer(mode);
|
||||
if (!selectedServerId) {
|
||||
throw new Error(`No available ${mode} servers`);
|
||||
}
|
||||
}
|
||||
|
||||
// Вычислить время окончания (по умолчанию +30 минут)
|
||||
const endAt = new Date();
|
||||
endAt.setMinutes(endAt.getMinutes() + 30);
|
||||
|
||||
// Создать сессию
|
||||
const [newSession] = await db
|
||||
.insert(serverSessions)
|
||||
.values({
|
||||
serverId: selectedServerId,
|
||||
appId,
|
||||
userId,
|
||||
mode,
|
||||
status: "starting",
|
||||
endAt,
|
||||
})
|
||||
.returning();
|
||||
|
||||
return newSession;
|
||||
},
|
||||
|
||||
/**
|
||||
* Обновить сессию
|
||||
*/
|
||||
async update(sessionId: string, params: UpdateSessionParams) {
|
||||
const updateData: any = {
|
||||
updatedAt: new Date(),
|
||||
};
|
||||
|
||||
if (params.status) {
|
||||
updateData.status = params.status;
|
||||
}
|
||||
|
||||
if (params.appPid !== undefined) {
|
||||
updateData.appPid = params.appPid;
|
||||
}
|
||||
|
||||
if (params.cirrusPid !== undefined) {
|
||||
updateData.cirrusPid = params.cirrusPid;
|
||||
}
|
||||
|
||||
if (params.endAt) {
|
||||
updateData.endAt = params.endAt;
|
||||
}
|
||||
|
||||
const [updatedSession] = await db
|
||||
.update(serverSessions)
|
||||
.set(updateData)
|
||||
.where(eq(serverSessions.id, sessionId))
|
||||
.returning();
|
||||
|
||||
return updatedSession;
|
||||
},
|
||||
|
||||
/**
|
||||
* Завершить сессию (изменить статус на "ending")
|
||||
*/
|
||||
async end(sessionId: string) {
|
||||
const [updatedSession] = await db
|
||||
.update(serverSessions)
|
||||
.set({
|
||||
status: "ending",
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(serverSessions.id, sessionId))
|
||||
.returning();
|
||||
|
||||
return updatedSession;
|
||||
},
|
||||
|
||||
/**
|
||||
* Продлить сессию
|
||||
*/
|
||||
async extend(sessionId: string, minutes: number) {
|
||||
const session = await this.findById(sessionId);
|
||||
|
||||
if (!session) {
|
||||
throw new Error("Session not found");
|
||||
}
|
||||
|
||||
const newEndAt = session.endAt ? new Date(session.endAt) : new Date();
|
||||
newEndAt.setMinutes(newEndAt.getMinutes() + minutes);
|
||||
|
||||
const [updatedSession] = await db
|
||||
.update(serverSessions)
|
||||
.set({
|
||||
endAt: newEndAt,
|
||||
updatedAt: new Date(),
|
||||
})
|
||||
.where(eq(serverSessions.id, sessionId))
|
||||
.returning();
|
||||
|
||||
return updatedSession;
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user