Files
evo-sync/web/src/routes/profile.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

117 lines
4.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Hono } from "hono";
import { db } from "../db/client.js";
import { users } from "../db/schema.js";
import { eq, and, ne } from "drizzle-orm";
import { hashPassword, verifyPassword, getSessionUserId, type AppEnv } from "../lib/auth.js";
import { validateProfile, validateResetPassword } from "../lib/validate.js";
import { render } from "../lib/render.js";
export const profileRouter = new Hono<AppEnv>();
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;
}
profileRouter.get("/profile", async (c) => {
const user = await requireUser(c);
if (!user) return c.redirect("/login", 303);
return c.html(render("profile_view.njk", { user }));
});
profileRouter.get("/profile/edit", async (c) => {
const user = await requireUser(c);
if (!user) return c.redirect("/login", 303);
return c.html(render("profile_edit.njk", { user }));
});
profileRouter.post("/profile/edit", async (c) => {
const user = await requireUser(c);
if (!user) return c.redirect("/login", 303);
const form = await c.req.formData();
const data: Record<string, string> = {};
form.forEach((v, k) => { data[k] = String(v); });
const errors = validateProfile(data);
if (!errors.length) {
const existing = await db.select().from(users).where(
and(eq(users.phone, data.phone.trim()), ne(users.id, user.id))
).limit(1);
if (existing.length) errors.push("Пользователь с таким телефоном уже существует");
}
if (errors.length) {
return c.html(render("profile_edit.njk", { user, errors, form: data }));
}
await db.update(users)
.set({ first_name: data.first_name.trim(), last_name: data.last_name.trim(), phone: data.phone.trim() })
.where(eq(users.id, user.id));
const [updated] = await db.select().from(users).where(eq(users.id, user.id)).limit(1);
return c.html(render("profile_edit.njk", { user: updated, success: "Профиль обновлен" }));
});
profileRouter.get("/profile/change-password", async (c) => {
const user = await requireUser(c);
if (!user) return c.redirect("/login", 303);
return c.html(render("profile_change_password.njk", { user }));
});
profileRouter.post("/profile/change-password", async (c) => {
const user = await requireUser(c);
if (!user) return c.redirect("/login", 303);
const form = await c.req.formData();
const data: Record<string, string> = {};
form.forEach((v, k) => { data[k] = String(v); });
const errors: string[] = [];
const currentPassword = data.current_password ?? "";
if (!currentPassword) {
errors.push("Введите текущий пароль");
} else if (!(await verifyPassword(currentPassword, user.password_hash))) {
errors.push("Неверный текущий пароль");
}
errors.push(...validateResetPassword(data));
if (errors.length) {
return c.html(render("profile_change_password.njk", { user, errors }));
}
await db.update(users)
.set({ password_hash: await hashPassword(data.password) })
.where(eq(users.id, user.id));
return c.html(render("profile_change_password.njk", { user, success: "Пароль изменен" }));
});
profileRouter.get("/profile/delete", async (c) => {
const user = await requireUser(c);
if (!user) return c.redirect("/login", 303);
return c.html(render("profile_delete.njk", { user }));
});
profileRouter.post("/profile/delete", async (c) => {
const user = await requireUser(c);
if (!user) return c.redirect("/login", 303);
const form = await c.req.formData();
const password = String(form.get("password") ?? "");
if (!password) {
return c.html(render("profile_delete.njk", { user, errors: ["Введите пароль для подтверждения"] }));
}
if (!(await verifyPassword(password, user.password_hash))) {
return c.html(render("profile_delete.njk", { user, errors: ["Неверный пароль"] }));
}
await db.delete(users).where(eq(users.id, user.id));
const session = c.get("session") as { deleteSession: () => void };
session.deleteSession();
return c.redirect("/", 303);
});