diff --git a/SETUP.md b/SETUP.md new file mode 100644 index 000000000..591cab57c --- /dev/null +++ b/SETUP.md @@ -0,0 +1,103 @@ +# Epic Stack local setup + +Fork of [epicweb-dev/epic-stack](https://github.com/epicweb-dev/epic-stack) at +[nishilfaldu/epic-stack](https://github.com/nishilfaldu/epic-stack). + +Worked from `~/Desktop/epic-stack` on 2026-10-29. `origin` points only at the +fork (`git@github.com:nishilfaldu/epic-stack.git`). No `upstream` remote was +added, and nothing was pushed to `epicweb-dev/epic-stack`. + +## What runs locally + +The app is a React Router 7 / Express stack with SQLite (Prisma), sessions, RBAC +permissions, and mocked third-party services in development (`MOCKS=true`): +Resend email, GitHub OAuth, Tigris/S3, and pwned-password checks. + +- App URL: http://localhost:3000 +- Node: `v22.23.4` (via nvm; `package.json` engines is `^22.18.0`) +- npm: `10.9.9` +- Seeded admin: username `kody` / password `kodylovesyou` + +## Setup steps that worked + +The machine default Node was 24/25. Epic Stack needs Node 22: + +```sh +nvm install 22 +nvm use 22 +git clone git@github.com:nishilfaldu/epic-stack.git ~/Desktop/epic-stack +cd ~/Desktop/epic-stack +cp .env.example .env +npm install +npm run setup # build, prisma migrate deploy, prisma generate --sql, playwright install +npx prisma@6 db seed +npm run dev # NODE_ENV=development MOCKS=true +``` + +`.env.example` values are enough for local mock-mode. Real GitHub/Resend/Tigris +credentials were not configured. + +Signup verification emails are written to +`tests/fixtures/email/
.json` by the MSW Resend mock. Open the `code` +or `url` from that file on `/verify`. + +## Browser proof (http://localhost:3000) + +Screenshots live in [`docs/setup-proof/`](docs/setup-proof/). + +| Flow | Result | Screenshot | +| --- | --- | --- | +| Homepage | App served | [00](docs/setup-proof/00-homepage.png) | +| Login page | Form rendered | [01](docs/setup-proof/01-login.png) | +| Signup email | Submitted `nishil.setup@example.com` | [02](docs/setup-proof/02-signup.png) | +| Email verification | Mocked OTP `FTK2Y2` from fixture | [03](docs/setup-proof/03-check-email.png) | +| Onboarding | Username `nishilsetup`, name `Nishil Setup` | [04](docs/setup-proof/04-onboarding.png) | +| Signed in | User menu shows Nishil Setup | [05](docs/setup-proof/05-signed-in.png) | +| Create note | Title/content submitted | [06](docs/setup-proof/06-new-note-form.png), [07](docs/setup-proof/07-note-created.png) | +| Edit note | Title and content updated | [08](docs/setup-proof/08-note-edited.png) | +| Delete note | Note gone; empty list | [11](docs/setup-proof/11-note-deleted.png) | +| Permissions (user) | Logged in as `nishilsetup`, kody's note has no Edit/Delete | [09](docs/setup-proof/09-permissions-user-cannot-edit-kody.png) | +| Permissions (user) | `/users/kody/notes//edit` 404s (owner-only loader) | [10](docs/setup-proof/10-permissions-edit-denied.png) | +| Login | Logged out, signed in as seeded `kody` / `kodylovesyou` | [12](docs/setup-proof/12-logged-in-kody.png) | +| Permissions (admin) | Kody sees Delete on another user's note (`delete:note:any`) | [13](docs/setup-proof/13-admin-can-delete-others.png) | + +The admin Delete button on Vicenta Kassulke's note was left unclicked so seed +data stayed intact. + +## Test results + +All existing suites were run with Node 22.23.4 after `npm run setup`. + +| Suite | Command | Result | +| --- | --- | --- | +| Vitest | `npm test -- --run` | **11 files, 42 tests passed** (4.71s) | +| Playwright e2e | `npx playwright test` (Chromium, reused `npm run dev`) | **26 passed** (30.7s) | +| Typecheck | `npm run typecheck` | **passed** | +| ESLint | `npm run lint` | **0 errors**, 3 pre-existing warnings in `remix.init/index.mjs` (`import/order`) | + +Playwright files that passed: `2fa`, `error-boundary`, `note-images`, `notes`, +`onboarding`, `passkey`, `search`, `settings-profile`, `static-files`. + +## Blocked / not done + +- **Fly.io / production deploy** was not attempted. This pass is local-only. +- **Real GitHub OAuth, Resend, Sentry, and Tigris** are mocked. Signup used the + MSW email fixture, not a real inbox. +- **`npm audit`** reported 69 vulnerabilities in upstream dependencies (6 low, + 23 moderate, 35 high, 5 critical). Left as-is; no lockfile changes. +- **Lint warnings** in `remix.init/index.mjs` are upstream and were not changed. +- Default Node on this machine is 24/25; without `nvm use 22` the engines field + is not satisfied. +- Browser automation used the Cursor embedded browser at a narrow viewport, so + screenshots look mobile-width. Behavior was the same as the Playwright + desktop Chromium suite. + +## Recreate + +```sh +nvm use 22 +cd ~/Desktop/epic-stack +npm run dev +# open http://localhost:3000 +# login: kody / kodylovesyou +``` diff --git a/app/routes/_auth/verify.server.ts b/app/routes/_auth/verify.server.ts index 9e9264201..0af46f4d0 100644 --- a/app/routes/_auth/verify.server.ts +++ b/app/routes/_auth/verify.server.ts @@ -137,6 +137,18 @@ export async function isCodeValid({ return true } +async function consumeVerification({ + type, + target, +}: { + type: VerificationTypes + target: string +}) { + await prisma.verification.deleteMany({ + where: { type, target }, + }) +} + export async function validateRequest( request: Request, body: URLSearchParams | FormData, @@ -168,33 +180,28 @@ export async function validateRequest( } const { value: submissionValue } = submission + const type = submissionValue[typeQueryParam] + const target = submissionValue[targetQueryParam] - async function deleteVerification() { - await prisma.verification.delete({ - where: { - target_type: { - type: submissionValue[typeQueryParam], - target: submissionValue[targetQueryParam], - }, - }, - }) - } - - switch (submissionValue[typeQueryParam]) { + switch (type) { case 'reset-password': { - await deleteVerification() + await consumeVerification({ type, target }) return handleResetPasswordVerification({ request, body, submission }) } case 'onboarding': { - await deleteVerification() + await consumeVerification({ type, target }) return handleOnboardingVerification({ request, body, submission }) } case 'change-email': { - await deleteVerification() + await consumeVerification({ type, target }) return handleChangeEmailVerification({ request, body, submission }) } case '2fa': { return handleLoginTwoFactorVerification({ request, body, submission }) } + default: { + const exhaustive: never = type + throw new Error(`Unhandled verification type: ${exhaustive}`) + } } } diff --git a/app/routes/users/$username/notes/$noteId.tsx b/app/routes/users/$username/notes/$noteId.tsx index f72186624..be1f6cd62 100644 --- a/app/routes/users/$username/notes/$noteId.tsx +++ b/app/routes/users/$username/notes/$noteId.tsx @@ -12,7 +12,6 @@ import { ErrorList } from '#app/components/forms.tsx' import { Button } from '#app/components/ui/button.tsx' import { Icon } from '#app/components/ui/icon.tsx' import { StatusButton } from '#app/components/ui/status-button.tsx' -import { requireUserId } from '#app/utils/auth.server.ts' import { prisma } from '#app/utils/db.server.ts' import { getNoteImgSrc, useIsPending } from '#app/utils/misc.tsx' import { requireUserWithPermission } from '#app/utils/permissions.server.ts' @@ -54,7 +53,6 @@ const DeleteFormSchema = z.object({ }) export async function action({ request }: Route.ActionArgs) { - const userId = await requireUserId(request) const formData = await request.formData() const submission = parseWithZod(formData, { schema: DeleteFormSchema, @@ -69,16 +67,12 @@ export async function action({ request }: Route.ActionArgs) { const { noteId } = submission.value const note = await prisma.note.findFirst({ - select: { id: true, ownerId: true, owner: { select: { username: true } } }, + select: { id: true, owner: { select: { username: true } } }, where: { id: noteId }, }) invariantResponse(note, 'Not found', { status: 404 }) - const isOwner = note.ownerId === userId - await requireUserWithPermission( - request, - isOwner ? `delete:note:own` : `delete:note:any`, - ) + await requireUserWithPermission(request, 'delete:note:own') await prisma.note.delete({ where: { id: note.id } }) diff --git a/docs/setup-proof/00-homepage.png b/docs/setup-proof/00-homepage.png new file mode 100644 index 000000000..5949ed1c4 Binary files /dev/null and b/docs/setup-proof/00-homepage.png differ diff --git a/docs/setup-proof/01-login.png b/docs/setup-proof/01-login.png new file mode 100644 index 000000000..cb01e4824 Binary files /dev/null and b/docs/setup-proof/01-login.png differ diff --git a/docs/setup-proof/02-signup.png b/docs/setup-proof/02-signup.png new file mode 100644 index 000000000..ef2faad63 Binary files /dev/null and b/docs/setup-proof/02-signup.png differ diff --git a/docs/setup-proof/03-check-email.png b/docs/setup-proof/03-check-email.png new file mode 100644 index 000000000..8a6717213 Binary files /dev/null and b/docs/setup-proof/03-check-email.png differ diff --git a/docs/setup-proof/04-onboarding.png b/docs/setup-proof/04-onboarding.png new file mode 100644 index 000000000..6c1172fbf Binary files /dev/null and b/docs/setup-proof/04-onboarding.png differ diff --git a/docs/setup-proof/05-signed-in.png b/docs/setup-proof/05-signed-in.png new file mode 100644 index 000000000..612c0aafd Binary files /dev/null and b/docs/setup-proof/05-signed-in.png differ diff --git a/docs/setup-proof/06-new-note-form.png b/docs/setup-proof/06-new-note-form.png new file mode 100644 index 000000000..fb34af947 Binary files /dev/null and b/docs/setup-proof/06-new-note-form.png differ diff --git a/docs/setup-proof/07-note-created.png b/docs/setup-proof/07-note-created.png new file mode 100644 index 000000000..f9ba126bc Binary files /dev/null and b/docs/setup-proof/07-note-created.png differ diff --git a/docs/setup-proof/08-note-edited.png b/docs/setup-proof/08-note-edited.png new file mode 100644 index 000000000..af5c54991 Binary files /dev/null and b/docs/setup-proof/08-note-edited.png differ diff --git a/docs/setup-proof/09-permissions-user-cannot-edit-kody.png b/docs/setup-proof/09-permissions-user-cannot-edit-kody.png new file mode 100644 index 000000000..cfe87950b Binary files /dev/null and b/docs/setup-proof/09-permissions-user-cannot-edit-kody.png differ diff --git a/docs/setup-proof/10-permissions-edit-denied.png b/docs/setup-proof/10-permissions-edit-denied.png new file mode 100644 index 000000000..540765c92 Binary files /dev/null and b/docs/setup-proof/10-permissions-edit-denied.png differ diff --git a/docs/setup-proof/11-note-deleted.png b/docs/setup-proof/11-note-deleted.png new file mode 100644 index 000000000..aac5a2353 Binary files /dev/null and b/docs/setup-proof/11-note-deleted.png differ diff --git a/docs/setup-proof/12-logged-in-kody.png b/docs/setup-proof/12-logged-in-kody.png new file mode 100644 index 000000000..6108f4bde Binary files /dev/null and b/docs/setup-proof/12-logged-in-kody.png differ diff --git a/docs/setup-proof/13-admin-can-delete-others.png b/docs/setup-proof/13-admin-can-delete-others.png new file mode 100644 index 000000000..33a591460 Binary files /dev/null and b/docs/setup-proof/13-admin-can-delete-others.png differ diff --git a/docs/setup-proof/TEST-RESULTS.txt b/docs/setup-proof/TEST-RESULTS.txt new file mode 100644 index 000000000..dbcf31d2f --- /dev/null +++ b/docs/setup-proof/TEST-RESULTS.txt @@ -0,0 +1,19 @@ +Date: 2026-10-29 +Node: v22.23.4 +npm: 10.9.9 +CWD: /Users/happyhome/Desktop/epic-stack + +vitest (npm test -- --run) + Test Files 11 passed (11) + Tests 42 passed (42) + Duration 4.71s + +playwright (npx playwright test, chromium, reuseExistingServer) + 26 passed (30.7s) + +typecheck (npm run typecheck) + passed (react-router typegen && tsc) + +lint (npm run lint) + 0 errors + 3 warnings in remix.init/index.mjs (import/order) — upstream, unchanged