[阶段1.1-1.7] 实现完整的认证系统
后端实现: - 创建 AuthService 包含密码加密、JWT 生成和验证 - 创建 authMiddleware 用于 API 路由保护 - 实现 auth 路由 (register, login, logout, /me) 前端实现: - 创建 Login 页面 (登录/注册标签页) - 创建 ProtectedRoute 组件用于路由保护 - 更新 App.tsx 集成路由保护 - 前端 API 客户端已包含认证方法和拦截器 验收标准已满足: - 用户可以注册和登录 - JWT Token 正确生成和验证 - 受保护的路由需要有效 Token - 未认证用户重定向到登录页面 Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -11,22 +11,23 @@
|
||||
"lint": "eslint src --ext .ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"axios": "^1.5.0",
|
||||
"bcryptjs": "^3.0.3",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"express": "^4.18.2",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"mysql2": "^3.6.0",
|
||||
"sqlite3": "^5.1.6",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"axios": "^1.5.0",
|
||||
"ts-node": "^10.9.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/cors": "^2.8.13",
|
||||
"@types/node": "^20.3.1",
|
||||
"@types/express": "^4.17.17",
|
||||
"@types/jsonwebtoken": "^9.0.2",
|
||||
"@types/node": "^20.3.1",
|
||||
"@types/sqlite3": "^3.1.8",
|
||||
"typescript": "^5.1.3",
|
||||
"tsx": "^3.12.7"
|
||||
"tsx": "^3.12.7",
|
||||
"typescript": "^5.1.3"
|
||||
}
|
||||
}
|
||||
|
||||
65
server/src/middleware/authMiddleware.ts
Normal file
65
server/src/middleware/authMiddleware.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import { Request, Response, NextFunction } from 'express';
|
||||
import { verifyToken } from '../services/AuthService';
|
||||
import { AppError } from './errorHandler';
|
||||
|
||||
export interface AuthRequest extends Request {
|
||||
userId?: string;
|
||||
userEmail?: string;
|
||||
}
|
||||
|
||||
export function authMiddleware(req: AuthRequest, res: Response, next: NextFunction) {
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (!authHeader) {
|
||||
return res.status(401).json({
|
||||
error: {
|
||||
status: 401,
|
||||
message: 'Missing authorization header',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const parts = authHeader.split(' ');
|
||||
if (parts.length !== 2 || parts[0] !== 'Bearer') {
|
||||
return res.status(401).json({
|
||||
error: {
|
||||
status: 401,
|
||||
message: 'Invalid authorization header format',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const token = parts[1];
|
||||
|
||||
try {
|
||||
const payload = verifyToken(token);
|
||||
req.userId = payload.userId;
|
||||
next();
|
||||
} catch (error: any) {
|
||||
return res.status(401).json({
|
||||
error: {
|
||||
status: 401,
|
||||
message: 'Invalid or expired token',
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function optionalAuthMiddleware(req: AuthRequest, res: Response, next: NextFunction) {
|
||||
const authHeader = req.headers.authorization;
|
||||
|
||||
if (authHeader) {
|
||||
const parts = authHeader.split(' ');
|
||||
if (parts.length === 2 && parts[0] === 'Bearer') {
|
||||
const token = parts[1];
|
||||
try {
|
||||
const payload = verifyToken(token);
|
||||
req.userId = payload.userId;
|
||||
} catch (error) {
|
||||
// Silently fail - continue without authentication
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
next();
|
||||
}
|
||||
@@ -1,18 +1,154 @@
|
||||
import express from 'express';
|
||||
import { register, login, logout, getUserById } from '../services/AuthService';
|
||||
import { authMiddleware, AuthRequest } from '../middleware/authMiddleware';
|
||||
|
||||
const router = express.Router();
|
||||
|
||||
// TODO: Implement authentication routes
|
||||
router.post('/login', (req, res) => {
|
||||
res.json({ message: 'Login endpoint' });
|
||||
/**
|
||||
* POST /api/auth/register
|
||||
* Register a new user
|
||||
*/
|
||||
router.post('/register', async (req, res, next) => {
|
||||
try {
|
||||
const { email, garminEmail, garminPassword } = req.body;
|
||||
|
||||
if (!email || !garminEmail || !garminPassword) {
|
||||
return res.status(400).json({
|
||||
error: {
|
||||
status: 400,
|
||||
message: 'Missing required fields: email, garminEmail, garminPassword',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const result = await register({ email, garminEmail, garminPassword });
|
||||
|
||||
res.status(201).json({
|
||||
success: true,
|
||||
data: {
|
||||
userId: result.id,
|
||||
email: result.email,
|
||||
token: result.token,
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
if (error.code === 'EMAIL_TAKEN') {
|
||||
return res.status(409).json({
|
||||
error: {
|
||||
status: 409,
|
||||
message: error.message,
|
||||
},
|
||||
});
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/logout', (req, res) => {
|
||||
res.json({ message: 'Logout endpoint' });
|
||||
/**
|
||||
* POST /api/auth/login
|
||||
* Login with email and password
|
||||
*/
|
||||
router.post('/login', async (req, res, next) => {
|
||||
try {
|
||||
const { email, password } = req.body;
|
||||
|
||||
if (!email || !password) {
|
||||
return res.status(400).json({
|
||||
error: {
|
||||
status: 400,
|
||||
message: 'Missing required fields: email, password',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const result = await login(email, password);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
userId: result.id,
|
||||
email: result.email,
|
||||
token: result.token,
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
if (error.code === 'INVALID_CREDENTIALS') {
|
||||
return res.status(401).json({
|
||||
error: {
|
||||
status: 401,
|
||||
message: error.message,
|
||||
},
|
||||
});
|
||||
}
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
router.post('/refresh', (req, res) => {
|
||||
res.json({ message: 'Refresh token endpoint' });
|
||||
/**
|
||||
* POST /api/auth/logout
|
||||
* Logout (requires authentication)
|
||||
*/
|
||||
router.post('/logout', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const userId = req.userId;
|
||||
if (!userId) {
|
||||
return res.status(401).json({
|
||||
error: {
|
||||
status: 401,
|
||||
message: 'User not authenticated',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
await logout(userId);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: 'Logged out successfully',
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* GET /api/auth/me
|
||||
* Get current user info (requires authentication)
|
||||
*/
|
||||
router.get('/me', authMiddleware, async (req: AuthRequest, res, next) => {
|
||||
try {
|
||||
const userId = req.userId;
|
||||
if (!userId) {
|
||||
return res.status(401).json({
|
||||
error: {
|
||||
status: 401,
|
||||
message: 'User not authenticated',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
const user = await getUserById(userId);
|
||||
if (!user) {
|
||||
return res.status(404).json({
|
||||
error: {
|
||||
status: 404,
|
||||
message: 'User not found',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
id: user.id,
|
||||
email: user.email,
|
||||
garminEmail: user.garmin_email,
|
||||
createdAt: user.created_at,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
});
|
||||
|
||||
export default router;
|
||||
|
||||
Reference in New Issue
Block a user