first commit
This commit is contained in:
@@ -0,0 +1,29 @@
|
||||
# Server Configuration
|
||||
PORT=5000
|
||||
NODE_ENV=development
|
||||
|
||||
# Database
|
||||
DATABASE_PATH=./database/footprint.db
|
||||
|
||||
# JWT Secret
|
||||
JWT_SECRET=your-super-secret-jwt-key-change-this-in-production
|
||||
|
||||
# Session Secret
|
||||
SESSION_SECRET=your-session-secret-change-this-in-production
|
||||
|
||||
# OAuth2 - Google
|
||||
GOOGLE_CLIENT_ID=your-google-client-id
|
||||
GOOGLE_CLIENT_SECRET=your-google-client-secret
|
||||
GOOGLE_CALLBACK_URL=http://localhost:5000/auth/google/callback
|
||||
|
||||
# OAuth2 - GitHub
|
||||
GITHUB_CLIENT_ID=your-github-client-id
|
||||
GITHUB_CLIENT_SECRET=your-github-client-secret
|
||||
GITHUB_CALLBACK_URL=http://localhost:5000/auth/github/callback
|
||||
|
||||
# Frontend URL
|
||||
FRONTEND_URL=http://localhost:3000
|
||||
|
||||
# API Rate Limiting
|
||||
RATE_LIMIT_WINDOW_MS=900000
|
||||
RATE_LIMIT_MAX_REQUESTS=100
|
||||
@@ -0,0 +1,22 @@
|
||||
# 后端 Dockerfile
|
||||
FROM node:18-alpine
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
# 复制package文件
|
||||
COPY package*.json ./
|
||||
|
||||
# 安装依赖
|
||||
RUN npm ci --only=production
|
||||
|
||||
# 复制源代码
|
||||
COPY src ./src
|
||||
|
||||
# 创建数据库目录
|
||||
RUN mkdir -p /app/database
|
||||
|
||||
# 暴露端口
|
||||
EXPOSE 5000
|
||||
|
||||
# 启动应用
|
||||
CMD ["node", "src/index.js"]
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Generated
+2490
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "footprint-backend",
|
||||
"version": "1.0.0",
|
||||
"description": "Footprint check-in system backend API",
|
||||
"main": "src/index.js",
|
||||
"scripts": {
|
||||
"start": "node src/index.js",
|
||||
"dev": "nodemon src/index.js",
|
||||
"init-db": "node src/config/initDb.js"
|
||||
},
|
||||
"keywords": ["footprint", "check-in", "geolocation"],
|
||||
"author": "",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"express": "^4.18.2",
|
||||
"cors": "^2.8.5",
|
||||
"dotenv": "^16.3.1",
|
||||
"better-sqlite3": "^9.2.2",
|
||||
"passport": "^0.7.0",
|
||||
"passport-google-oauth20": "^2.0.0",
|
||||
"passport-github2": "^0.1.12",
|
||||
"express-session": "^1.17.3",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"bcrypt": "^5.1.1",
|
||||
"express-rate-limit": "^7.1.5",
|
||||
"helmet": "^7.1.0",
|
||||
"morgan": "^1.10.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"nodemon": "^3.0.2"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
const Database = require('better-sqlite3');
|
||||
const path = require('path');
|
||||
require('dotenv').config();
|
||||
|
||||
const dbPath = process.env.DATABASE_PATH || path.join(__dirname, '../../database/footprint.db');
|
||||
|
||||
// 创建数据库连接
|
||||
const db = new Database(dbPath);
|
||||
db.pragma('journal_mode = WAL');
|
||||
|
||||
// 初始化数据库表
|
||||
const initDb = () => {
|
||||
// 用户表
|
||||
const createUsersTable = `
|
||||
CREATE TABLE IF NOT EXISTS users (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
oauth_provider TEXT NOT NULL,
|
||||
oauth_id TEXT NOT NULL,
|
||||
email TEXT,
|
||||
username TEXT NOT NULL,
|
||||
avatar_url TEXT,
|
||||
user_type TEXT DEFAULT 'basic' CHECK(user_type IN ('basic', 'premium')),
|
||||
api_key TEXT UNIQUE,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
UNIQUE(oauth_provider, oauth_id)
|
||||
)
|
||||
`;
|
||||
|
||||
// 打卡记录表
|
||||
const createCheckinsTable = `
|
||||
CREATE TABLE IF NOT EXISTS checkins (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
latitude REAL NOT NULL,
|
||||
longitude REAL NOT NULL,
|
||||
location_name TEXT,
|
||||
address TEXT,
|
||||
mood TEXT,
|
||||
note TEXT,
|
||||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
|
||||
)
|
||||
`;
|
||||
|
||||
// 创建索引以提高查询性能
|
||||
const createIndexes = `
|
||||
CREATE INDEX IF NOT EXISTS idx_checkins_user_id ON checkins(user_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_checkins_created_at ON checkins(created_at);
|
||||
CREATE INDEX IF NOT EXISTS idx_users_api_key ON users(api_key);
|
||||
`;
|
||||
|
||||
try {
|
||||
db.exec(createUsersTable);
|
||||
db.exec(createCheckinsTable);
|
||||
db.exec(createIndexes);
|
||||
console.log('✅ Database initialized successfully');
|
||||
} catch (error) {
|
||||
console.error('❌ Error initializing database:', error);
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
// 如果直接运行此文件,则初始化数据库
|
||||
if (require.main === module) {
|
||||
initDb();
|
||||
db.close();
|
||||
}
|
||||
|
||||
module.exports = { db, initDb };
|
||||
@@ -0,0 +1,100 @@
|
||||
const passport = require('passport');
|
||||
const GoogleStrategy = require('passport-google-oauth20').Strategy;
|
||||
const GitHubStrategy = require('passport-github2').Strategy;
|
||||
const { db } = require('./initDb');
|
||||
const crypto = require('crypto');
|
||||
|
||||
// 序列化用户
|
||||
passport.serializeUser((user, done) => {
|
||||
done(null, user.id);
|
||||
});
|
||||
|
||||
// 反序列化用户
|
||||
passport.deserializeUser((id, done) => {
|
||||
try {
|
||||
const user = db.prepare('SELECT * FROM users WHERE id = ?').get(id);
|
||||
done(null, user);
|
||||
} catch (error) {
|
||||
done(error, null);
|
||||
}
|
||||
});
|
||||
|
||||
// Google OAuth Strategy
|
||||
if (process.env.GOOGLE_CLIENT_ID && process.env.GOOGLE_CLIENT_SECRET) {
|
||||
passport.use(
|
||||
new GoogleStrategy(
|
||||
{
|
||||
clientID: process.env.GOOGLE_CLIENT_ID,
|
||||
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
|
||||
callbackURL: process.env.GOOGLE_CALLBACK_URL,
|
||||
},
|
||||
(accessToken, refreshToken, profile, done) => {
|
||||
try {
|
||||
let user = db
|
||||
.prepare('SELECT * FROM users WHERE oauth_provider = ? AND oauth_id = ?')
|
||||
.get('google', profile.id);
|
||||
|
||||
if (!user) {
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO users (oauth_provider, oauth_id, email, username, avatar_url)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`);
|
||||
const result = stmt.run(
|
||||
'google',
|
||||
profile.id,
|
||||
profile.emails?.[0]?.value,
|
||||
profile.displayName,
|
||||
profile.photos?.[0]?.value
|
||||
);
|
||||
user = db.prepare('SELECT * FROM users WHERE id = ?').get(result.lastInsertRowid);
|
||||
}
|
||||
|
||||
return done(null, user);
|
||||
} catch (error) {
|
||||
return done(error, null);
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
// GitHub OAuth Strategy
|
||||
if (process.env.GITHUB_CLIENT_ID && process.env.GITHUB_CLIENT_SECRET) {
|
||||
passport.use(
|
||||
new GitHubStrategy(
|
||||
{
|
||||
clientID: process.env.GITHUB_CLIENT_ID,
|
||||
clientSecret: process.env.GITHUB_CLIENT_SECRET,
|
||||
callbackURL: process.env.GITHUB_CALLBACK_URL,
|
||||
},
|
||||
(accessToken, refreshToken, profile, done) => {
|
||||
try {
|
||||
let user = db
|
||||
.prepare('SELECT * FROM users WHERE oauth_provider = ? AND oauth_id = ?')
|
||||
.get('github', profile.id);
|
||||
|
||||
if (!user) {
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO users (oauth_provider, oauth_id, email, username, avatar_url)
|
||||
VALUES (?, ?, ?, ?, ?)
|
||||
`);
|
||||
const result = stmt.run(
|
||||
'github',
|
||||
profile.id,
|
||||
profile.emails?.[0]?.value,
|
||||
profile.username || profile.displayName,
|
||||
profile.photos?.[0]?.value
|
||||
);
|
||||
user = db.prepare('SELECT * FROM users WHERE id = ?').get(result.lastInsertRowid);
|
||||
}
|
||||
|
||||
return done(null, user);
|
||||
} catch (error) {
|
||||
return done(error, null);
|
||||
}
|
||||
}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
module.exports = passport;
|
||||
@@ -0,0 +1,149 @@
|
||||
const { db } = require('../config/initDb');
|
||||
|
||||
// 获取所有打卡记录(API方式,使用API Key认证)
|
||||
const getAllCheckinsApi = (req, res) => {
|
||||
const userId = req.user.id;
|
||||
const { format = 'json', start_date, end_date, limit = 1000 } = req.query;
|
||||
|
||||
try {
|
||||
let query = 'SELECT * FROM checkins WHERE user_id = ?';
|
||||
const params = [userId];
|
||||
|
||||
// 添加日期过滤
|
||||
if (start_date) {
|
||||
query += ' AND created_at >= ?';
|
||||
params.push(start_date);
|
||||
}
|
||||
if (end_date) {
|
||||
query += ' AND created_at <= ?';
|
||||
params.push(end_date);
|
||||
}
|
||||
|
||||
query += ' ORDER BY created_at DESC LIMIT ?';
|
||||
params.push(parseInt(limit));
|
||||
|
||||
const checkins = db.prepare(query).all(...params);
|
||||
|
||||
// 根据格式返回数据
|
||||
if (format === 'csv') {
|
||||
return res.type('text/csv').send(convertToCSV(checkins));
|
||||
} else if (format === 'geojson') {
|
||||
return res.json(convertToGeoJSON(checkins));
|
||||
} else {
|
||||
return res.json({
|
||||
success: true,
|
||||
data: checkins,
|
||||
count: checkins.length,
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error fetching check-ins via API:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch check-ins' });
|
||||
}
|
||||
};
|
||||
|
||||
// 转换为CSV格式
|
||||
const convertToCSV = (checkins) => {
|
||||
if (checkins.length === 0) {
|
||||
return 'id,user_id,latitude,longitude,location_name,address,mood,note,created_at\n';
|
||||
}
|
||||
|
||||
const headers = Object.keys(checkins[0]).join(',');
|
||||
const rows = checkins.map((checkin) =>
|
||||
Object.values(checkin)
|
||||
.map((val) => (val === null ? '' : `"${val}"`))
|
||||
.join(',')
|
||||
);
|
||||
|
||||
return headers + '\n' + rows.join('\n');
|
||||
};
|
||||
|
||||
// 转换为GeoJSON格式
|
||||
const convertToGeoJSON = (checkins) => {
|
||||
return {
|
||||
type: 'FeatureCollection',
|
||||
features: checkins.map((checkin) => ({
|
||||
type: 'Feature',
|
||||
geometry: {
|
||||
type: 'Point',
|
||||
coordinates: [checkin.longitude, checkin.latitude],
|
||||
},
|
||||
properties: {
|
||||
id: checkin.id,
|
||||
location_name: checkin.location_name,
|
||||
address: checkin.address,
|
||||
mood: checkin.mood,
|
||||
note: checkin.note,
|
||||
created_at: checkin.created_at,
|
||||
},
|
||||
})),
|
||||
};
|
||||
};
|
||||
|
||||
// 获取统计信息(API方式)
|
||||
const getStatsApi = (req, res) => {
|
||||
const userId = req.user.id;
|
||||
|
||||
try {
|
||||
// 基础统计
|
||||
const basicStats = db
|
||||
.prepare(
|
||||
`
|
||||
SELECT
|
||||
COUNT(*) as total_checkins,
|
||||
COUNT(DISTINCT DATE(created_at)) as total_days,
|
||||
MIN(created_at) as first_checkin,
|
||||
MAX(created_at) as latest_checkin
|
||||
FROM checkins
|
||||
WHERE user_id = ?
|
||||
`
|
||||
)
|
||||
.get(userId);
|
||||
|
||||
// 按心情统计
|
||||
const moodStats = db
|
||||
.prepare(
|
||||
`
|
||||
SELECT mood, COUNT(*) as count
|
||||
FROM checkins
|
||||
WHERE user_id = ? AND mood IS NOT NULL
|
||||
GROUP BY mood
|
||||
ORDER BY count DESC
|
||||
`
|
||||
)
|
||||
.all(userId);
|
||||
|
||||
// 按月份统计
|
||||
const monthlyStats = db
|
||||
.prepare(
|
||||
`
|
||||
SELECT
|
||||
strftime('%Y-%m', created_at) as month,
|
||||
COUNT(*) as count
|
||||
FROM checkins
|
||||
WHERE user_id = ?
|
||||
GROUP BY month
|
||||
ORDER BY month DESC
|
||||
LIMIT 12
|
||||
`
|
||||
)
|
||||
.all(userId);
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: {
|
||||
basic: basicStats,
|
||||
by_mood: moodStats,
|
||||
by_month: monthlyStats,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching stats via API:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch statistics' });
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getAllCheckinsApi,
|
||||
getStatsApi,
|
||||
};
|
||||
@@ -0,0 +1,203 @@
|
||||
const { db } = require('../config/initDb');
|
||||
|
||||
// 创建打卡记录
|
||||
const createCheckin = (req, res) => {
|
||||
const { latitude, longitude, location_name, address, mood, note } = req.body;
|
||||
const userId = req.user.id;
|
||||
|
||||
// 验证必填字段
|
||||
if (!latitude || !longitude) {
|
||||
return res.status(400).json({ error: 'Latitude and longitude are required' });
|
||||
}
|
||||
|
||||
// 验证坐标范围
|
||||
if (latitude < -90 || latitude > 90 || longitude < -180 || longitude > 180) {
|
||||
return res.status(400).json({ error: 'Invalid coordinates' });
|
||||
}
|
||||
|
||||
try {
|
||||
const stmt = db.prepare(`
|
||||
INSERT INTO checkins (user_id, latitude, longitude, location_name, address, mood, note)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?)
|
||||
`);
|
||||
|
||||
const result = stmt.run(userId, latitude, longitude, location_name, address, mood, note);
|
||||
|
||||
const checkin = db
|
||||
.prepare('SELECT * FROM checkins WHERE id = ?')
|
||||
.get(result.lastInsertRowid);
|
||||
|
||||
res.status(201).json({
|
||||
message: 'Check-in created successfully',
|
||||
checkin,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error creating check-in:', error);
|
||||
res.status(500).json({ error: 'Failed to create check-in' });
|
||||
}
|
||||
};
|
||||
|
||||
// 获取用户的所有打卡记录
|
||||
const getUserCheckins = (req, res) => {
|
||||
const userId = req.user.id;
|
||||
const { limit = 50, offset = 0, start_date, end_date } = req.query;
|
||||
|
||||
try {
|
||||
let query = 'SELECT * FROM checkins WHERE user_id = ?';
|
||||
const params = [userId];
|
||||
|
||||
// 添加日期过滤
|
||||
if (start_date) {
|
||||
query += ' AND created_at >= ?';
|
||||
params.push(start_date);
|
||||
}
|
||||
if (end_date) {
|
||||
query += ' AND created_at <= ?';
|
||||
params.push(end_date);
|
||||
}
|
||||
|
||||
query += ' ORDER BY created_at DESC LIMIT ? OFFSET ?';
|
||||
params.push(parseInt(limit), parseInt(offset));
|
||||
|
||||
const checkins = db.prepare(query).all(...params);
|
||||
|
||||
// 获取总数
|
||||
let countQuery = 'SELECT COUNT(*) as total FROM checkins WHERE user_id = ?';
|
||||
const countParams = [userId];
|
||||
if (start_date) {
|
||||
countQuery += ' AND created_at >= ?';
|
||||
countParams.push(start_date);
|
||||
}
|
||||
if (end_date) {
|
||||
countQuery += ' AND created_at <= ?';
|
||||
countParams.push(end_date);
|
||||
}
|
||||
|
||||
const { total } = db.prepare(countQuery).get(...countParams);
|
||||
|
||||
res.json({
|
||||
checkins,
|
||||
pagination: {
|
||||
total,
|
||||
limit: parseInt(limit),
|
||||
offset: parseInt(offset),
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error fetching check-ins:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch check-ins' });
|
||||
}
|
||||
};
|
||||
|
||||
// 获取单个打卡记录
|
||||
const getCheckinById = (req, res) => {
|
||||
const { id } = req.params;
|
||||
const userId = req.user.id;
|
||||
|
||||
try {
|
||||
const checkin = db
|
||||
.prepare('SELECT * FROM checkins WHERE id = ? AND user_id = ?')
|
||||
.get(id, userId);
|
||||
|
||||
if (!checkin) {
|
||||
return res.status(404).json({ error: 'Check-in not found' });
|
||||
}
|
||||
|
||||
res.json({ checkin });
|
||||
} catch (error) {
|
||||
console.error('Error fetching check-in:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch check-in' });
|
||||
}
|
||||
};
|
||||
|
||||
// 删除打卡记录
|
||||
const deleteCheckin = (req, res) => {
|
||||
const { id } = req.params;
|
||||
const userId = req.user.id;
|
||||
|
||||
try {
|
||||
const result = db.prepare('DELETE FROM checkins WHERE id = ? AND user_id = ?').run(id, userId);
|
||||
|
||||
if (result.changes === 0) {
|
||||
return res.status(404).json({ error: 'Check-in not found' });
|
||||
}
|
||||
|
||||
res.json({ message: 'Check-in deleted successfully' });
|
||||
} catch (error) {
|
||||
console.error('Error deleting check-in:', error);
|
||||
res.status(500).json({ error: 'Failed to delete check-in' });
|
||||
}
|
||||
};
|
||||
|
||||
// 更新打卡记录
|
||||
const updateCheckin = (req, res) => {
|
||||
const { id } = req.params;
|
||||
const userId = req.user.id;
|
||||
const { mood, note, location_name } = req.body;
|
||||
|
||||
try {
|
||||
// 先检查记录是否存在
|
||||
const checkin = db
|
||||
.prepare('SELECT * FROM checkins WHERE id = ? AND user_id = ?')
|
||||
.get(id, userId);
|
||||
|
||||
if (!checkin) {
|
||||
return res.status(404).json({ error: 'Check-in not found' });
|
||||
}
|
||||
|
||||
const stmt = db.prepare(`
|
||||
UPDATE checkins
|
||||
SET mood = COALESCE(?, mood),
|
||||
note = COALESCE(?, note),
|
||||
location_name = COALESCE(?, location_name)
|
||||
WHERE id = ? AND user_id = ?
|
||||
`);
|
||||
|
||||
stmt.run(mood, note, location_name, id, userId);
|
||||
|
||||
const updated = db.prepare('SELECT * FROM checkins WHERE id = ?').get(id);
|
||||
|
||||
res.json({
|
||||
message: 'Check-in updated successfully',
|
||||
checkin: updated,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error updating check-in:', error);
|
||||
res.status(500).json({ error: 'Failed to update check-in' });
|
||||
}
|
||||
};
|
||||
|
||||
// 获取打卡统计信息
|
||||
const getCheckinStats = (req, res) => {
|
||||
const userId = req.user.id;
|
||||
|
||||
try {
|
||||
const stats = db
|
||||
.prepare(
|
||||
`
|
||||
SELECT
|
||||
COUNT(*) as total_checkins,
|
||||
COUNT(DISTINCT DATE(created_at)) as total_days,
|
||||
MIN(created_at) as first_checkin,
|
||||
MAX(created_at) as latest_checkin
|
||||
FROM checkins
|
||||
WHERE user_id = ?
|
||||
`
|
||||
)
|
||||
.get(userId);
|
||||
|
||||
res.json({ stats });
|
||||
} catch (error) {
|
||||
console.error('Error fetching stats:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch statistics' });
|
||||
}
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
createCheckin,
|
||||
getUserCheckins,
|
||||
getCheckinById,
|
||||
deleteCheckin,
|
||||
updateCheckin,
|
||||
getCheckinStats,
|
||||
};
|
||||
@@ -0,0 +1,107 @@
|
||||
const { db } = require('../config/initDb');
|
||||
const crypto = require('crypto');
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
// 获取当前用户信息
|
||||
const getCurrentUser = (req, res) => {
|
||||
try {
|
||||
const user = db.prepare('SELECT id, username, email, avatar_url, user_type, api_key, created_at FROM users WHERE id = ?').get(req.user.id);
|
||||
|
||||
if (!user) {
|
||||
return res.status(404).json({ error: 'User not found' });
|
||||
}
|
||||
|
||||
res.json({ user });
|
||||
} catch (error) {
|
||||
console.error('Error fetching user:', error);
|
||||
res.status(500).json({ error: 'Failed to fetch user information' });
|
||||
}
|
||||
};
|
||||
|
||||
// 升级为高级用户
|
||||
const upgradeToPremium = (req, res) => {
|
||||
const userId = req.user.id;
|
||||
|
||||
try {
|
||||
// 生成API密钥
|
||||
const apiKey = 'fp_' + crypto.randomBytes(32).toString('hex');
|
||||
|
||||
const stmt = db.prepare(`
|
||||
UPDATE users
|
||||
SET user_type = 'premium', api_key = ?
|
||||
WHERE id = ?
|
||||
`);
|
||||
|
||||
stmt.run(apiKey, userId);
|
||||
|
||||
const updatedUser = db
|
||||
.prepare('SELECT id, username, email, avatar_url, user_type, api_key, created_at FROM users WHERE id = ?')
|
||||
.get(userId);
|
||||
|
||||
res.json({
|
||||
message: 'Successfully upgraded to premium',
|
||||
user: updatedUser,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error upgrading user:', error);
|
||||
res.status(500).json({ error: 'Failed to upgrade account' });
|
||||
}
|
||||
};
|
||||
|
||||
// 重新生成API密钥
|
||||
const regenerateApiKey = (req, res) => {
|
||||
const userId = req.user.id;
|
||||
|
||||
try {
|
||||
const user = db.prepare('SELECT user_type FROM users WHERE id = ?').get(userId);
|
||||
|
||||
if (user.user_type !== 'premium') {
|
||||
return res.status(403).json({ error: 'Premium account required' });
|
||||
}
|
||||
|
||||
const apiKey = 'fp_' + crypto.randomBytes(32).toString('hex');
|
||||
|
||||
db.prepare('UPDATE users SET api_key = ? WHERE id = ?').run(apiKey, userId);
|
||||
|
||||
res.json({
|
||||
message: 'API key regenerated successfully',
|
||||
api_key: apiKey,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error regenerating API key:', error);
|
||||
res.status(500).json({ error: 'Failed to regenerate API key' });
|
||||
}
|
||||
};
|
||||
|
||||
// 生成JWT Token(用于前端API调用)
|
||||
const generateToken = (req, res) => {
|
||||
const user = req.user;
|
||||
|
||||
const token = jwt.sign(
|
||||
{
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
user_type: user.user_type,
|
||||
},
|
||||
process.env.JWT_SECRET,
|
||||
{ expiresIn: '7d' }
|
||||
);
|
||||
|
||||
res.json({
|
||||
token,
|
||||
user: {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
avatar_url: user.avatar_url,
|
||||
user_type: user.user_type,
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
getCurrentUser,
|
||||
upgradeToPremium,
|
||||
regenerateApiKey,
|
||||
generateToken,
|
||||
};
|
||||
@@ -0,0 +1,100 @@
|
||||
require('dotenv').config();
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const helmet = require('helmet');
|
||||
const morgan = require('morgan');
|
||||
const session = require('express-session');
|
||||
const rateLimit = require('express-rate-limit');
|
||||
const passport = require('./config/passport');
|
||||
const { initDb } = require('./config/initDb');
|
||||
|
||||
// 初始化数据库
|
||||
initDb();
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 5000;
|
||||
|
||||
// 中间件
|
||||
app.use(helmet());
|
||||
app.use(morgan('combined'));
|
||||
app.use(
|
||||
cors({
|
||||
origin: process.env.FRONTEND_URL || 'http://localhost:3000',
|
||||
credentials: true,
|
||||
})
|
||||
);
|
||||
app.use(express.json());
|
||||
app.use(express.urlencoded({ extended: true }));
|
||||
|
||||
// Session配置
|
||||
app.use(
|
||||
session({
|
||||
secret: process.env.SESSION_SECRET || 'your-session-secret',
|
||||
resave: false,
|
||||
saveUninitialized: false,
|
||||
cookie: {
|
||||
secure: process.env.NODE_ENV === 'production',
|
||||
maxAge: 24 * 60 * 60 * 1000, // 24 hours
|
||||
},
|
||||
})
|
||||
);
|
||||
|
||||
// Passport初始化
|
||||
app.use(passport.initialize());
|
||||
app.use(passport.session());
|
||||
|
||||
// 速率限制
|
||||
const limiter = rateLimit({
|
||||
windowMs: parseInt(process.env.RATE_LIMIT_WINDOW_MS) || 15 * 60 * 1000,
|
||||
max: parseInt(process.env.RATE_LIMIT_MAX_REQUESTS) || 100,
|
||||
message: 'Too many requests from this IP, please try again later.',
|
||||
});
|
||||
app.use('/api/', limiter);
|
||||
|
||||
// 路由
|
||||
app.use('/auth', require('./routes/auth'));
|
||||
app.use('/api/checkins', require('./routes/checkins'));
|
||||
app.use('/api/users', require('./routes/users'));
|
||||
app.use('/api/v1', require('./routes/api')); // 高级用户API
|
||||
|
||||
// 健康检查
|
||||
app.get('/health', (req, res) => {
|
||||
res.json({ status: 'ok', timestamp: new Date().toISOString() });
|
||||
});
|
||||
|
||||
// 根路由
|
||||
app.get('/', (req, res) => {
|
||||
res.json({
|
||||
message: 'Footprint API Server',
|
||||
version: '1.0.0',
|
||||
endpoints: {
|
||||
auth: '/auth',
|
||||
checkins: '/api/checkins',
|
||||
users: '/api/users',
|
||||
premium_api: '/api/v1',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
// 404处理
|
||||
app.use((req, res) => {
|
||||
res.status(404).json({ error: 'Route not found' });
|
||||
});
|
||||
|
||||
// 错误处理
|
||||
app.use((err, req, res, next) => {
|
||||
console.error(err.stack);
|
||||
res.status(500).json({
|
||||
error: 'Something went wrong!',
|
||||
message: process.env.NODE_ENV === 'development' ? err.message : undefined,
|
||||
});
|
||||
});
|
||||
|
||||
// 启动服务器
|
||||
app.listen(PORT, () => {
|
||||
console.log(`✅ Server is running on port ${PORT}`);
|
||||
console.log(`📍 Environment: ${process.env.NODE_ENV || 'development'}`);
|
||||
console.log(`🔗 Frontend URL: ${process.env.FRONTEND_URL || 'http://localhost:3000'}`);
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
@@ -0,0 +1,74 @@
|
||||
const jwt = require('jsonwebtoken');
|
||||
const { db } = require('../config/initDb');
|
||||
|
||||
// 验证JWT Token
|
||||
const authenticateToken = (req, res, next) => {
|
||||
const authHeader = req.headers['authorization'];
|
||||
const token = authHeader && authHeader.split(' ')[1]; // Bearer TOKEN
|
||||
|
||||
if (!token) {
|
||||
return res.status(401).json({ error: 'Access token required' });
|
||||
}
|
||||
|
||||
jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
|
||||
if (err) {
|
||||
return res.status(403).json({ error: 'Invalid or expired token' });
|
||||
}
|
||||
req.user = user;
|
||||
next();
|
||||
});
|
||||
};
|
||||
|
||||
// 验证是否登录(使用session)
|
||||
const isAuthenticated = (req, res, next) => {
|
||||
if (req.isAuthenticated()) {
|
||||
return next();
|
||||
}
|
||||
res.status(401).json({ error: 'Not authenticated' });
|
||||
};
|
||||
|
||||
// 验证API Key(用于高级用户API访问)
|
||||
const authenticateApiKey = (req, res, next) => {
|
||||
const apiKey = req.headers['x-api-key'];
|
||||
|
||||
if (!apiKey) {
|
||||
return res.status(401).json({ error: 'API key required' });
|
||||
}
|
||||
|
||||
try {
|
||||
const user = db.prepare('SELECT * FROM users WHERE api_key = ?').get(apiKey);
|
||||
|
||||
if (!user) {
|
||||
return res.status(403).json({ error: 'Invalid API key' });
|
||||
}
|
||||
|
||||
if (user.user_type !== 'premium') {
|
||||
return res.status(403).json({ error: 'Premium account required for API access' });
|
||||
}
|
||||
|
||||
req.user = user;
|
||||
next();
|
||||
} catch (error) {
|
||||
res.status(500).json({ error: 'Internal server error' });
|
||||
}
|
||||
};
|
||||
|
||||
// 验证高级用户权限
|
||||
const isPremiumUser = (req, res, next) => {
|
||||
if (!req.user) {
|
||||
return res.status(401).json({ error: 'Authentication required' });
|
||||
}
|
||||
|
||||
if (req.user.user_type !== 'premium') {
|
||||
return res.status(403).json({ error: 'Premium account required' });
|
||||
}
|
||||
|
||||
next();
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
authenticateToken,
|
||||
isAuthenticated,
|
||||
authenticateApiKey,
|
||||
isPremiumUser,
|
||||
};
|
||||
@@ -0,0 +1,16 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const { getAllCheckinsApi, getStatsApi } = require('../controllers/apiController');
|
||||
const { authenticateApiKey } = require('../middleware/auth');
|
||||
|
||||
// 所有API路由都需要API Key认证
|
||||
router.use(authenticateApiKey);
|
||||
|
||||
// 获取所有打卡记录
|
||||
// 支持查询参数: format=json|csv|geojson, start_date, end_date, limit
|
||||
router.get('/checkins', getAllCheckinsApi);
|
||||
|
||||
// 获取统计信息
|
||||
router.get('/stats', getStatsApi);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,78 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const passport = require('../config/passport');
|
||||
const jwt = require('jsonwebtoken');
|
||||
|
||||
// Google OAuth
|
||||
router.get('/google', passport.authenticate('google', { scope: ['profile', 'email'] }));
|
||||
|
||||
router.get(
|
||||
'/google/callback',
|
||||
passport.authenticate('google', { failureRedirect: `${process.env.FRONTEND_URL}/login` }),
|
||||
(req, res) => {
|
||||
const token = jwt.sign(
|
||||
{
|
||||
id: req.user.id,
|
||||
username: req.user.username,
|
||||
user_type: req.user.user_type,
|
||||
},
|
||||
process.env.JWT_SECRET,
|
||||
{ expiresIn: '7d' }
|
||||
);
|
||||
|
||||
// 重定向到前端,并在URL中带上token
|
||||
res.redirect(`${process.env.FRONTEND_URL}/?token=${token}`);
|
||||
}
|
||||
);
|
||||
|
||||
// GitHub OAuth
|
||||
router.get('/github', passport.authenticate('github', { scope: ['user:email'] }));
|
||||
|
||||
router.get(
|
||||
'/github/callback',
|
||||
passport.authenticate('github', { failureRedirect: `${process.env.FRONTEND_URL}/login` }),
|
||||
(req, res) => {
|
||||
const token = jwt.sign(
|
||||
{
|
||||
id: req.user.id,
|
||||
username: req.user.username,
|
||||
user_type: req.user.user_type,
|
||||
},
|
||||
process.env.JWT_SECRET,
|
||||
{ expiresIn: '7d' }
|
||||
);
|
||||
|
||||
// 重定向到前端,并在URL中带上token
|
||||
res.redirect(`${process.env.FRONTEND_URL}/?token=${token}`);
|
||||
}
|
||||
);
|
||||
|
||||
// 登出
|
||||
router.post('/logout', (req, res) => {
|
||||
req.logout((err) => {
|
||||
if (err) {
|
||||
return res.status(500).json({ error: 'Failed to logout' });
|
||||
}
|
||||
res.json({ message: 'Logged out successfully' });
|
||||
});
|
||||
});
|
||||
|
||||
// 检查认证状态
|
||||
router.get('/status', (req, res) => {
|
||||
if (req.isAuthenticated()) {
|
||||
res.json({
|
||||
authenticated: true,
|
||||
user: {
|
||||
id: req.user.id,
|
||||
username: req.user.username,
|
||||
email: req.user.email,
|
||||
avatar_url: req.user.avatar_url,
|
||||
user_type: req.user.user_type,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
res.json({ authenticated: false });
|
||||
}
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,34 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const {
|
||||
createCheckin,
|
||||
getUserCheckins,
|
||||
getCheckinById,
|
||||
deleteCheckin,
|
||||
updateCheckin,
|
||||
getCheckinStats,
|
||||
} = require('../controllers/checkinController');
|
||||
const { authenticateToken } = require('../middleware/auth');
|
||||
|
||||
// 所有路由都需要认证
|
||||
router.use(authenticateToken);
|
||||
|
||||
// 创建打卡
|
||||
router.post('/', createCheckin);
|
||||
|
||||
// 获取用户所有打卡记录
|
||||
router.get('/', getUserCheckins);
|
||||
|
||||
// 获取统计信息
|
||||
router.get('/stats', getCheckinStats);
|
||||
|
||||
// 获取单个打卡记录
|
||||
router.get('/:id', getCheckinById);
|
||||
|
||||
// 更新打卡记录
|
||||
router.put('/:id', updateCheckin);
|
||||
|
||||
// 删除打卡记录
|
||||
router.delete('/:id', deleteCheckin);
|
||||
|
||||
module.exports = router;
|
||||
@@ -0,0 +1,19 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
const {
|
||||
getCurrentUser,
|
||||
upgradeToPremium,
|
||||
regenerateApiKey,
|
||||
} = require('../controllers/userController');
|
||||
const { authenticateToken, isPremiumUser } = require('../middleware/auth');
|
||||
|
||||
// 获取当前用户信息
|
||||
router.get('/me', authenticateToken, getCurrentUser);
|
||||
|
||||
// 升级为高级用户
|
||||
router.post('/upgrade', authenticateToken, upgradeToPremium);
|
||||
|
||||
// 重新生成API密钥(仅高级用户)
|
||||
router.post('/regenerate-api-key', authenticateToken, isPremiumUser, regenerateApiKey);
|
||||
|
||||
module.exports = router;
|
||||
Reference in New Issue
Block a user