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