Add JWT local auth with protected API routes
- User model with bcrypt password hashing - Register, login, logout, and /me endpoints - authenticate middleware applied to all API routes - JWT_SECRET configurable via environment variable Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
87e4147e8c
commit
42f0587cf6
5 changed files with 107 additions and 16 deletions
|
|
@ -36,6 +36,7 @@ services:
|
||||||
- PORT=8080
|
- PORT=8080
|
||||||
- CLIENT_URL=http://nginx
|
- CLIENT_URL=http://nginx
|
||||||
- MONGODB_URI=mongodb://mongodb:27017/etsy-tracker
|
- MONGODB_URI=mongodb://mongodb:27017/etsy-tracker
|
||||||
|
- JWT_SECRET=${JWT_SECRET:-changeme}
|
||||||
volumes:
|
volumes:
|
||||||
- etsy_data:/app/data
|
- etsy_data:/app/data
|
||||||
- etsy_uploads:/app/uploads
|
- etsy_uploads:/app/uploads
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ import orderRoutes from './routes/orders';
|
||||||
import customerRoutes from './routes/customers';
|
import customerRoutes from './routes/customers';
|
||||||
import expenseRoutes from './routes/expenses';
|
import expenseRoutes from './routes/expenses';
|
||||||
import analyticsRoutes from './routes/analytics';
|
import analyticsRoutes from './routes/analytics';
|
||||||
|
import { authenticate } from './middleware/authenticate';
|
||||||
|
|
||||||
// Load environment variables
|
// Load environment variables
|
||||||
dotenv.config();
|
dotenv.config();
|
||||||
|
|
@ -60,11 +61,11 @@ mongoose.connect(process.env.MONGODB_URI || 'mongodb://localhost:27017/etsy-trac
|
||||||
|
|
||||||
// Routes
|
// Routes
|
||||||
app.use('/api/auth', authRoutes);
|
app.use('/api/auth', authRoutes);
|
||||||
app.use('/api/products', productRoutes);
|
app.use('/api/products', authenticate, productRoutes);
|
||||||
app.use('/api/orders', orderRoutes);
|
app.use('/api/orders', authenticate, orderRoutes);
|
||||||
app.use('/api/customers', customerRoutes);
|
app.use('/api/customers', authenticate, customerRoutes);
|
||||||
app.use('/api/expenses', expenseRoutes);
|
app.use('/api/expenses', authenticate, expenseRoutes);
|
||||||
app.use('/api/analytics', analyticsRoutes);
|
app.use('/api/analytics', authenticate, analyticsRoutes);
|
||||||
|
|
||||||
// Health check endpoint
|
// Health check endpoint
|
||||||
app.get('/api/health', (req, res) => {
|
app.get('/api/health', (req, res) => {
|
||||||
|
|
|
||||||
22
server/src/middleware/authenticate.ts
Normal file
22
server/src/middleware/authenticate.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
||||||
|
import { Request, Response, NextFunction } from 'express';
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
|
||||||
|
export interface AuthRequest extends Request {
|
||||||
|
userId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function authenticate(req: AuthRequest, res: Response, next: NextFunction) {
|
||||||
|
const authHeader = req.headers.authorization;
|
||||||
|
if (!authHeader?.startsWith('Bearer ')) {
|
||||||
|
return res.status(401).json({ message: 'No token provided' });
|
||||||
|
}
|
||||||
|
|
||||||
|
const token = authHeader.slice(7);
|
||||||
|
try {
|
||||||
|
const payload = jwt.verify(token, process.env.JWT_SECRET || 'changeme') as { userId: string };
|
||||||
|
req.userId = payload.userId;
|
||||||
|
next();
|
||||||
|
} catch {
|
||||||
|
res.status(401).json({ message: 'Invalid or expired token' });
|
||||||
|
}
|
||||||
|
}
|
||||||
27
server/src/models/User.ts
Normal file
27
server/src/models/User.ts
Normal file
|
|
@ -0,0 +1,27 @@
|
||||||
|
import mongoose, { Document, Schema } from 'mongoose';
|
||||||
|
import bcrypt from 'bcryptjs';
|
||||||
|
|
||||||
|
export interface IUser extends Document {
|
||||||
|
name: string;
|
||||||
|
email: string;
|
||||||
|
password: string;
|
||||||
|
comparePassword(candidate: string): Promise<boolean>;
|
||||||
|
}
|
||||||
|
|
||||||
|
const UserSchema: Schema = new Schema({
|
||||||
|
name: { type: String, required: true, trim: true },
|
||||||
|
email: { type: String, required: true, unique: true, trim: true, lowercase: true },
|
||||||
|
password: { type: String, required: true },
|
||||||
|
}, { timestamps: true });
|
||||||
|
|
||||||
|
UserSchema.pre('save', async function (next) {
|
||||||
|
if (!this.isModified('password')) return next();
|
||||||
|
this.password = await bcrypt.hash(this.password as string, 12);
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
|
||||||
|
UserSchema.methods.comparePassword = function (candidate: string): Promise<boolean> {
|
||||||
|
return bcrypt.compare(candidate, this.password);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default mongoose.model<IUser>('User', UserSchema);
|
||||||
|
|
@ -1,20 +1,60 @@
|
||||||
import { Router } from 'express';
|
import { Router, Request, Response } from 'express';
|
||||||
|
import jwt from 'jsonwebtoken';
|
||||||
|
import User from '../models/User';
|
||||||
|
import { authenticate, AuthRequest } from '../middleware/authenticate';
|
||||||
|
|
||||||
const router = Router();
|
const router = Router();
|
||||||
|
const JWT_SECRET = process.env.JWT_SECRET || 'changeme';
|
||||||
|
const JWT_EXPIRES_IN = '7d';
|
||||||
|
|
||||||
// POST /api/auth/login
|
router.post('/register', async (req: Request, res: Response) => {
|
||||||
router.post('/login', (req, res) => {
|
try {
|
||||||
res.json({ message: 'Login endpoint - coming soon!' });
|
const { name, email, password } = req.body;
|
||||||
|
if (!name || !email || !password) {
|
||||||
|
return res.status(400).json({ message: 'Name, email and password are required' });
|
||||||
|
}
|
||||||
|
const existing = await User.findOne({ email });
|
||||||
|
if (existing) {
|
||||||
|
return res.status(409).json({ message: 'Email already in use' });
|
||||||
|
}
|
||||||
|
const user = new User({ name, email, password });
|
||||||
|
await user.save();
|
||||||
|
const token = jwt.sign({ userId: user._id }, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
|
||||||
|
res.status(201).json({ token, user: { id: user._id, name: user.name, email: user.email } });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ message: 'Registration failed', error: err });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// POST /api/auth/register
|
router.post('/login', async (req: Request, res: Response) => {
|
||||||
router.post('/register', (req, res) => {
|
try {
|
||||||
res.json({ message: 'Register endpoint - coming soon!' });
|
const { email, password } = req.body;
|
||||||
|
if (!email || !password) {
|
||||||
|
return res.status(400).json({ message: 'Email and password are required' });
|
||||||
|
}
|
||||||
|
const user = await User.findOne({ email });
|
||||||
|
if (!user || !(await user.comparePassword(password))) {
|
||||||
|
return res.status(401).json({ message: 'Invalid email or password' });
|
||||||
|
}
|
||||||
|
const token = jwt.sign({ userId: user._id }, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
|
||||||
|
res.json({ token, user: { id: user._id, name: user.name, email: user.email } });
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ message: 'Login failed', error: err });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// POST /api/auth/logout
|
router.get('/me', authenticate, async (req: AuthRequest, res: Response) => {
|
||||||
router.post('/logout', (req, res) => {
|
try {
|
||||||
res.json({ message: 'Logout endpoint - coming soon!' });
|
const user = await User.findById(req.userId).select('-password');
|
||||||
|
if (!user) return res.status(404).json({ message: 'User not found' });
|
||||||
|
res.json(user);
|
||||||
|
} catch (err) {
|
||||||
|
res.status(500).json({ message: 'Failed to fetch user', error: err });
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
export default router;
|
router.post('/logout', (_req, res) => {
|
||||||
|
res.json({ message: 'Logged out' });
|
||||||
|
});
|
||||||
|
|
||||||
|
export default router;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue