diff --git a/.dockerignore b/.dockerignore index 9ff273c..f700751 100644 --- a/.dockerignore +++ b/.dockerignore @@ -1,7 +1,7 @@ node_modules .next dist -.env.local +.env* .git .gitignore npm-debug.log @@ -12,4 +12,4 @@ yarn-error.log *.db-shm *.db-wal tsconfig.tsbuildinfo -.chunk-cache \ No newline at end of file +.chunk-cache diff --git a/.gitignore b/.gitignore index ed2a7fe..019d9c0 100644 --- a/.gitignore +++ b/.gitignore @@ -42,4 +42,7 @@ next-env.d.ts public/form drizzle/* +!drizzle/0010_rainy_speedball.sql !drizzle/0012_clammy_the_stranger.sql +!drizzle/0013_regular_unus.sql +!drizzle/0014_lethal_pepper_potts.sql diff --git a/README.md b/README.md index cf97749..a4c02a4 100644 --- a/README.md +++ b/README.md @@ -34,4 +34,29 @@ You can check out [the Next.js GitHub repository](https://github.com/vercel/next The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details. + +## Secret safety + +- Keep credentials in `.env` or `.env.local`. Never prefix a credential with + `NEXT_PUBLIC_`. +- Restrict local access before starting the app: + + ```bash + chmod 600 .env .env.local + ``` + +- Scan tracked files and Git history before deployment: + + ```bash + bun run secrets:scan + ``` + +- Deploy with Docker Compose so `.env` is injected only when the container + starts. Environment files are excluded from the Docker build context. +- Give the Discord bot only the permissions it needs. Avoid the Administrator + permission. +- Rotate every credential from `.env` if an environment file, build context, + builder cache, log, or host may have been shared. If prior exposure cannot be + ruled out, treat the credentials as exposed. + # erika diff --git a/app/admin/form/[id]/result/[submissionId]/client.tsx b/app/admin/form/[id]/result/[submissionId]/client.tsx index 4f6af32..53f427a 100644 --- a/app/admin/form/[id]/result/[submissionId]/client.tsx +++ b/app/admin/form/[id]/result/[submissionId]/client.tsx @@ -1,8 +1,15 @@ "use client"; -import React, { useState } from "react"; +import React, { useCallback, useState } from "react"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Button } from "@/components/ui/button"; +import { Textarea } from "@/components/ui/textarea"; +import { + Field, + FieldDescription, + FieldGroup, + FieldLabel, +} from "@/components/ui/field"; import { ContextMenu, ContextMenuTrigger, @@ -12,7 +19,15 @@ import { ContextMenuItem, ContextMenuGroup } from "@/components/ui/context-menu"; -import { Copy, Check, Users, Calendar, Clock } from "lucide-react"; +import { + AlertCircle, + Calendar, + Check, + Clock, + Copy, + Loader2, + Users, +} from "lucide-react"; import { ScrollArea } from "@/components/ui/scroll-area"; import { Dialog, @@ -27,6 +42,11 @@ import { stripHtml } from "@/lib/utils"; import { HtmlDisplay } from "@/components/html-display"; import { formatStoredAnswer } from "@/lib/form-answer-values"; import { normalizeStoredAnswerSnapshot } from "@/lib/submission-intake"; +import { updateSubmissionAdminNote } from "@/app/admin/form/actions"; +import { + useDebouncedAutoSave, + type AutoSaveStatus, +} from "@/hooks/use-debounced-autosave"; import { DiscordProfileDropdown, type DiscordMemberProfile, @@ -42,10 +62,12 @@ interface Question { interface SubmissionDetailClientProps { submission: { id: string; + formId: string; userName: string | null; userDiscordId: string | null; userProfile: DiscordMemberProfile | null; submittedAt: Date; + adminNote: string | null; editHistory?: { editedAt: string; oldAnswers: Record }[] | null; }; questions: Question[]; @@ -58,6 +80,7 @@ export default function SubmissionDetailClient({ answers, }: SubmissionDetailClientProps) { const [copiedId, setCopiedId] = useState(null); + const [adminNote, setAdminNote] = useState(submission.adminNote ?? ""); const handleCopy = (text: string, id: string) => { if (!text) return; @@ -72,7 +95,19 @@ export default function SubmissionDetailClient({ const editHistory = Array.isArray(submission.editHistory) ? submission.editHistory : []; - + const saveAdminNote = useCallback( + () => + updateSubmissionAdminNote( + submission.formId, + submission.id, + adminNote + ).then(() => undefined), + [adminNote, submission.formId, submission.id] + ); + const noteAutoSave = useDebouncedAutoSave({ + save: saveAdminNote, + delay: 1000, + }); return (
@@ -120,6 +155,42 @@ export default function SubmissionDetailClient({ + + + Admin note + + Private context for administrators. Respondents cannot see this note. + + + + + + Note +