Strict Authentication System: MongoDB + JWT + OAuth (Google, GitHub, Email)
This repository has been split into dedicated frontend (/client) and backend (/server) with a strict login system. Frontend no longer talks to Supabase directly; all data flows through the backend API secured by JWT + httpOnly cookies.
accio-job-market/
├─ client/ # React + Vite frontend (port 5173)
└─ server/ # Express + MongoDB backend (port 5000)
| Layer | Implementation |
|---|---|
| Database | MongoDB (Mongoose) — User, Job, Application, SavedJob |
| JWT | Access (15m) + Refresh (7d) via jsonwebtoken |
POST /api/auth/register + POST /api/auth/login with bcrypt (12 rounds) |
|
| OAuth | Google (passport-google-oauth20) + GitHub (passport-github2) → JWT + redirect to CLIENT_URL/oauth/callback?accessToken=... |
| Cookies | httpOnly accessToken (15m) + refreshToken (7d, path /api/auth) |
| Validation | express-validator strict (name 2-50, email, password 8-64 + upper/lower/num/special) |
| Security | helmet, cors whitelist, express-rate-limit (auth 20/15m, login 10/15m), account lockout (5 fails → 15 min), cookie-parser, body limit 10kb |
| Middleware | authenticate (Bearer or cookie, verify + user existence + lock check), authorize(role) |
| Frontend | AuthContext + api.js (axios, Bearer, auto-refresh on 401 TOKEN_EXPIRED), ProtectedRoute / PublicRoute, Login / Register / OAuthCallback screens |
POST /api/auth/login→ setshttpOnlycookies + returnsaccessToken- Frontend stores
accessTokeninlocalStorage, sendsAuthorization: Bearer <token> - On
401 TOKEN_EXPIRED, axios interceptor callsPOST /api/auth/refresh(cookie) → newaccessToken→ retry - On refresh failure, clear storage + redirect to
/login?error=session_expired POST /api/auth/logoutclears both cookies
- 8-64 chars
- Requires:
/(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&.#^_-])/ - Bcrypt salt 12,
select: falseon password field
MAX_LOGIN_ATTEMPTS=5,LOCK_TIME_MS=900000(15 min)loginAttemptsincrement on fail,lockUntilset when threshold hit, reset on success- OAuth users without password must use OAuth (
provider !== 'local')
- Stack: React 18, React Router 6, Axios, Vite, Tailwind, Chart.js
- Env:
VITE_API_URL=http://localhost:5000(see.env.example) - No Direct Supabase:
src/lib/supabase.jsis now a deprecated shim (throws if used). All data viasrc/lib/api.js→ backend. - Auth:
src/context/AuthContext.jsx,src/components/ProtectedRoute.jsx - App Data:
src/context/AppContext.jsxnow callsGET /api/profile/dashboard,POST /api/jobs/import, etc., with_id → idnormalization - Routes:
- Public:
/login,/register,/oauth/callback - Protected (via
Layout):/dashboard,/explorer,/source,/resume,/recent,/trending Layout.jsxshows user (initials, provider badge) + logout, strict badge
- Public:
cd client
cp .env.example .env # set VITE_API_URL
npm install
npm run dev # http://localhost:5173
npm run buildVite proxies /api → VITE_API_URL (see vite.config.js).
- Stack: Express 5, Mongoose 8, Passport (Google/GitHub), JWT, bcryptjs, helmet, cors, cookie-parser
- Env: see
.env.example(MONGO_URI, JWT secrets, OAuth creds, CLIENT_URL) - Entry:
src/index.js(helmet, cors whitelist, passport init,/api/auth,/api/jobs,/api/profile) - Models:
User.js(with lockout),Job.js,Application.js,SavedJob.js - Routes:
GET /,GET /api/healthPOST /api/auth/register,POST /api/auth/login,POST /api/auth/refresh,POST /api/auth/logout,GET /api/auth/me,PUT /api/auth/profile,GET /api/auth/google,GET /api/auth/google/callback,GET /api/auth/github,GET /api/auth/github/callback,GET /api/auth/healthGET /api/jobs,POST /api/jobs/import,GET /api/jobs/saved,POST /api/jobs/:id/save,DELETE /api/jobs/:id/save,GET /api/jobs/applications/all,PUT /api/jobs/:id/application,POST /api/jobs/live-searchGET /api/profile,PUT /api/profile,GET /api/profile/dashboard,POST /api/profile/resume-upload,POST /api/profile/send-job-emails
cd server
cp .env.example .env # set MONGO_URI, JWT secrets, OAuth creds
npm install
npm run dev # http://localhost:5000 (nodemon)
npm start # productionTested: npm run dev connects to mongodb://localhost:27017/jobpilot, auth flow works, 401 without token, lockout after 5 fails.
# Terminal 1 — Backend
cd server && npm install && npm run dev
# → [DB] MongoDB connected: localhost / jobpilot
# → [Server] Running on http://localhost:5000
# Terminal 2 — Frontend
cd client && npm install && npm run dev
# → Vite dev server at http://localhost:5173- Register at
http://localhost:5173/register(strict password) or login via Google/GitHub (requires OAuth creds in server.env) - All
/dashboardetc. are protected — unauthenticated redirect to/login
Google: https://console.cloud.google.com/apis/credentials
- Create OAuth 2.0 Client ID (Web)
- Authorized redirect URI:
http://localhost:5000/api/auth/google/callback(or prodGOOGLE_CALLBACK_URL) - Set
GOOGLE_CLIENT_ID,GOOGLE_CLIENT_SECRET,GOOGLE_CALLBACK_URLinserver/.env
GitHub: https://github.com/settings/developers
- New OAuth App → Callback URL:
http://localhost:5000/api/auth/github/callback - Set
GITHUB_CLIENT_ID,GITHUB_CLIENT_SECRET,GITHUB_CALLBACK_URL
If not configured, /api/auth/health shows google: false, github: false, and /api/auth/google returns 503 with message.
- Missing token →
401 Authentication required - Expired →
401 TOKEN_EXPIRED→ auto-refresh - Invalid →
401 TOKEN_INVALID - Locked →
423 Account locked... - Rate-limit →
429 Too many requests - Weak password →
400 Password must contain uppercase, ... - Duplicate email →
409 Email already registered - OAuth user password login →
400 Please login with google/github OAuth
- Removed
VITE_SUPABASE_URL/VITE_SUPABASE_ANON_KEYdirect usage src/lib/supabase.jsnow shim;ResumeUploadScreen&JobSourceScreennow useapi+ backend endpointssupabase/functions kept for reference but not used; backend re-implementslive-searchandsend-job-emailsasPOST /api/jobs/live-searchandPOST /api/profile/send-job-emails(demo stubs, integrate JSearch/Adzuna/Nodemailer)- Jobs/profile data now per-user (
user: ObjectIdfilter, JWT scoped)
client: npm run build✓ (1.3M chunk, 2297 modules)server: GET /api/health,GET /api/auth/health✓POST /api/auth/register→201+ JWT, weak password →400, duplicate →409✓POST /api/auth/login→200+ httpOnly cookies, wrong pass →401with remaining attempts, 5th fail → lock423✓GET /api/auth/mewith Bearer →200, without →401✓POST /api/auth/refreshvia cookie → new accessToken ✓GET /api/jobs&GET /api/profile/dashboardprotected, per-user isolation ✓- Frontend routes protected, OAuth callback handling ✓
server/.env.example includes MONGO_URI, JWT_ACCESS_SECRET, JWT_REFRESH_SECRET, CLIENT_URL, GOOGLE_*, GITHUB_*, MAX_LOGIN_ATTEMPTS, LOCK_TIME_MS
client/.env.example includes VITE_API_URL
Never commit real .env; rotate JWT_*_SECRET (≥32 chars) for production, set NODE_ENV=production + secure: true cookies.
- Implement real live-search provider (JSearch/Adzuna) behind
POST /api/jobs/live-search - Add email
nodemailer+ SMTP inPOST /api/profile/send-job-emails - Add refresh token rotation + DB denylist, email verification, 2FA for stricter auth
- Add frontend
ForgotPasswordflow (backend already prepared for extension)