116 lines
6.1 KiB
JavaScript
116 lines
6.1 KiB
JavaScript
const express = require('express');
|
||
const jwt = require('jsonwebtoken');
|
||
const db = require('../db');
|
||
const { SECRET } = require('../middleware/auth');
|
||
const { resolveAvatar } = require('../lib/avatar');
|
||
|
||
const router = express.Router();
|
||
|
||
// 可选鉴权:有效 token 解析出 req.user(用于论坛私密模式门禁判定是否登录),匿名放行
|
||
function optionalAuth(req, res, next) {
|
||
const header = req.headers.authorization;
|
||
if (header && header.startsWith('Bearer ')) {
|
||
try { req.user = jwt.verify(header.slice(7), SECRET); } catch { /* 无效 token 按匿名 */ }
|
||
}
|
||
next();
|
||
}
|
||
|
||
// 论坛私密模式门禁:forum_guest_visible='0' 且请求未登录 → 403
|
||
//(与 routes/forum.js requireGuestVisible 同策略,公开主页内容流同样受私密开关约束)
|
||
function requireForumVisible(req, res, next) {
|
||
if (db.getSetting('forum_guest_visible') === '1' || req.user) return next();
|
||
return res.status(403).json({ error: '论坛已设为私密' });
|
||
}
|
||
|
||
// 分页参数解析:page≥1 整数、pageSize 1-50,非法用默认 1/10
|
||
function parsePage(query) {
|
||
const page = parseInt(query.page);
|
||
const pageSize = parseInt(query.pageSize);
|
||
return {
|
||
page: Number.isInteger(page) && page >= 1 ? page : 1,
|
||
pageSize: Number.isInteger(pageSize) && pageSize >= 1 && pageSize <= 50 ? pageSize : 10,
|
||
};
|
||
}
|
||
|
||
// 列表内容净化:剥 [image:]/[file:] 标签与 [lock:] 块(防止锁块残留/锁定内容泄露),再截断
|
||
function sanitizeContent(c, maxLen) {
|
||
let s = String(c || '')
|
||
.replace(/\[(image|file):[^\]]*\]/g, '')
|
||
.replace(/\[lock(?::[^\]]*)?\][\s\S]*?\[\/lock\]/g, '')
|
||
.trim();
|
||
if (maxLen && s.length > maxLen) s = s.slice(0, maxLen) + '…';
|
||
return s;
|
||
}
|
||
|
||
// 公开个人主页:身份信息 + 统计(绝不返回 email/rainid_user_id/password)
|
||
// GET /api/users/:id
|
||
router.get('/:id', (req, res) => {
|
||
const uid = parseInt(req.params.id);
|
||
if (!Number.isInteger(uid) || uid < 1) return res.status(404).json({ error: '用户不存在' });
|
||
const user = db.get(
|
||
`SELECT id, username, role, avatar, bio, nickname, title, title_color, website, qq,
|
||
CASE WHEN qq IS NOT NULL AND trim(qq) <> '' THEN ''
|
||
WHEN email GLOB '[0-9]*@qq.com' THEN substr(email, 1, instr(email, '@') - 1)
|
||
ELSE '' END AS qq_from_email,
|
||
created_at, last_active_at
|
||
FROM users WHERE id = ?`, [uid]);
|
||
if (!user) return res.status(404).json({ error: '用户不存在' });
|
||
// 头像按优先级解析(自传 > QQ > qq_from_email > email 前缀 > RainID),兼容无 avatar 用户
|
||
user.avatar = resolveAvatar(user);
|
||
// 展示名:昵称优先,空则 username;QQ 号脱敏(前3后4,如 273****3776),空则省略
|
||
user.display_name = String(user.nickname || '').trim() || user.username || '';
|
||
if (user.qq && /^\d{5,12}$/.test(user.qq)) {
|
||
user.qq = user.qq.slice(0, 3) + '****' + user.qq.slice(-4);
|
||
} else {
|
||
delete user.qq; // 空则省略字段
|
||
}
|
||
delete user.qq_from_email; // 内部辅助列不外露
|
||
// 统计:论坛帖子 / 论坛回复 / 已发布博文 / 帖子获得的赞
|
||
// 注:post_likes 目前只记录博文点赞(blog.js /posts/:id/like),故按 blog_posts 聚合;
|
||
// post_id 与 forum_posts.id 存在冲突,不能 JOIN forum_posts(会错配作者)。
|
||
user.stats = {
|
||
posts: (db.get('SELECT COUNT(*) c FROM forum_posts WHERE author_id = ?', [uid]) || {}).c || 0,
|
||
replies: (db.get('SELECT COUNT(*) c FROM forum_replies WHERE author_id = ?', [uid]) || {}).c || 0,
|
||
articles: (db.get('SELECT COUNT(*) c FROM blog_posts WHERE author_id = ? AND published = 1', [uid]) || {}).c || 0,
|
||
likes: (db.get('SELECT COUNT(*) c FROM post_likes l JOIN blog_posts bp ON bp.id = l.post_id WHERE bp.author_id = ?', [uid]) || {}).c || 0,
|
||
};
|
||
res.json(user);
|
||
});
|
||
|
||
// 公开个人主页:论坛帖子流(标题/分类/回复数,不含正文,无锁泄露风险)
|
||
// GET /api/users/:id/posts?page=1&pageSize=10
|
||
router.get('/:id/posts', optionalAuth, requireForumVisible, (req, res) => {
|
||
const uid = parseInt(req.params.id);
|
||
if (!Number.isInteger(uid) || uid < 1) return res.status(404).json({ error: '用户不存在' });
|
||
if (!db.get('SELECT id FROM users WHERE id = ?', [uid])) return res.status(404).json({ error: '用户不存在' });
|
||
const { page, pageSize } = parsePage(req.query);
|
||
const total = (db.get('SELECT COUNT(*) c FROM forum_posts WHERE author_id = ?', [uid]) || {}).c || 0;
|
||
const list = db.all(
|
||
`SELECT fp.id, fp.title, fp.category_id, fc.name as category_name, fp.created_at, fp.is_pinned,
|
||
(SELECT COUNT(*) FROM forum_replies WHERE post_id = fp.id) as reply_count
|
||
FROM forum_posts fp LEFT JOIN forum_categories fc ON fp.category_id = fc.id
|
||
WHERE fp.author_id = ? ORDER BY fp.created_at DESC, fp.id DESC LIMIT ? OFFSET ?`,
|
||
[uid, pageSize, (page - 1) * pageSize]);
|
||
res.json({ list, total, page, pageSize, totalPages: Math.ceil(total / pageSize) });
|
||
});
|
||
|
||
// 公开个人主页:论坛回复流(content 净化 + 截断 120 字,附帖子标题)
|
||
// GET /api/users/:id/replies?page=1&pageSize=10
|
||
router.get('/:id/replies', optionalAuth, requireForumVisible, (req, res) => {
|
||
const uid = parseInt(req.params.id);
|
||
if (!Number.isInteger(uid) || uid < 1) return res.status(404).json({ error: '用户不存在' });
|
||
if (!db.get('SELECT id FROM users WHERE id = ?', [uid])) return res.status(404).json({ error: '用户不存在' });
|
||
const { page, pageSize } = parsePage(req.query);
|
||
const total = (db.get('SELECT COUNT(*) c FROM forum_replies WHERE author_id = ?', [uid]) || {}).c || 0;
|
||
const list = db.all(
|
||
`SELECT fr.id, fr.content, fr.post_id, fp.title as post_title, fp.category_id, fr.created_at
|
||
FROM forum_replies fr LEFT JOIN forum_posts fp ON fr.post_id = fp.id
|
||
WHERE fr.author_id = ? ORDER BY fr.created_at DESC, fr.id DESC LIMIT ? OFFSET ?`,
|
||
[uid, pageSize, (page - 1) * pageSize]);
|
||
// 先净化(剥附件/锁块)再截断,避免锁块内容残留泄露
|
||
list.forEach(r => { r.content = sanitizeContent(r.content, 120); });
|
||
res.json({ list, total, page, pageSize, totalPages: Math.ceil(total / pageSize) });
|
||
});
|
||
|
||
module.exports = router;
|