A production-ready authentication boilerplate built with React 19, Express, and PostgreSQL. Drop it in, configure, and start building your app — no need to re-invent auth every time.
- Email + Password signup with bcrypt hashing
- OTP two-factor login — 6-digit code sent via email
- Password reset with OTP verification (3-step flow)
- JWT-based sessions — 1-hour expiry, stored in localStorage
- Protected CRUD routes — items with AES-256-CBC encrypted pricing
- Clean, minimal UI — Tailwind CSS v4, responsive, mobile-first
| Layer | Tech |
|---|---|
| Frontend | React 19, Vite 8, React Router v7, Tailwind CSS v4 |
| Backend | Express 5, PostgreSQL, JWT, bcrypt |
| Auth | OTP via Nodemailer (Gmail), JWT stateless auth |
| Encryption | AES-256-CBC (Node.js crypto) |
cybersecurity/
├── backend/
│ ├── server.js # Express entry point (port 5000)
│ ├── db.js # PostgreSQL connection pool
│ ├── .env # Secrets (JWT, email, encryption key)
│ ├── routes/ # Express route definitions
│ ├── controllers/ # Business logic
│ ├── middleware/ # JWT verification middleware
│ └── utils/ # AES-256-CBC encryption utility
│
└── secure-auth-app/
└── src/
├── pages/ # Home, Signup, Login, ResetPassword, Items
├── components/ # Navbar (and legacy components)
├── App.jsx # Router configuration
├── main.jsx # React entry point
└── index.css # Tailwind v4 import
- Node.js 18+
- PostgreSQL running locally on port 5432
Create a database named secure_auth_db:
CREATE DATABASE secure_auth_db;Run these table migrations:
CREATE TABLE users (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
otp_code VARCHAR(6),
otp_expiry TIMESTAMP,
reset_otp VARCHAR(6),
reset_otp_expiry TIMESTAMP
);
CREATE TABLE items (
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
description TEXT,
price TEXT NOT NULL
);cd backend
npm installCreate a .env file (see below), then:
node server.js
# Server running on port 5000cd secure-auth-app
npm install
npm run dev
# Vite dev server on http://localhost:5173Create backend/.env:
GMAIL_USER=your-email@gmail.com
GMAIL_PASS=your-gmail-app-password
JWT_SECRET=your-random-jwt-secret
SECRET_KEY=your-aes-encryption-key| Variable | Purpose |
|---|---|
GMAIL_USER |
Gmail address for sending OTP emails |
GMAIL_PASS |
Gmail app password (not your regular password) |
JWT_SECRET |
Secret used to sign and verify JWT tokens |
SECRET_KEY |
Key for AES-256-CBC encryption of item prices |
All endpoints are prefixed with http://localhost:5000/api.
| Method | Endpoint | Description |
|---|---|---|
| POST | /api/auth/signup |
Register a new user (email, password) |
| POST | /api/auth/login |
Authenticate user, sends OTP email |
| POST | /api/auth/verify-otp |
Verify OTP, receives JWT token |
| POST | /api/auth/request-reset |
Request password reset OTP |
| POST | /api/auth/reset-password |
Reset password with OTP + new password |
All endpoints require Authorization: Bearer <token> header.
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/items |
List all items (prices decrypted) |
| POST | /api/items |
Create an item (name, description?, price) |
| PUT | /api/items/:id |
Update an item |
| DELETE | /api/items/:id |
Delete an item |
Signup → Login (email + password) → OTP verification (email) → JWT issued → Access protected routes
- Minimum 8 characters
- At least one uppercase letter
- At least one digit
- At least one special character
Password strength is evaluated in real time on the signup form.
- Passwords hashed with bcrypt (10 salt rounds)
- OTP is 6 digits, expires after 5 minutes
- OTP codes are deleted from the database after successful verification
- Item prices encrypted at rest using AES-256-CBC with a random IV per record
- JWT tokens expire after 1 hour
- CORS is open (
app.use(cors())) — restrict origins in production
MIT — free to use for any project.