Add sign-in and make every row owned
Continues from the last build: It has a real schema with clients, invoices and line items, and validated CRUD through server actions.
Right now InvoiceDesk works, technically. Anyone who opens http://localhost:3000/dashboard sees every client and every invoice in the database, yours, a teammate's test data, whoever seeded it last.
What you'll build
By the end, every clients and invoices row carries an owning user_id, every server action fetches the row first and checks ownership before doing anything with it, middleware bounces anonymous visitors to /login, and a Vitest integration test proves, not assumes, that user B cannot read, edit, or delete user A's invoice by id. You also know, concretely, why your session cookie is httpOnly and why server actions don't need a hand-rolled CSRF token.
See how we teach, before you sign up
You don't just get code dumped on you. Every starter file and every solution is explained line-by-line, in plain English. Here's one real file from this project:
import { handlers } from "@/lib/auth";
// Auth.js v5 handlers cover both GET (OAuth callbacks) and POST (credentials sign in)
export const { GET, POST } = handlers;
Reading this file
import { handlers } from "@/lib/auth";handlers is exported once from lib/auth.ts and reused here.// Auth.js v5 handlers cover both GET (OAuth callbacks) and POST (credentials sign in)One route file handles the whole Auth.js API surface.export const { GET, POST } = handlers;This is the entire route handler; all the logic lives in lib/auth.ts.
Starter file for this rung.
That's 1 of 9 explained code blocks in this single project.
The build, milestone by milestone
- 1
Wire Auth.js v5 with credentials and Google
6 guided stepsEvery ownership check you write later depends on knowing who is asking. Without a real session, 'who owns this invoice' has no answer.
- 2
Protect routes with middleware and session checks
5 guided stepsMiddleware stops the obvious case cheaply, before a single database query runs, but it only knows the route, not which row is being requested, so you still need checks deeper in the stack.
- 3
Scope every query by owner
5 guided stepsA session tells you someone is logged in. It says nothing about which rows are theirs until you attach an owner to the data and check it on every access.
- 4
Kill the IDOR
5 guided stepsA user can be fully authenticated and still not be authorized for a specific resource. An invoice id is just a guessable string in a URL, so 'is there a session' is not enough to protect it.
- 5
Sessions, cookies, and CSRF basics
5 guided stepsA correct ownership check doesn't help if the session cookie itself can be read by a script or replayed cross-site. This milestone makes the transport layer as deliberate as the authorization logic.
What's inside when you start
You'll walk away with
This is portfolio-grade. Build it free.
Sign up to unlock every milestone step-by-step, the code skeletons, full reference solutions, and checkable tasks, with your progress saved as you build.
Start building