Skip to content

Commit f462b2e

Browse files
committed
minor UI fixes
1 parent 01fda32 commit f462b2e

15 files changed

Lines changed: 953 additions & 512 deletions

File tree

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
-- Improve dashboard query performance for user-scoped collection and drawing listings.
2+
CREATE INDEX IF NOT EXISTS "Collection_userId_updatedAt_idx"
3+
ON "Collection" ("userId", "updatedAt");
4+
5+
CREATE INDEX IF NOT EXISTS "Drawing_userId_updatedAt_idx"
6+
ON "Drawing" ("userId", "updatedAt");
7+
8+
CREATE INDEX IF NOT EXISTS "Drawing_userId_collectionId_updatedAt_idx"
9+
ON "Drawing" ("userId", "collectionId", "updatedAt");

backend/prisma/schema.prisma

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ model Collection {
4949
drawings Drawing[]
5050
createdAt DateTime @default(now())
5151
updatedAt DateTime @updatedAt
52+
53+
@@index([userId, updatedAt])
5254
}
5355

5456
model Drawing {
@@ -65,6 +67,9 @@ model Drawing {
6567
collection Collection? @relation(fields: [collectionId], references: [id])
6668
createdAt DateTime @default(now())
6769
updatedAt DateTime @updatedAt
70+
71+
@@index([userId, updatedAt])
72+
@@index([userId, collectionId, updatedAt])
6873
}
6974

7075
model Library {

backend/src/auth.ts

Lines changed: 43 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@ import { z } from "zod";
99
import { PrismaClient, Prisma } from "./generated/client";
1010
import { config } from "./config";
1111
import { requireAuth, optionalAuth } from "./middleware/auth";
12-
import { sanitizeText } from "./security";
12+
import { sanitizeText, getCsrfTokenHeader, validateCsrfToken } from "./security";
1313
import rateLimit, { MemoryStore } from "express-rate-limit";
1414
import { logAuditEvent } from "./utils/audit";
1515
import crypto from "crypto";
@@ -281,6 +281,36 @@ const requireAdmin = (
281281
return true;
282282
};
283283

284+
const getClientId = (req: Request): string => {
285+
const ip = req.ip || req.connection.remoteAddress || "unknown";
286+
const userAgent = req.headers["user-agent"] || "unknown";
287+
return `${ip}:${userAgent}`.slice(0, 256);
288+
};
289+
290+
const requireCsrf = (req: Request, res: Response): boolean => {
291+
const headerName = getCsrfTokenHeader();
292+
const tokenHeader = req.headers[headerName];
293+
const token = Array.isArray(tokenHeader) ? tokenHeader[0] : tokenHeader;
294+
295+
if (!token) {
296+
res.status(403).json({
297+
error: "CSRF token missing",
298+
message: `Missing ${headerName} header`,
299+
});
300+
return false;
301+
}
302+
303+
if (!validateCsrfToken(getClientId(req), token)) {
304+
res.status(403).json({
305+
error: "CSRF token invalid",
306+
message: "Invalid or expired CSRF token. Please refresh and try again.",
307+
});
308+
return false;
309+
}
310+
311+
return true;
312+
};
313+
284314
const countActiveAdmins = async () => {
285315
return prisma.user.count({
286316
where: { role: "ADMIN", isActive: true },
@@ -968,6 +998,8 @@ router.get("/status", optionalAuth, async (req: Request, res: Response) => {
968998
*/
969999
router.post("/auth-enabled", optionalAuth, async (req: Request, res: Response) => {
9701000
try {
1001+
if (!requireCsrf(req, res)) return;
1002+
9711003
const parsed = authEnabledToggleSchema.safeParse(req.body);
9721004
if (!parsed.success) {
9731005
return res
@@ -1477,6 +1509,15 @@ router.patch("/users/:id", requireAuth, async (req: Request, res: Response) => {
14771509

14781510
res.json({ user: updated });
14791511
} catch (error) {
1512+
if (
1513+
error instanceof Prisma.PrismaClientKnownRequestError &&
1514+
error.code === "P2002"
1515+
) {
1516+
return res.status(409).json({
1517+
error: "Conflict",
1518+
message: "User with this username already exists",
1519+
});
1520+
}
14801521
console.error("Update user error:", error);
14811522
res.status(500).json({
14821523
error: "Internal server error",
@@ -1745,7 +1786,7 @@ router.post("/password-reset-request", loginAttemptRateLimiter, async (req: Requ
17451786
: `http://${baseUrlRaw}`
17461787
: "http://localhost:6767";
17471788
const baseUrl = baseUrlWithProtocol.replace(/\/$/, "");
1748-
console.log(`[DEV] Reset URL: ${baseUrl}/reset-password?token=${resetToken}`);
1789+
console.log(`[DEV] Reset URL: ${baseUrl}/reset-password-confirm?token=${resetToken}`);
17491790
}
17501791
}
17511792

0 commit comments

Comments
 (0)