security hardening, team invites, granular locking, view counts, board subscriptions, scheduled changelog, mentions, recovery codes, accessibility and hover states
This commit is contained in:
100
packages/api/src/routes/admin/notes.ts
Normal file
100
packages/api/src/routes/admin/notes.ts
Normal file
@@ -0,0 +1,100 @@
|
||||
import { FastifyInstance } from "fastify";
|
||||
import { z } from "zod";
|
||||
import { prisma } from "../../lib/prisma.js";
|
||||
|
||||
function redactEmail(email: string): string {
|
||||
const [local, domain] = email.split("@");
|
||||
if (!domain) return "***";
|
||||
return local[0] + "***@" + domain;
|
||||
}
|
||||
|
||||
const createNoteBody = z.object({
|
||||
body: z.string().min(1).max(2000).trim(),
|
||||
});
|
||||
|
||||
export default async function adminNoteRoutes(app: FastifyInstance) {
|
||||
// Get notes for a post
|
||||
app.get<{ Params: { id: string } }>(
|
||||
"/admin/posts/:id/notes",
|
||||
{ preHandler: [app.requireAdmin], config: { rateLimit: { max: 60, timeWindow: "1 minute" } } },
|
||||
async (req, reply) => {
|
||||
const post = await prisma.post.findUnique({ where: { id: req.params.id } });
|
||||
if (!post) {
|
||||
reply.status(404).send({ error: "Post not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
const notes = await prisma.adminNote.findMany({
|
||||
where: { postId: post.id },
|
||||
orderBy: { createdAt: "desc" },
|
||||
take: 100,
|
||||
include: {
|
||||
admin: { select: { email: true } },
|
||||
},
|
||||
});
|
||||
|
||||
reply.send({
|
||||
notes: notes.map((n) => ({
|
||||
id: n.id,
|
||||
body: n.body,
|
||||
adminEmail: n.admin.email ? redactEmail(n.admin.email) : "Team member",
|
||||
createdAt: n.createdAt,
|
||||
})),
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// Add a note to a post
|
||||
app.post<{ Params: { id: string }; Body: z.infer<typeof createNoteBody> }>(
|
||||
"/admin/posts/:id/notes",
|
||||
{ preHandler: [app.requireAdmin], config: { rateLimit: { max: 20, timeWindow: "1 minute" } } },
|
||||
async (req, reply) => {
|
||||
const post = await prisma.post.findUnique({ where: { id: req.params.id } });
|
||||
if (!post) {
|
||||
reply.status(404).send({ error: "Post not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
const { body } = createNoteBody.parse(req.body);
|
||||
|
||||
const admin = await prisma.adminUser.findUnique({ where: { id: req.adminId! }, select: { email: true } });
|
||||
|
||||
const note = await prisma.adminNote.create({
|
||||
data: {
|
||||
body,
|
||||
postId: post.id,
|
||||
adminId: req.adminId!,
|
||||
},
|
||||
});
|
||||
|
||||
reply.status(201).send({
|
||||
id: note.id,
|
||||
body: note.body,
|
||||
postId: note.postId,
|
||||
adminEmail: admin?.email ? redactEmail(admin.email) : "Team member",
|
||||
createdAt: note.createdAt,
|
||||
});
|
||||
}
|
||||
);
|
||||
|
||||
// Delete a note
|
||||
app.delete<{ Params: { noteId: string } }>(
|
||||
"/admin/notes/:noteId",
|
||||
{ preHandler: [app.requireAdmin], config: { rateLimit: { max: 20, timeWindow: "1 minute" } } },
|
||||
async (req, reply) => {
|
||||
const note = await prisma.adminNote.findUnique({ where: { id: req.params.noteId } });
|
||||
if (!note) {
|
||||
reply.status(404).send({ error: "Note not found" });
|
||||
return;
|
||||
}
|
||||
|
||||
if (note.adminId !== req.adminId) {
|
||||
reply.status(403).send({ error: "You can only delete your own notes" });
|
||||
return;
|
||||
}
|
||||
|
||||
await prisma.adminNote.delete({ where: { id: note.id } });
|
||||
reply.status(204).send();
|
||||
}
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user