feat(auth): password change (self + admin reset) with rate limiting; dedupe watchlist to user menu
Build & Push Docker Image / build (push) Successful in 2m19s
Build & Push Docker Image / build (push) Successful in 2m19s
This commit is contained in:
@@ -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({
|
||||
|
||||
Reference in New Issue
Block a user