[阶段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:
ericwyuan
2026-08-23 12:25:33 +08:00
parent 6b05d04773
commit 3b2d0697f0
28 changed files with 1970 additions and 75 deletions

View 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();
}

View File

@@ -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;