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

@@ -1,6 +1,7 @@
import axios, { AxiosInstance } from 'axios';
const API_BASE_URL = process.env.REACT_APP_API_URL || 'http://localhost:5000/api';
const TOKEN_KEY = 'ghl_token';
class ApiClient {
private client: AxiosInstance;
@@ -12,9 +13,34 @@ class ApiClient {
'Content-Type': 'application/json',
},
});
// Attach the saved JWT to every request.
this.client.interceptors.request.use((config) => {
const token = localStorage.getItem(TOKEN_KEY);
if (token) {
config.headers = config.headers || {};
config.headers.Authorization = `Bearer ${token}`;
}
return config;
});
// On 401, drop the stored session so the UI can redirect to login.
this.client.interceptors.response.use(
(resp) => resp,
(error) => {
if (error.response?.status === 401) {
localStorage.removeItem(TOKEN_KEY);
}
return Promise.reject(error);
}
);
}
// --- Auth ---
register(email: string, garminEmail: string, garminPassword: string) {
return this.client.post('/auth/register', { email, garminEmail, garminPassword });
}
// Auth endpoints
login(email: string, password: string) {
return this.client.post('/auth/login', { email, password });
}
@@ -23,50 +49,55 @@ class ApiClient {
return this.client.post('/auth/logout');
}
// Garmin endpoints
syncGarminData() {
return this.client.post('/garmin/sync');
refresh() {
return this.client.post('/auth/refresh');
}
// Persist the JWT returned by register/login.
setSession(token: string) {
localStorage.setItem(TOKEN_KEY, token);
}
clearSession() {
localStorage.removeItem(TOKEN_KEY);
}
// --- Garmin ---
syncGarminData(garminEmail?: string, garminPassword?: string) {
const body =
garminEmail || garminPassword ? { garminEmail, garminPassword } : {};
return this.client.post('/garmin/sync', body);
}
getGarminSyncStatus() {
return this.client.get('/garmin/status');
}
// Health endpoints
// --- Health ---
getHealthSummary(startDate?: string, endDate?: string) {
return this.client.get('/health/summary', {
params: { startDate, endDate }
});
return this.client.get('/health/summary', { params: { startDate, endDate } });
}
getStepsData(startDate?: string, endDate?: string) {
return this.client.get('/health/steps', {
params: { startDate, endDate }
});
return this.client.get('/health/steps', { params: { startDate, endDate } });
}
getHeartRateData(startDate?: string, endDate?: string) {
return this.client.get('/health/heart-rate', {
params: { startDate, endDate }
});
return this.client.get('/health/heart-rate', { params: { startDate, endDate } });
}
getSleepData(startDate?: string, endDate?: string) {
return this.client.get('/health/sleep', {
params: { startDate, endDate }
});
return this.client.get('/health/sleep', { params: { startDate, endDate } });
}
getActivities(startDate?: string, endDate?: string) {
return this.client.get('/health/activities', {
params: { startDate, endDate }
});
return this.client.get('/health/activities', { params: { startDate, endDate } });
}
// Analysis endpoints
getTrends(metricType?: string) {
// --- Analysis ---
getTrends(metricType?: string, startDate?: string, endDate?: string) {
return this.client.get('/analysis/trends', {
params: { metricType }
params: { metricType, startDate, endDate },
});
}