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>
This commit is contained in:
120
web/src/routes/vk.ts
Normal file
120
web/src/routes/vk.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
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);
|
||||
});
|
||||
Reference in New Issue
Block a user