Files
evo-sync/web/src/routes/vk.ts
mguschin 854c912a88 Migrate web app from Python/FastAPI to Node.js/TypeScript
Replace the entire Python/FastAPI backend with a Node.js/TypeScript stack:
- Framework: Hono + @hono/node-server
- Templates: Nunjucks (.njk) replacing Jinja2 (.html)
- ORM: Drizzle ORM with mysql2 (same MariaDB schema, no migrations needed)
- Sessions: hono-sessions with CookieStore
- CSS: Pico CSS v2 replacing Bootstrap 5 (Bootstrap Icons CDN kept)
- Dev: tsx watch; Prod: tsc + node dist/index.js

Original Python app preserved in web-python/ as backup.
Updated Dockerfile.web and docker-compose.yml for Node.js deployment.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-17 19:33:32 +03:00

121 lines
4.0 KiB
TypeScript

import { Hono } from "hono";
import { db } from "../db/client.js";
import { users, vk_connections } from "../db/schema.js";
import { eq } from "drizzle-orm";
import { getSessionUserId, type AppEnv } from "../lib/auth.js";
import { render } from "../lib/render.js";
import { config } from "../config.js";
export const vkRouter = new Hono<AppEnv>();
const VK_API_URL = "https://api.vk.com/method";
const VK_OAUTH_URL = "https://oauth.vk.com/authorize";
async function requireUser(c: Parameters<typeof getSessionUserId>[0]) {
const id = getSessionUserId(c);
if (!id) return null;
const [user] = await db.select().from(users).where(eq(users.id, id)).limit(1);
return user ?? null;
}
async function fetchGroupInfo(token: string): Promise<{ groupId: string | null; groupName: string | null }> {
try {
const params = new URLSearchParams({
access_token: token,
v: config.VK_API_VERSION,
filter: "admin",
extended: "1",
count: "1",
});
const resp = await fetch(`${VK_API_URL}/groups.get?${params}`, {
signal: AbortSignal.timeout(15000),
});
if (!resp.ok) return { groupId: null, groupName: null };
const data = await resp.json() as { error?: unknown; response?: { items?: Array<{ id: number; name: string }> } };
if (data.error) return { groupId: null, groupName: null };
const items = data.response?.items ?? [];
if (items.length === 0) return { groupId: null, groupName: null };
return { groupId: String(items[0].id), groupName: items[0].name };
} catch {
return { groupId: null, groupName: null };
}
}
vkRouter.get("/vk", async (c) => {
const user = await requireUser(c);
if (!user) return c.redirect("/login", 303);
const [connection] = await db.select().from(vk_connections).where(eq(vk_connections.user_id, user.id)).limit(1);
const error = c.req.query("error") ?? null;
return c.html(render("vk.njk", {
user,
connection: connection ?? null,
error,
vk_client_id: config.VK_CLIENT_ID,
callback_url: `${config.BASE_URL}/vk/callback`,
}));
});
vkRouter.get("/vk/connect", async (c) => {
const user = await requireUser(c);
if (!user) return c.redirect("/login", 303);
if (!config.VK_CLIENT_ID) return c.redirect("/vk?error=no_client_id", 303);
const params = new URLSearchParams({
client_id: config.VK_CLIENT_ID,
scope: "market,groups",
redirect_uri: `${config.BASE_URL}/vk/callback`,
display: "page",
response_type: "token",
v: config.VK_API_VERSION,
});
return c.redirect(`${VK_OAUTH_URL}?${params}`, 302);
});
vkRouter.get("/vk/callback", async (c) => {
const user = await requireUser(c);
if (!user) return c.redirect("/login", 303);
return c.html(render("vk_callback.njk", { user }));
});
vkRouter.post("/vk/token", async (c) => {
const user = await requireUser(c);
if (!user) return c.redirect("/login", 303);
const form = await c.req.formData();
const token = String(form.get("token") ?? "").trim();
if (!token) return c.redirect("/vk?error=empty_token", 303);
const { groupId, groupName } = await fetchGroupInfo(token);
if (!groupId) return c.redirect("/vk?error=invalid_token", 303);
const now = new Date();
const [existing] = await db.select().from(vk_connections).where(eq(vk_connections.user_id, user.id)).limit(1);
if (existing) {
await db.update(vk_connections)
.set({ access_token: token, vk_user_id: groupId, first_name: groupName, last_name: null, is_online: true, last_checked_at: now, updated_at: now })
.where(eq(vk_connections.id, existing.id));
} else {
await db.insert(vk_connections).values({
user_id: user.id,
access_token: token,
vk_user_id: groupId,
first_name: groupName,
last_name: null,
is_online: true,
last_checked_at: now,
});
}
return c.redirect("/connections", 303);
});
vkRouter.post("/vk/disconnect", async (c) => {
const user = await requireUser(c);
if (!user) return c.redirect("/login", 303);
await db.delete(vk_connections).where(eq(vk_connections.user_id, user.id));
return c.redirect("/connections", 303);
});