diff --git a/README.md b/README.md index a8c39cf..75ad08f 100644 --- a/README.md +++ b/README.md @@ -134,7 +134,8 @@ python tests/smoke.py ## 📚 API 文档 ### 认证 -- `POST /api/auth/login` - 用户登录 +- `POST /api/auth/register` - 用户注册(email, garminEmail, garminPassword) +- `POST /api/auth/login` - 用户登录(email, password) - `POST /api/auth/logout` - 用户登出 - `POST /api/auth/refresh` - 刷新 Token diff --git a/package-lock.json b/package-lock.json index 70a0612..530b120 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5690,6 +5690,15 @@ "node": ">= 8" } }, + "node_modules/app-root-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/app-root-path/-/app-root-path-3.1.0.tgz", + "integrity": "sha512-biN3PwB2gUtjaYy/isrU3aNWI5w+fAfvHkSvCKeQGxhmYpwKFUxudR3Yya+KqVRHBmEDYh+/lTozYCFbmzX4nA==", + "license": "MIT", + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/aproba": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.1.0.tgz", @@ -7429,6 +7438,13 @@ "node": ">= 8" } }, + "node_modules/crypto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/crypto/-/crypto-1.0.1.tgz", + "integrity": "sha512-VxBKmeNcqQdiUQUW2Tzq0t377b54N2bMtXO/qiLa+6eRRmmC4qT3D4OnTGoT/U6O9aklQ/jTwbOtRMTTY8G0Ig==", + "deprecated": "This package is no longer supported. It's now a built-in Node module. If you've depended on crypto, you should switch to the one that's built-in.", + "license": "ISC" + }, "node_modules/crypto-random-string": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-2.0.0.tgz", @@ -10346,6 +10362,22 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/garmin-connect": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/garmin-connect/-/garmin-connect-1.6.2.tgz", + "integrity": "sha512-8QGeB3ZQTfaG1UZe1hd3CEOsaAozC1quG+CjX8yV0P6x+AyyWz4jqgIJ3MEK2NhDsxlXsF4jBOdpjUQubzmdFQ==", + "license": "MIT", + "dependencies": { + "app-root-path": "^3.1.0", + "axios": "^1.5.1", + "crypto": "^1.0.1", + "form-data": "^4.0.0", + "lodash": "^4.17.21", + "luxon": "^3.4.3", + "oauth-1.0a": "^2.2.6", + "qs": "^6.11.2" + } + }, "node_modules/garmin-health-lab-client": { "resolved": "client", "link": true @@ -13665,6 +13697,15 @@ "url": "https://github.com/sponsors/wellwelwel" } }, + "node_modules/luxon": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/luxon/-/luxon-3.7.2.tgz", + "integrity": "sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew==", + "license": "MIT", + "engines": { + "node": ">=12" + } + }, "node_modules/magic-string": { "version": "0.25.9", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.25.9.tgz", @@ -14452,6 +14493,12 @@ "dev": true, "license": "MIT" }, + "node_modules/oauth-1.0a": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/oauth-1.0a/-/oauth-1.0a-2.2.6.tgz", + "integrity": "sha512-6bkxv3N4Gu5lty4viIcIAnq5GbxECviMBeKR3WX/q87SPQ8E8aursPZUtsXDnxCs787af09WPRBLqYrf/lwoYQ==", + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -21599,6 +21646,7 @@ "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.18.2", + "garmin-connect": "^1.6.2", "jsonwebtoken": "^9.0.2", "mysql2": "^3.6.0", "sqlite3": "^5.1.6", diff --git a/server/package.json b/server/package.json index 3967708..2e5e479 100644 --- a/server/package.json +++ b/server/package.json @@ -16,6 +16,7 @@ "cors": "^2.8.5", "dotenv": "^16.3.1", "express": "^4.18.2", + "garmin-connect": "^1.6.2", "jsonwebtoken": "^9.0.2", "mysql2": "^3.6.0", "sqlite3": "^5.1.6", diff --git a/server/src/routes/garmin.ts b/server/src/routes/garmin.ts index 06d9c7f..f2d37b4 100644 --- a/server/src/routes/garmin.ts +++ b/server/src/routes/garmin.ts @@ -1,14 +1,76 @@ import express from 'express'; +import { syncData, getSyncStatus } from '../services/GarminService'; +import { getUserById } from '../services/AuthService'; +import { authMiddleware, AuthRequest } from '../middleware/authMiddleware'; const router = express.Router(); -// TODO: Implement Garmin API integration -router.post('/sync', (req, res) => { - res.json({ message: 'Garmin sync endpoint' }); +/** + * POST /api/garmin/sync + * Trigger Garmin data synchronization (requires authentication) + */ +router.post('/sync', 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', + }, + }); + } + + // Sync Garmin data using stored credentials + const result = await syncData(userId, { + garminEmail: user.garminEmail, + garminPassword: req.body.garminPassword || '', + }); + + res.json({ + success: result.status === 'success', + data: result, + }); + } catch (error) { + next(error); + } }); -router.get('/status', (req, res) => { - res.json({ message: 'Garmin sync status endpoint' }); +/** + * GET /api/garmin/status + * Get Garmin synchronization status (requires authentication) + */ +router.get('/status', 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 status = await getSyncStatus(userId); + + res.json({ + success: true, + data: status, + }); + } catch (error) { + next(error); + } }); export default router; diff --git a/server/src/routes/health.ts b/server/src/routes/health.ts index 9a21227..a64e9f8 100644 --- a/server/src/routes/health.ts +++ b/server/src/routes/health.ts @@ -1,26 +1,170 @@ import express from 'express'; +import { getSummary, getSteps, getHeartRate, getSleep, getActivities } from '../services/HealthService'; +import { authMiddleware, AuthRequest } from '../middleware/authMiddleware'; const router = express.Router(); -// TODO: Implement health data endpoints -router.get('/summary', (req, res) => { - res.json({ message: 'Health summary endpoint' }); +// Apply authentication middleware to all health routes +router.use(authMiddleware); + +/** + * GET /api/health/summary + * Get health data summary for a date range + * Query params: startDate, endDate (ISO 8601 format, e.g., 2024-08-23) + */ +router.get('/summary', 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 { startDate, endDate } = req.query; + const data = await getSummary(userId, { + startDate: startDate as string | undefined, + endDate: endDate as string | undefined, + }); + + res.json({ + success: true, + data, + }); + } catch (error) { + next(error); + } }); -router.get('/steps', (req, res) => { - res.json({ message: 'Steps data endpoint' }); +/** + * GET /api/health/steps + * Get steps data for a date range + * Query params: startDate, endDate + */ +router.get('/steps', 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 { startDate, endDate } = req.query; + const data = await getSteps(userId, { + startDate: startDate as string | undefined, + endDate: endDate as string | undefined, + }); + + res.json({ + success: true, + data, + }); + } catch (error) { + next(error); + } }); -router.get('/heart-rate', (req, res) => { - res.json({ message: 'Heart rate data endpoint' }); +/** + * GET /api/health/heart-rate + * Get heart rate data for a date range + * Query params: startDate, endDate + */ +router.get('/heart-rate', 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 { startDate, endDate } = req.query; + const data = await getHeartRate(userId, { + startDate: startDate as string | undefined, + endDate: endDate as string | undefined, + }); + + res.json({ + success: true, + data, + }); + } catch (error) { + next(error); + } }); -router.get('/sleep', (req, res) => { - res.json({ message: 'Sleep data endpoint' }); +/** + * GET /api/health/sleep + * Get sleep data for a date range + * Query params: startDate, endDate + */ +router.get('/sleep', 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 { startDate, endDate } = req.query; + const data = await getSleep(userId, { + startDate: startDate as string | undefined, + endDate: endDate as string | undefined, + }); + + res.json({ + success: true, + data, + }); + } catch (error) { + next(error); + } }); -router.get('/activities', (req, res) => { - res.json({ message: 'Activities endpoint' }); +/** + * GET /api/health/activities + * Get activities for a date range + * Query params: startDate, endDate + */ +router.get('/activities', 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 { startDate, endDate } = req.query; + const data = await getActivities(userId, { + startDate: startDate as string | undefined, + endDate: endDate as string | undefined, + }); + + res.json({ + success: true, + data, + }); + } catch (error) { + next(error); + } }); export default router;