-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathserver.js
More file actions
188 lines (158 loc) · 5.89 KB
/
Copy pathserver.js
File metadata and controls
188 lines (158 loc) · 5.89 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
/*
Secure Express API Starter Kit
File: server.js
Features included:
- Express server with Helmet security headers
- Rate limiting (express-rate-limit)
- Input validation and sanitization (express-validator)
- Password hashing (bcrypt)
- JWT authentication (jsonwebtoken)
- Request logging (morgan + winston)
- Centralized error handling
- Example routes: /health, /register, /login, /profile (protected)
- Minimal in-memory user store for demo (replace with DB in prod)
Usage:
1) Create a project folder and save this file as `server.js`.
2) Create a `.env` file with these vars (example below).
3) Install dependencies:
npm init -y
npm install express helmet express-rate-limit express-validator bcrypt jsonwebtoken dotenv cors morgan winston
4) Run:
node server.js
.env example:
PORT=3000
JWT_SECRET=your-very-secret-key
JWT_EXPIRES_IN=1h
RATE_LIMIT_WINDOW_MS=900000
RATE_LIMIT_MAX=100
IMPORTANT: This starter kit is for educational/dev use. Replace the in-memory user store with a database,
use HTTPS, rotate secrets, and follow production best practices before deploying.
*/
require('dotenv').config();
const express = require('express');
const helmet = require('helmet');
const rateLimit = require('express-rate-limit');
const { body, validationResult } = require('express-validator');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const cors = require('cors');
const morgan = require('morgan');
const winston = require('winston');
// ---- Configuration ----
const PORT = process.env.PORT || 3000;
const JWT_SECRET = process.env.JWT_SECRET || 'dev_secret_change_me';
const JWT_EXPIRES_IN = process.env.JWT_EXPIRES_IN || '1h';
// Rate limiter configuration (can be tuned via env)
const limiter = rateLimit({
windowMs: Number(process.env.RATE_LIMIT_WINDOW_MS) || 15 * 60 * 1000, // 15 minutes
max: Number(process.env.RATE_LIMIT_MAX) || 100, // limit each IP
standardHeaders: true,
legacyHeaders: false,
});
// ---- Simple logger (winston + morgan) ----
const logger = winston.createLogger({
level: 'info',
format: winston.format.combine(
winston.format.timestamp(),
winston.format.json()
),
transports: [
new winston.transports.Console(),
new winston.transports.File({ filename: 'logs/app.log' }),
],
});
// Morgan to use winston
const stream = {
write: (message) => logger.info(message.trim()),
};
// ---- In-memory user store (for demo) ----
// Replace with a DB (Postgres, MongoDB, etc.) and proper email uniqueness checks in production.
const users = new Map(); // key: email, value: { id, email, passwordHash }
let nextUserId = 1;
// ---- Helpers ----
function generateJwt(payload) {
return jwt.sign(payload, JWT_SECRET, { expiresIn: JWT_EXPIRES_IN });
}
function authenticateToken(req, res, next) {
const authHeader = req.headers['authorization'];
const token = authHeader && authHeader.split(' ')[1];
if (!token) return res.status(401).json({ error: 'Unauthorized' });
jwt.verify(token, JWT_SECRET, (err, user) => {
if (err) return res.status(403).json({ error: 'Token invalid or expired' });
req.user = user;
next();
});
}
// ---- App setup ----
const app = express();
app.use(helmet());
app.use(cors({ origin: true }));
app.use(express.json());
app.use(limiter);
app.use(morgan('combined', { stream }));
// ---- Routes ----
app.get('/health', (req, res) => res.json({ status: 'ok', time: new Date().toISOString() }));
// Registration
app.post('/register',
// Validation
body('email').isEmail().withMessage('Valid email required').normalizeEmail(),
body('password')
.isLength({ min: 8 }).withMessage('Password must be at least 8 characters')
.matches(/[0-9]/).withMessage('Password must contain a number')
.matches(/[A-Z]/).withMessage('Password must contain an uppercase letter')
.trim(),
async (req, res, next) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() });
const { email, password } = req.body;
if (users.has(email)) return res.status(409).json({ error: 'User already exists' });
const saltRounds = 12; // increase for production if CPU allows
const passwordHash = await bcrypt.hash(password, saltRounds);
const user = { id: nextUserId++, email, passwordHash };
users.set(email, user);
logger.info('User registered', { email, id: user.id });
res.status(201).json({ message: 'User created', id: user.id });
} catch (err) {
next(err);
}
}
);
// Login
app.post('/login',
body('email').isEmail().normalizeEmail(),
body('password').exists(),
async (req, res, next) => {
try {
const errors = validationResult(req);
if (!errors.isEmpty()) return res.status(400).json({ errors: errors.array() });
const { email, password } = req.body;
const user = users.get(email);
if (!user) return res.status(401).json({ error: 'Invalid credentials' });
const match = await bcrypt.compare(password, user.passwordHash);
if (!match) return res.status(401).json({ error: 'Invalid credentials' });
const token = generateJwt({ id: user.id, email: user.email });
logger.info('User logged in', { email, id: user.id });
res.json({ token, expiresIn: JWT_EXPIRES_IN });
} catch (err) {
next(err);
}
}
);
// Protected profile route
app.get('/profile', authenticateToken, (req, res) => {
const user = users.get(req.user.email);
if (!user) return res.status(404).json({ error: 'User not found' });
// Return safe profile info
res.json({ id: user.id, email: user.email });
});
// ---- Centralized error handler ----
app.use((err, req, res, next) => {
logger.error('Unhandled error', { message: err.message, stack: err.stack });
res.status(500).json({ error: 'Internal Server Error' });
});
// ---- Start server ----
app.listen(PORT, () => {
logger.info(`Server running on port ${PORT}`);
console.log(`Server running on port ${PORT}`);
});