fix: 修复首页空白/失效外链/云函数越权,补齐缺失页面与种子数据
## 阻断性缺陷 - list.vue 是 0 字节空文件、slist.vue 与 search/search.vue 从未存在, 而前者是 tabBar 首页、后者是 tabBar「搜索」页 —— 开屏即白屏。 按 .nvue 原型与详情页契约重建三页(CSS 渐变主视觉、分类筛选、搜索历史/热搜/联想)。 - parse-image-url.js 对空封面调 undefined.startsWith 直接抛错,列表页整页崩。 - 云函数目录缺 uni-cms-articles / uni-cms-categories / uni-cms-unlock-record schema 与 schema.ext.js,线上内容渲染与解锁逻辑无配置可用。 ## 越权与数据一致性 - uni-cms-articles:del/update/add 全部无鉴权,未登录即可删任意文章、 改他人文章作者与阅读量。补 login + 作者归属校验,作者与计数改为服务端取值。 - comments.likeComment/relikeComment:直接采信客户端传入的 user_id, 可冒名点赞刷计数。改为以令牌为准,并纳入事务。 - comments.updateComment:对数组取 .author_id,权限判断恒失败; 字段名 updateTime 与 ip_location 类型与 schema 不符。 - comments.deleteComment:`!root_id === 0` 优先级错误导致计数恒不减; 且误更新 uni-cms-articles、按不存在的 type 字段删点赞明细产生孤儿数据。 - cms-articles-like/collect:查重条件混入本次请求时间戳,防重永远失效, 可无限重复刷计数;补唯一索引并事务化。 - cms-vote:读-改-写票数导致并发丢票,记录与统计非原子;改为事务 + 原子自增。 - cms-articles-log:忽略传入 user_id 直接返回全表,泄露全站浏览记录。 - article_info:get() 使用未定义变量必崩;读接口全部无鉴权。 - user-info:公开资料接口可查任意用户 last_login_ip。 ## 资源与数据 - 全项目清空失效的签名外链(expire_at 均为 2025-03,必然 403), 改为本地生成资源:6 套文章模板、8 个编辑器图标、2 张文章配图。 - 新增分类 / 模板 / 礼物 / 热搜词种子数据,并在 db_init.json 登记, 同时补上点赞、收藏、投票、浏览日志的唯一索引。 ## 功能 - 草稿箱:预览页拆出「发布」与「存为草稿」,作品列表按状态筛选并显示徽标。 原实现有 4 个 tab 但只有 1 个有内容,且 article_status 在 UI 上无体现。 - 编辑中断恢复:接上原本空实现的「编辑草稿」回调,区分新建与编辑已有文章。 ## 工具 - tools/audit-project.js:编码 / 页面路由 / 云调用 / 云函数鉴权 / 敏感信息检查 - tools/check-vue.js:SFC 脚本语法(词法扫描处理 import·export 与条件编译) - tools/verify.js:一键验证;两个检查器各带自测,防止"永远通过" - tools/gen-*.py:模板与图标资源生成脚本
This commit is contained in:
@@ -1,135 +1,180 @@
|
||||
const db = uniCloud.database();
|
||||
const _ = db.command;
|
||||
const jqb_db = uniCloud.databaseForJQL();
|
||||
const uniID = require('uni-id-common');
|
||||
|
||||
const ARTICLE_DB = 'uni-cms-articles';
|
||||
|
||||
module.exports = {
|
||||
_before: function() { // 通用预处理器
|
||||
|
||||
_before: function () { // 通用预处理器
|
||||
this.uniID = uniID.createInstance({
|
||||
context: this.getClientInfo()
|
||||
});
|
||||
},
|
||||
|
||||
/** 校验登录并返回 payload */
|
||||
async _requireLogin() {
|
||||
const payload = await this.uniID.checkToken(this.getUniIdToken());
|
||||
if (payload.errCode) throw new Error('登录状态失效,请重新登录');
|
||||
return payload;
|
||||
},
|
||||
|
||||
/**
|
||||
* 读取文章详情
|
||||
* 只返回已发布文章;作者与管理员可读自己的草稿
|
||||
*/
|
||||
async getDetail(query) {
|
||||
console.log(query)
|
||||
const res = await db.collection("uni-cms-articles").doc(query.id).get();
|
||||
return res;
|
||||
try {
|
||||
const id = query && query.id;
|
||||
if (!id) return { code: 400, msg: '缺少文章 id' };
|
||||
|
||||
const res = await db.collection(ARTICLE_DB).doc(id).get();
|
||||
const article = res.data && res.data[0];
|
||||
if (!article) return { code: 404, msg: '文章不存在' };
|
||||
|
||||
if (article.article_status !== 1) {
|
||||
const payload = await this.uniID.checkToken(this.getUniIdToken());
|
||||
const owner = !payload.errCode && article.user_id === payload.uid;
|
||||
const admin = !payload.errCode && (payload.role || []).includes('admin');
|
||||
if (!owner && !admin) {
|
||||
return { code: 403, msg: '无权查看该文章' };
|
||||
}
|
||||
}
|
||||
return { code: 200, data: article };
|
||||
} catch (e) {
|
||||
return { code: 500, msg: e.message };
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 是否已投过票:以登录态为准,不再采信客户端传入的 user_id
|
||||
*/
|
||||
async isSelectVote(query) {
|
||||
try {
|
||||
let {
|
||||
user_id,
|
||||
vote_id
|
||||
} = query;
|
||||
|
||||
const payload = await this._requireLogin();
|
||||
const vote_id = query && query.vote_id;
|
||||
if (!vote_id) return { code: 400, msg: '缺少 vote_id' };
|
||||
|
||||
const res = await db.collection("cms-vote")
|
||||
.where({
|
||||
user_id: user_id,
|
||||
vote_id: vote_id
|
||||
})
|
||||
.count();
|
||||
let isSelectVote = false;
|
||||
console.log(res.total)
|
||||
if (res.total > 0) {
|
||||
isSelectVote = true;
|
||||
} else {
|
||||
isSelectVote = false;
|
||||
}
|
||||
|
||||
.where({
|
||||
user_id: payload.uid,
|
||||
vote_id: vote_id
|
||||
})
|
||||
.count();
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
isSelectVote: isSelectVote
|
||||
}
|
||||
} catch(e) {
|
||||
return {
|
||||
code: 500,
|
||||
msg: e.message
|
||||
}
|
||||
isSelectVote: res.total > 0
|
||||
};
|
||||
} catch (e) {
|
||||
return { code: 500, msg: e.message };
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 当前登录用户对某文章的点赞/收藏状态
|
||||
*/
|
||||
async getInfo(query) {
|
||||
try {
|
||||
const payload = await this._requireLogin();
|
||||
const article_id = query && query.article_id;
|
||||
if (!article_id) return { code: 400, msg: '缺少 article_id' };
|
||||
|
||||
let user_id = query.user_id;
|
||||
let article_id = query.article_id;
|
||||
|
||||
let isLike = false;
|
||||
let isCollect = false;
|
||||
|
||||
const likeRes = await db.collection("cms-articles-like").where({
|
||||
user_id: user_id,
|
||||
article_id: article_id
|
||||
})
|
||||
.count();
|
||||
|
||||
if (likeRes.total > 0) {
|
||||
isLike = true;
|
||||
}
|
||||
|
||||
const collectRes = await db.collection("cms-articles-collect").where({
|
||||
user_id: user_id,
|
||||
article_id: article_id
|
||||
})
|
||||
.count();
|
||||
|
||||
|
||||
if (collectRes.total > 0) {
|
||||
isCollect = true;
|
||||
}
|
||||
const [likeRes, collectRes] = await Promise.all([
|
||||
db.collection("cms-articles-like").where({
|
||||
user_id: payload.uid,
|
||||
article_id
|
||||
}).count(),
|
||||
db.collection("cms-articles-collect").where({
|
||||
user_id: payload.uid,
|
||||
article_id
|
||||
}).count()
|
||||
]);
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
isLike: isLike,
|
||||
isCollect: isCollect
|
||||
}
|
||||
isLike: likeRes.total > 0,
|
||||
isCollect: collectRes.total > 0
|
||||
};
|
||||
} catch (e) {
|
||||
console.log(e.message);
|
||||
return {
|
||||
code: 500,
|
||||
msg: e.message
|
||||
}
|
||||
return { code: 500, msg: e.message };
|
||||
}
|
||||
},
|
||||
async get() {
|
||||
const res = await db.collection(dbCollectionName).doc("67ba7308bf7490a7f48d3ba3").field(
|
||||
"_id,is_admin,user_id,category_id,title,content,excerpt,article_status,is_sticky,is_essence,comment_status,thumbnail,publish_date"
|
||||
|
||||
/**
|
||||
* 按 id 读取公开文章字段。
|
||||
* 原实现用了未定义变量 dbCollectionName 且硬编码文档 id,调用必然抛错。
|
||||
*/
|
||||
async get(query) {
|
||||
try {
|
||||
const id = query && (query.id || query._id);
|
||||
if (!id) return { code: 400, msg: '缺少文章 id' };
|
||||
|
||||
const res = await db.collection(ARTICLE_DB).doc(id).field(
|
||||
"_id,user_id,category_id,title,content,excerpt,article_status,thumbnail,publish_date"
|
||||
).get();
|
||||
console.log(res);
|
||||
return res;
|
||||
|
||||
const article = res.data && res.data[0];
|
||||
if (!article) return { code: 404, msg: '文章不存在' };
|
||||
if (article.article_status !== 1) {
|
||||
const payload = await this.uniID.checkToken(this.getUniIdToken());
|
||||
const owner = !payload.errCode && article.user_id === payload.uid;
|
||||
const admin = !payload.errCode && (payload.role || []).includes('admin');
|
||||
if (!owner && !admin) return { code: 403, msg: '无权查看该文章' };
|
||||
}
|
||||
return { code: 200, data: article };
|
||||
} catch (e) {
|
||||
return { code: 500, msg: e.message };
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* 某篇文章的阅读记录(详情页用于展示"谁看过")
|
||||
* 只有文章作者(或管理员)能看,普通读者不返回任何数据。
|
||||
*/
|
||||
async getCmsArticleLog(query) {
|
||||
try {
|
||||
const payload = await this._requireLogin();
|
||||
const article_id = query && query.article_id;
|
||||
if (!article_id) return { code: 400, msg: '缺少 article_id' };
|
||||
|
||||
const articleRes = await db.collection(ARTICLE_DB).doc(article_id).field({ user_id: true }).get();
|
||||
const article = articleRes.data && articleRes.data[0];
|
||||
if (!article) return { code: 404, msg: '文章不存在' };
|
||||
|
||||
let article_id = query.article_id;
|
||||
// cms-articles-log
|
||||
const isAdmin = (payload.role || []).includes('admin');
|
||||
if (article.user_id !== payload.uid && !isAdmin) {
|
||||
return { code: 403, msg: '只有作者可以查看阅读记录' };
|
||||
}
|
||||
|
||||
// 生成文章日志临时表(过滤 article_id)
|
||||
const logTemp = jqb_db.collection("cms-articles-log")
|
||||
.where({
|
||||
article_id: article_id
|
||||
})
|
||||
.field('article_id, user_id, first_view_time')
|
||||
.getTemp();
|
||||
const page = Math.max(1, Number(query.page) || 1);
|
||||
const pageSize = Math.min(50, Math.max(1, Number(query.page_size) || 21));
|
||||
|
||||
// 生成用户信息临时表(仅返回必要字段)
|
||||
const userTemp = jqb_db.collection("uni-id-users")
|
||||
.field('_id,avatar_file')
|
||||
.getTemp();
|
||||
|
||||
// 联表查询并分页
|
||||
const collectRes = await jqb_db.collection(logTemp, userTemp)
|
||||
.orderBy('logTemp.first_view_time asc') // 按时间倒序
|
||||
.skip(0)
|
||||
.limit(21)
|
||||
// 浏览日志的读权限是"仅本人",这里用云函数的管理连接跨用户查询
|
||||
const logsRes = await db.collection('cms-articles-log')
|
||||
.where({ article_id })
|
||||
.orderBy('last_view_time', 'desc')
|
||||
.skip((page - 1) * pageSize)
|
||||
.limit(pageSize)
|
||||
.field('article_id, user_id, first_view_time, last_view_time')
|
||||
.get();
|
||||
|
||||
const uids = [...new Set(logsRes.data.map((l) => l.user_id).filter(Boolean))];
|
||||
const usersRes = uids.length
|
||||
? await db.collection('uni-id-users').where({ _id: _.in(uids) }).field('_id,avatar_file').get()
|
||||
: { data: [] };
|
||||
const userMap = new Map(usersRes.data.map((u) => [u._id, u]));
|
||||
|
||||
return {
|
||||
code: 200,
|
||||
cmsArticleLog: collectRes.data
|
||||
}
|
||||
cmsArticleLog: logsRes.data.map((log) => ({
|
||||
...log,
|
||||
user_id: [userMap.get(log.user_id) || { _id: log.user_id, avatar_file: {} }]
|
||||
}))
|
||||
};
|
||||
} catch (e) {
|
||||
console.log(e.message);
|
||||
return {
|
||||
code: 500,
|
||||
msg: e.message
|
||||
}
|
||||
return { code: 500, msg: e.message };
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user