[阶段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:
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