feat(auth): password change (self + admin reset) with rate limiting; dedupe watchlist to user menu
Build & Push Docker Image / build (push) Successful in 2m19s

This commit is contained in:
opencode
2026-08-03 07:43:24 +02:00
parent 63f0bdbab6
commit 68a81ec775
20 changed files with 825 additions and 16 deletions
+43 -1
View File
@@ -4,6 +4,7 @@ import bcrypt from "bcryptjs";
import { db, usersTable } from "@workspace/db";
import { requireAdmin } from "../middleware/auth";
import { writeAuditLog } from "../lib/audit";
import { passwordRateLimit } from "../lib/rate-limit";
import { z } from "zod";
const router: IRouter = Router();
@@ -23,6 +24,46 @@ const UserUpdateSchema = z.object({
tier: Tier.optional(),
});
const SetPasswordSchema = z.object({
password: z.string().min(8),
});
router.patch("/users/:id/password", requireAdmin, passwordRateLimit, async (req, res): Promise<void> => {
const id = parseInt(String(req.params.id), 10);
if (isNaN(id)) {
res.status(400).json({ error: "Invalid user id" });
return;
}
const parsed = SetPasswordSchema.safeParse(req.body);
if (!parsed.success) {
res.status(400).json({ error: parsed.error.message });
return;
}
const [target] = await db
.select({ id: usersTable.id, authProvider: usersTable.authProvider, username: usersTable.username })
.from(usersTable)
.where(eq(usersTable.id, id))
.limit(1);
if (!target) {
res.status(404).json({ error: "User not found" });
return;
}
if (target.authProvider !== "local") {
res.status(422).json({ error: "oidc" });
return;
}
const passwordHash = await bcrypt.hash(parsed.data.password, 12);
await db.update(usersTable).set({ passwordHash }).where(eq(usersTable.id, id));
await writeAuditLog(req, "user", id, "set_password", { username: target.username });
res.sendStatus(204);
});
router.get("/users", requireAdmin, async (req, res): Promise<void> => {
const users = await db
.select({
@@ -31,6 +72,7 @@ router.get("/users", requireAdmin, async (req, res): Promise<void> => {
email: usersTable.email,
role: usersTable.role,
tier: usersTable.tier,
authProvider: usersTable.authProvider,
createdAt: usersTable.createdAt,
})
.from(usersTable)
@@ -56,7 +98,7 @@ router.post("/users", requireAdmin, async (req, res): Promise<void> => {
return;
}
const passwordHash = await bcrypt.hash(parsed.data.password, 10);
const passwordHash = await bcrypt.hash(parsed.data.password, 12);
const [user] = await db
.insert(usersTable)
.values({