Files
iistwin/server/vpn/vpn.db.ts
Ильяс Султанов 586a591773 fix(vpn): восстановление VPN-модуля после затирания chat/push fix'ом
- Добавлена ветка bot.type === 'vpn' в messenger.messages.routes.ts
- Добавлен полный модуль server/vpn/ (routes, service, db, bot, config)
- Подключены VPN-маршруты в server/routes/index.ts
- Добавлена раздача /apps статики и COPY в Dockerfile
- apps/ пока содержит .gitkeep и README; APK/IPA нужно добавить отдельно
2026-07-17 09:32:43 +03:00

287 lines
7.8 KiB
TypeScript

import { db } from '../db';
import {
formFields,
formStatuses,
tasks,
taskFieldValues,
type Task,
type FormField,
type FormStatus,
} from '@shared/schema';
import { eq, and, sql } from 'drizzle-orm';
import { storage } from '../storage';
import { VPN_FORM_ID } from './vpn.config';
export interface VpnFormCache {
fields: FormField[];
statuses: FormStatus[];
fieldByCode: Map<string, FormField>;
statusByName: Map<string, FormStatus>;
}
const cacheByOrg = new Map<number, VpnFormCache>();
export async function getVpnFormCache(organizationId: number): Promise<VpnFormCache> {
if (cacheByOrg.has(organizationId)) {
return cacheByOrg.get(organizationId)!;
}
const [fields, statuses] = await Promise.all([
db
.select()
.from(formFields)
.where(eq(formFields.formId, VPN_FORM_ID))
.then(rows => rows as FormField[]),
db
.select()
.from(formStatuses)
.where(eq(formStatuses.formId, VPN_FORM_ID))
.then(rows => rows as FormStatus[]),
]);
const cache: VpnFormCache = {
fields,
statuses,
fieldByCode: new Map(fields.map(f => [f.code, f])),
statusByName: new Map(statuses.map(s => [s.name, s])),
};
cacheByOrg.set(organizationId, cache);
return cache;
}
export function getFieldId(cache: VpnFormCache, code: string): number {
const f = cache.fieldByCode.get(code);
if (!f) throw new Error(`VPN field not found: ${code}`);
return f.id;
}
export function getStatusId(cache: VpnFormCache, name: string): number {
const s = cache.statusByName.get(name);
if (!s) throw new Error(`VPN status not found: ${name}`);
return s.id;
}
export function clearVpnFormCache(organizationId: number): void {
cacheByOrg.delete(organizationId);
}
export interface VpnTaskData {
task: Task;
values: Record<string, unknown>;
}
export async function findVpnTaskByToken(
token: string,
organizationId: number,
): Promise<VpnTaskData | null> {
const cache = await getVpnFormCache(organizationId);
const fieldId = getFieldId(cache, 'subscription_token');
const rows = await db
.select({ task: tasks, value: taskFieldValues.value })
.from(taskFieldValues)
.innerJoin(tasks, eq(taskFieldValues.taskId, tasks.id))
.where(
and(
eq(taskFieldValues.fieldId, fieldId),
eq(taskFieldValues.formId, VPN_FORM_ID),
sql`${taskFieldValues.value} = to_jsonb(${token}::text)`,
eq(tasks.organizationId, organizationId),
),
)
.limit(1);
if (!rows.length) return null;
return loadVpnTaskValues(rows[0].task.id, organizationId);
}
export async function findVpnTaskByRoomUrl(
roomUrl: string,
organizationId: number,
): Promise<VpnTaskData | null> {
const cache = await getVpnFormCache(organizationId);
const fieldId = getFieldId(cache, 'room_url');
const rows = await db
.select({ task: tasks })
.from(taskFieldValues)
.innerJoin(tasks, eq(taskFieldValues.taskId, tasks.id))
.where(
and(
eq(taskFieldValues.fieldId, fieldId),
eq(taskFieldValues.formId, VPN_FORM_ID),
sql`${taskFieldValues.value} = to_jsonb(${roomUrl}::text)`,
eq(tasks.organizationId, organizationId),
),
)
.limit(1);
if (!rows.length) return null;
return loadVpnTaskValues(rows[0].task.id, organizationId);
}
export async function findVpnTaskByRoomAndDevice(
roomUrl: string,
deviceId: string,
organizationId: number,
): Promise<VpnTaskData | null> {
const cache = await getVpnFormCache(organizationId);
const roomFieldId = getFieldId(cache, 'room_url');
const rows = await db
.select({ task: tasks })
.from(taskFieldValues)
.innerJoin(tasks, eq(taskFieldValues.taskId, tasks.id))
.where(
and(
eq(taskFieldValues.fieldId, roomFieldId),
eq(taskFieldValues.formId, VPN_FORM_ID),
sql`${taskFieldValues.value} = to_jsonb(${roomUrl}::text)`,
eq(tasks.organizationId, organizationId),
),
)
.limit(1);
if (!rows.length) return null;
const data = await loadVpnTaskValues(rows[0].task.id, organizationId);
if (data.values.hwid !== deviceId) return null;
return data;
}
export async function findVpnTaskByRoomAndSession(
roomUrl: string,
sessionId: string,
organizationId: number,
): Promise<VpnTaskData | null> {
const cache = await getVpnFormCache(organizationId);
const roomFieldId = getFieldId(cache, 'room_url');
const rows = await db
.select({ task: tasks })
.from(taskFieldValues)
.innerJoin(tasks, eq(taskFieldValues.taskId, tasks.id))
.where(
and(
eq(taskFieldValues.fieldId, roomFieldId),
eq(taskFieldValues.formId, VPN_FORM_ID),
sql`${taskFieldValues.value} = to_jsonb(${roomUrl}::text)`,
eq(tasks.organizationId, organizationId),
),
)
.limit(1);
if (!rows.length) return null;
const data = await loadVpnTaskValues(rows[0].task.id, organizationId);
if (data.values.active_session_id !== sessionId) return null;
return data;
}
export async function loadVpnTaskValues(
taskId: number,
organizationId: number,
): Promise<VpnTaskData> {
const cache = await getVpnFormCache(organizationId);
const [task, fvs] = await Promise.all([
storage.getTask(taskId, organizationId),
storage.getTaskFieldValues(taskId, organizationId),
]);
if (!task) throw new Error(`VPN task not found: ${taskId}`);
const values: Record<string, unknown> = {};
for (const fv of fvs) {
const field = cache.fields.find(f => f.id === fv.fieldId);
if (field) {
values[field.code] = fv.value;
}
}
return { task, values };
}
export async function setVpnTaskField(
taskId: number,
fieldCode: string,
value: unknown,
organizationId: number,
): Promise<void> {
const cache = await getVpnFormCache(organizationId);
const fieldId = getFieldId(cache, fieldCode);
const existing = await db
.select({ id: taskFieldValues.id })
.from(taskFieldValues)
.where(and(eq(taskFieldValues.taskId, taskId), eq(taskFieldValues.fieldId, fieldId)))
.limit(1);
if (existing.length) {
await storage.updateTaskFieldValue(taskId, fieldId, organizationId, { value: value as any });
} else {
await storage.createTaskFieldValue({
taskId,
fieldId,
formId: VPN_FORM_ID,
value: value as any,
});
}
}
export async function setVpnTaskStatus(
taskId: number,
statusName: string,
organizationId: number,
): Promise<void> {
const cache = await getVpnFormCache(organizationId);
const statusId = getStatusId(cache, statusName);
const status = cache.statusByName.get(statusName)!;
await storage.updateTask(taskId, organizationId, {
currentStatusId: statusId,
isCompleted: status.isFinal,
});
}
export async function createVpnTask(
input: {
organizationId: number;
createdBy: number;
title: string;
statusName: string;
fields: Record<string, unknown>;
},
): Promise<Task> {
const cache = await getVpnFormCache(input.organizationId);
const statusId = getStatusId(cache, input.statusName);
const task = await storage.createTask({
organizationId: input.organizationId,
formId: VPN_FORM_ID,
title: input.title,
currentStatusId: statusId,
createdBy: input.createdBy,
completedAt: null,
dueDate: null,
});
for (const [code, value] of Object.entries(input.fields)) {
if (value === undefined || value === null) continue;
const fieldId = getFieldId(cache, code);
await storage.createTaskFieldValue({
taskId: task.id,
fieldId,
formId: VPN_FORM_ID,
value,
});
}
return task;
}
export async function updateVpnTaskFieldMap(
taskId: number,
organizationId: number,
fields: Record<string, unknown>,
): Promise<void> {
for (const [code, value] of Object.entries(fields)) {
if (value === undefined) continue;
await setVpnTaskField(taskId, code, value, organizationId);
}
}