jwt-auth.js
3.07 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
/**
* JWT Authentication Middleware
*
* - Signs JWT with { userId, email, role }
* - Stores in httpOnly cookie "lemon_token" (7-day expiry)
* - requireAuth: blocks unauthenticated requests
* - optionalAuth: attaches user if present, doesn't block
*/
const jwt = require('jsonwebtoken');
const userDb = require('../services/user-db');
const JWT_SECRET = process.env.JWT_SECRET || 'lemon-dev-secret-change-in-production';
const COOKIE_NAME = 'lemon_token';
const TOKEN_EXPIRY = '7d';
/**
* Sign a JWT and set it as httpOnly cookie
*/
function signAndSetCookie(res, user) {
const payload = {
userId: user.id,
email: user.email,
role: user.role || 'user',
};
const token = jwt.sign(payload, JWT_SECRET, { expiresIn: TOKEN_EXPIRY });
res.cookie(COOKIE_NAME, token, {
httpOnly: true,
secure: process.env.NODE_ENV === 'production',
sameSite: 'lax',
maxAge: 7 * 24 * 60 * 60 * 1000, // 7 days
path: '/',
});
return token;
}
/**
* Clear the auth cookie
*/
function clearCookie(res) {
res.clearCookie(COOKIE_NAME, { path: '/' });
}
/**
* Extract and verify JWT from cookie or Authorization header
*/
function extractUser(req) {
let token = null;
// Cookie first
if (req.cookies && req.cookies[COOKIE_NAME]) {
token = req.cookies[COOKIE_NAME];
}
// Authorization: Bearer <token> fallback
if (!token) {
const auth = req.headers['authorization'];
if (auth && auth.startsWith('Bearer ')) {
token = auth.slice(7).trim();
}
}
if (!token) return null;
try {
const decoded = jwt.verify(token, JWT_SECRET);
const user = userDb.getUserById(decoded.userId);
if (!user) return null;
return userDb.toPublicUser(user);
} catch (err) {
return null;
}
}
/**
* Middleware: require authentication (401 if not logged in)
*/
function requireAuth(req, res, next) {
const user = extractUser(req);
if (!user) {
return res.status(401).json({
error: 'Unauthorized',
message: 'Please log in to access this resource.',
});
}
req.user = user;
next();
}
/**
* Middleware: optional authentication (attach user if present)
*/
function optionalAuth(req, res, next) {
req.user = extractUser(req) || null;
next();
}
/**
* Middleware: require admin role (403 if not admin)
*/
function requireAdmin(req, res, next) {
const user = extractUser(req);
if (!user) {
return res.status(401).json({ error: 'Unauthorized' });
}
if (user.role !== 'admin') {
return res.status(403).json({ error: 'Forbidden', message: 'Admin access required.' });
}
req.user = user;
next();
}
/**
* 直接从 token 字符串提取用户(供 WebSocket handshake 使用)
*/
function extractUserFromToken(token) {
if (!token) return null;
try {
const decoded = jwt.verify(token, JWT_SECRET);
const user = userDb.getUserById(decoded.userId);
return user ? userDb.toPublicUser(user) : null;
} catch {
return null;
}
}
module.exports = {
signAndSetCookie,
clearCookie,
requireAuth,
requireAdmin,
optionalAuth,
extractUser,
extractUserFromToken,
JWT_SECRET,
COOKIE_NAME,
};