-
Notifications
You must be signed in to change notification settings - Fork 4
Userauth part6 api token #39
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: userauth
Are you sure you want to change the base?
Changes from all commits
ae38bce
215d8e2
ed26223
efc52dc
e2827f1
3796592
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| /** | ||
| * Auth Controller | ||
| * Handles HTTP request/response for authentication endpoints. | ||
| * Validates input, delegates to authService, and formats responses. | ||
| */ | ||
|
|
||
| import { asyncHandler, APIError } from '../middleware/errorHandler.js'; | ||
| import * as authService from '../services/authService.js'; | ||
|
|
||
| /** | ||
| * POST /api/v1/auth/register | ||
| * Create a new user account. | ||
| * Body: { email, password, name? } | ||
| */ | ||
| export const register = asyncHandler(async (req, res) => { | ||
| const { email, password, name } = req.body; | ||
|
|
||
| // Input validation | ||
| if (!email || !password) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Malformed registration bodies return 500 instead of Prompt for AI agents |
||
| throw new APIError('Email and password are required', 400, 'VALIDATION_ERROR'); | ||
| } | ||
|
|
||
| // Basic email format check | ||
| const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; | ||
| if (!emailRegex.test(email)) { | ||
| throw new APIError('Invalid email format', 400, 'VALIDATION_ERROR'); | ||
| } | ||
|
|
||
| if (password.length < 8) { | ||
| throw new APIError('Password must be at least 8 characters', 400, 'VALIDATION_ERROR'); | ||
| } | ||
|
|
||
| if (password.length > 128) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Password length validation allows up to 128 characters, but bcrypt silently truncates at 72 bytes. A password between 73–128 characters passes validation, yet only the first 72 bytes are hashed and verified. Consider capping the password at 72 characters or adding a warning so users aren't misled about the effective strength of longer passwords. Prompt for AI agents |
||
| throw new APIError('Password must be at most 128 characters', 400, 'VALIDATION_ERROR'); | ||
| } | ||
|
|
||
| try { | ||
| const result = await authService.registerUser(email, password, name); | ||
| res.status(201).json({ success: true, data: result }); | ||
| } catch (err) { | ||
| if (err.statusCode === 409) { | ||
| throw new APIError(err.message, 409, err.code || 'EMAIL_EXISTS'); | ||
| } | ||
| throw err; | ||
| } | ||
| }); | ||
|
|
||
| /** | ||
| * POST /api/v1/auth/login | ||
| * Authenticate with email and password. | ||
| * Body: { email, password } | ||
| */ | ||
| export const login = asyncHandler(async (req, res) => { | ||
| const { email, password } = req.body; | ||
|
|
||
| if (!email || !password) { | ||
| throw new APIError('Email and password are required', 400, 'VALIDATION_ERROR'); | ||
| } | ||
|
|
||
| try { | ||
| const result = await authService.loginUser(email, password); | ||
| res.json({ success: true, data: result }); | ||
| } catch (err) { | ||
| if (err.statusCode === 401) { | ||
| throw new APIError(err.message, 401, err.code || 'INVALID_CREDENTIALS'); | ||
| } | ||
| throw err; | ||
| } | ||
| }); | ||
|
|
||
| /** | ||
| * GET /api/v1/auth/me | ||
| * Get the current user's profile. | ||
| * Requires: Authorization: Bearer <token> | ||
| */ | ||
| export const me = asyncHandler(async (req, res) => { | ||
| const user = await authService.getUserById(req.user.id); | ||
|
|
||
| if (!user) { | ||
| throw new APIError('User not found', 404, 'NOT_FOUND'); | ||
| } | ||
|
|
||
| res.json({ success: true, data: { user } }); | ||
| }); | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,59 @@ | ||
| /** | ||
| * JWT Authentication Middleware | ||
| * Extracts and verifies JWT from the Authorization header. | ||
| * Sets req.user = { id, email } on success. | ||
| */ | ||
|
|
||
| import jwt from 'jsonwebtoken'; | ||
| import config from '../config/index.js'; | ||
| import { APIError } from './errorHandler.js'; | ||
|
|
||
| /** | ||
| * requireAuth — blocks unauthenticated requests with 401. | ||
| * Expects header: Authorization: Bearer <token> | ||
| */ | ||
| export const requireAuth = (req, _res, next) => { | ||
| const authHeader = req.headers.authorization; | ||
|
|
||
| if (!authHeader || !authHeader.startsWith('Bearer ')) { | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: Guest-capable routes silently ignore valid JWTs when clients use a lowercase Prompt for AI agents |
||
| throw new APIError('Authentication required', 401, 'UNAUTHORIZED'); | ||
| } | ||
|
|
||
| const token = authHeader.split(' ')[1]; | ||
|
|
||
| try { | ||
| const decoded = jwt.verify(token, config.auth.jwtSecret); | ||
| req.user = { id: decoded.sub, email: decoded.email }; | ||
| next(); | ||
| } catch (err) { | ||
| if (err.name === 'TokenExpiredError') { | ||
| throw new APIError('Token expired — please log in again', 401, 'TOKEN_EXPIRED'); | ||
| } | ||
| throw new APIError('Invalid token', 401, 'INVALID_TOKEN'); | ||
| } | ||
| }; | ||
|
|
||
| /** | ||
| * optionalAuth — attaches user if a valid token is present, but does NOT block. | ||
| * Useful for routes that work for both guests and logged-in users. | ||
| * Sets req.user = null when no token or invalid token. | ||
| */ | ||
| export const optionalAuth = (req, _res, next) => { | ||
| const authHeader = req.headers.authorization; | ||
|
|
||
| if (!authHeader || !authHeader.startsWith('Bearer ')) { | ||
| req.user = null; | ||
| return next(); | ||
| } | ||
|
|
||
| const token = authHeader.split(' ')[1]; | ||
|
|
||
| try { | ||
| const decoded = jwt.verify(token, config.auth.jwtSecret); | ||
| req.user = { id: decoded.sub, email: decoded.email }; | ||
| } catch { | ||
| req.user = null; | ||
| } | ||
|
|
||
| next(); | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -73,8 +73,21 @@ export const ttsLimiter = rateLimit({ | |
| handler: createLimitHandler('tts'), | ||
| }); | ||
|
|
||
| /** | ||
| * Strict limiter for authentication endpoints | ||
| * Protects against brute-force attacks | ||
| */ | ||
| export const authLimiter = rateLimit({ | ||
| windowMs: 15 * 60 * 1000, // 15 minutes | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P3: Backend rate-limit documentation now omits the auth tier and incorrectly describes all tiers as one-minute windows. Update the README with the 20-per-15-minute authentication limit. Prompt for AI agents |
||
| max: 20, // Limit each IP to 20 requests per window | ||
| standardHeaders: true, | ||
| legacyHeaders: false, | ||
| handler: createLimitHandler('auth'), | ||
| }); | ||
|
|
||
| export default { | ||
| generalLimiter, | ||
| aiLimiter, | ||
| ttsLimiter, | ||
| authLimiter, | ||
| }; | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| /** | ||
| * Auth Routes | ||
| * Handles user registration, login, and profile endpoints. | ||
| * | ||
| * POST /register — Create a new account (open registration) | ||
| * POST /login — Authenticate and receive JWT | ||
| * GET /me — Get current user profile (requires auth) | ||
| */ | ||
|
|
||
| import { Router } from 'express'; | ||
| import * as authController from '../controllers/authController.js'; | ||
| import { requireAuth } from '../middleware/auth.js'; | ||
| import { authLimiter } from '../middleware/rateLimiter.js'; | ||
|
|
||
| const router = Router(); | ||
|
|
||
| // Apply strict rate limiting to auth routes | ||
| router.use(authLimiter); | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. P2: Prompt for AI agents |
||
|
|
||
| // Public routes | ||
| router.post('/register', authController.register); | ||
| router.post('/login', authController.login); | ||
|
|
||
| // Protected routes | ||
| router.get('/me', requireAuth, authController.me); | ||
|
|
||
| export default router; | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
P3: Placeholder value
your-jwt-secret-min-32-charsis 28 characters, but its name suggests a minimum of 32. A user who copies this file literally might not notice the discrepancy. Either pad the placeholder to 32+ characters (e.g.your-jwt-secret-min-32-chars-here) or rename it to match.Prompt for AI agents