What you'll build
In this part you'll add a complete authentication layer — users can register with an email and password, log in to receive a JWT token, and use that token to access protected endpoints.
By the end of this part:
POST /auth/registercreates a user with a hashed passwordPOST /auth/loginreturns a signed JWT on successGET /auth/mereturns the current user's profile (protected)POST /products,PUT /products/:id, andDELETE /products/:idrequire a valid JWT
Add the users table
In the Supabase SQL editor:
create table users (
id uuid primary key default gen_random_uuid(),
email text not null unique,
password_hash text not null,
name text,
role text not null default 'user',
created_at timestamptz default now()
);
Install auth dependencies:
npm install bcryptjs jsonwebtoken
npm install -D @types/bcryptjs @types/jsonwebtoken
Build the auth layer
Add JWT authentication to the product catalog API.
1. src/schemas/auth.schema.ts — RegisterSchema: { email: string email, password: string min 8, name: string optional }. LoginSchema: { email: string email, password: string }.
2. src/controllers/auth.controller.ts — export:
- register(req, res, next): check email not already in users table (return 409 if taken). Hash password with bcrypt (saltRounds=12). Insert into users. Sign a JWT with { sub: user.id, email, role } using JWT_SECRET, expires in "7d". Return 201 with { data: { token, user: { id, email, name, role } } }.
- login(req, res, next): fetch user by email. If not found, return 401 { error: "Invalid credentials" } (same message for both failure cases to avoid enumeration). Compare password with bcrypt.compare. If mismatch, return 401. Sign JWT. Return 200 with { data: { token, user } }.
- me(req, res, next): return the user attached to req.user by the auth middleware.
3. src/middleware/auth.ts — requireAuth middleware: reads Authorization header, expects "Bearer
Protect the product write routes
In src/routes/products.router.ts, add requireAuth middleware to the POST, PUT, and DELETE routes. The GET routes remain public. Import requireAuth from src/middleware/auth.ts.
Test authentication
# Register a new user
curl -X POST http://localhost:3000/auth/register \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]","password":"securepassword","name":"Test User"}'
# Log in (copy the token from the response)
curl -X POST http://localhost:3000/auth/login \
-H "Content-Type: application/json" \
-d '{"email":"[email protected]","password":"securepassword"}'
# Use the token (replace TOKEN with the actual value)
curl http://localhost:3000/auth/me \
-H "Authorization: Bearer TOKEN"
# Try creating a product without a token (should return 401)
curl -X POST http://localhost:3000/products \
-H "Content-Type: application/json" \
-d '{"name":"Test","price_ghs":10,"stock":5}'
# Create a product with the token (should return 201)
curl -X POST http://localhost:3000/products \
-H "Content-Type: application/json" \
-H "Authorization: Bearer TOKEN" \
-d '{"name":"Test Product","price_ghs":49.99,"stock":20}'
JWT vs sessions: JWTs are stateless — the server doesn't store them. This means you can't invalidate a token before it expires (without extra infrastructure like a token blocklist). For this API, 7-day expiry with short-lived tokens is a good default. If you need instant revocation (e.g. after a password change), add a token_version column to users and include it in the JWT payload.
Project structure after Part 3
src/
controllers/
auth.controller.ts
products.controller.ts
categories.controller.ts
middleware/
auth.ts
errorHandler.ts
validate.ts
routes/
auth.router.ts
products.router.ts
categories.router.ts
schemas/
auth.schema.ts
product.schema.ts
category.schema.ts
types.ts
index.ts
What's next
In Part 4 we add a Paystack webhook endpoint. When a customer pays for a product, Paystack calls your API with a signed event and you update the order status in the database.
BlocWeave