- auth 包:读者会话(one_reader,与后台令牌互斥)+ GitHub OAuth 客户端;Verify 校验 HMAC 与 reader: 前缀 - 公开 API:auth 五端点、评论列表(顶层可见+自己待审、回复内嵌)、发表(登录/禁言/开关/500 字校验)、10 分钟编辑窗、软删墓碑 - 管理端:评论列表(待审/已通过/全部)、通过、软删、读者列表、禁言切换 - 前台:登录卡/禁言卡;后台:评论管理页 + 设置页审核开关
161 lines
6.5 KiB
JavaScript
161 lines
6.5 KiB
JavaScript
const base = ''
|
||
|
||
// 侧栏数据(标签 / 最近更新)每页都会重新挂载请求,这里做 30s 的
|
||
// 模块级缓存 + 并发去重;失败不缓存,下一次调用会重新请求。
|
||
function cached(fn, ttl = 30000) {
|
||
let p = null
|
||
let at = 0
|
||
return (...args) => {
|
||
const now = Date.now()
|
||
if (p && now - at < ttl) return p
|
||
at = now
|
||
const cur = fn(...args)
|
||
p = cur
|
||
cur.catch(() => {
|
||
if (p === cur) p = null
|
||
})
|
||
return cur
|
||
}
|
||
}
|
||
|
||
async function request(path, { method = 'GET', body, auth = false } = {}) {
|
||
const headers = { 'Content-Type': 'application/json' }
|
||
const res = await fetch(base + path, {
|
||
method,
|
||
headers,
|
||
credentials: 'include',
|
||
body: body === undefined ? undefined : JSON.stringify(body)
|
||
})
|
||
if (res.status === 401) {
|
||
window.dispatchEvent(new CustomEvent('one:unauthorized'))
|
||
const err = new Error('未登录或登录已过期')
|
||
err.status = 401
|
||
throw err
|
||
}
|
||
if (!res.ok) {
|
||
let msg = `请求失败(${res.status})`
|
||
try {
|
||
const data = await res.json()
|
||
if (data && data.error) msg = data.error
|
||
} catch (_) {
|
||
/* 非 JSON 响应 */
|
||
}
|
||
const err = new Error(msg)
|
||
err.status = res.status
|
||
throw err
|
||
}
|
||
if (res.status === 204) return null
|
||
return res.json()
|
||
}
|
||
|
||
export const publicApi = {
|
||
site: () => request('/api/site'),
|
||
posts: (params = {}) => request('/api/posts?' + new URLSearchParams(params)),
|
||
post: (slug) => request('/api/posts/' + encodeURIComponent(slug)),
|
||
archive: () => request('/api/archive'),
|
||
tags: cached(() => request('/api/tags')),
|
||
projects: cached(() => request('/api/projects')),
|
||
latest: cached(() => request('/api/posts?size=5'))
|
||
}
|
||
|
||
export const adminApi = {
|
||
login: (username, password) =>
|
||
request('/api/admin/login', { method: 'POST', body: { username, password } }),
|
||
logout: () => request('/api/admin/logout', { method: 'POST' }),
|
||
me: () => request('/api/admin/me'),
|
||
dashboard: () => request('/api/admin/dashboard'),
|
||
posts: (params = {}) => request('/api/admin/posts?' + new URLSearchParams(params)),
|
||
post: (id) => request('/api/admin/posts/' + id),
|
||
createPost: (body) => request('/api/admin/posts', { method: 'POST', body }),
|
||
updatePost: (id, body) => request('/api/admin/posts/' + id, { method: 'PUT', body }),
|
||
deletePost: (id) => request('/api/admin/posts/' + id, { method: 'DELETE' }),
|
||
bulkPosts: (ids, action) =>
|
||
request('/api/admin/posts/bulk', { method: 'POST', body: { ids, action } }),
|
||
tags: () => request('/api/admin/tags'),
|
||
createTag: (body) => request('/api/admin/tags', { method: 'POST', body }),
|
||
updateTag: (id, body) => request('/api/admin/tags/' + id, { method: 'PUT', body }),
|
||
deleteTag: (id) => request('/api/admin/tags/' + id, { method: 'DELETE' }),
|
||
mergeTag: (id, toId) =>
|
||
request('/api/admin/tags/' + id + '/merge', { method: 'POST', body: { to_id: toId } }),
|
||
projects: (params = {}) => request('/api/admin/projects?' + new URLSearchParams(params)),
|
||
createProject: (body) => request('/api/admin/projects', { method: 'POST', body }),
|
||
updateProject: (id, body) => request('/api/admin/projects/' + id, { method: 'PUT', body }),
|
||
deleteProject: (id) => request('/api/admin/projects/' + id, { method: 'DELETE' }),
|
||
settings: () => request('/api/admin/settings'),
|
||
saveSettings: (body) => request('/api/admin/settings', { method: 'PUT', body }),
|
||
// ---------- 评论管理 ----------
|
||
comments: (params = {}) => request('/api/admin/comments?' + new URLSearchParams(params)),
|
||
approveComment: (id) =>
|
||
request('/api/admin/comments/' + id, { method: 'PUT', body: { status: 'visible' } }),
|
||
deleteComment: (id) => request('/api/admin/comments/' + id, { method: 'DELETE' }),
|
||
readers: (params = {}) => request('/api/admin/readers?' + new URLSearchParams(params)),
|
||
setReaderBanned: (id, banned) =>
|
||
request('/api/admin/readers/' + id + '/ban', { method: 'POST', body: { banned } }),
|
||
// ---------- 文件上传 ----------
|
||
files: (params = {}) => request('/api/admin/files?' + new URLSearchParams(params)),
|
||
deleteFile: (id) => request('/api/admin/files/' + id, { method: 'DELETE' }),
|
||
// 上传走 FormData:request() 是 JSON helper,这里单独 fetch。
|
||
// 401 同样广播 one:unauthorized,错误消息从 JSON body 里取(与 request 一致)。
|
||
uploadFiles: async (formData) => {
|
||
const res = await fetch(base + '/api/admin/files', {
|
||
method: 'POST',
|
||
credentials: 'include',
|
||
body: formData
|
||
})
|
||
if (res.status === 401) {
|
||
window.dispatchEvent(new CustomEvent('one:unauthorized'))
|
||
const err = new Error('未登录或登录已过期')
|
||
err.status = 401
|
||
throw err
|
||
}
|
||
const data = await res.json().catch(() => ({}))
|
||
if (!res.ok) {
|
||
const err = new Error(data.error || `上传失败(${res.status})`)
|
||
err.status = res.status
|
||
throw err
|
||
}
|
||
return data
|
||
}
|
||
}
|
||
|
||
// 读者(评论区)身份。会话是服务端 httpOnly cookie(one_reader),
|
||
// 前端只拿 /api/auth/me 的结果做展示,不存 token。
|
||
// 注意 me 在匿名时返回 200 {user:null} 而不是 401 —— request() 对每个 401
|
||
// 都会派发 one:unauthorized,匿名访客每次开页都会刷一遍事件。
|
||
export const readerApi = {
|
||
me: () => request('/api/auth/me'),
|
||
providers: () => request('/api/auth/providers'),
|
||
logout: () => request('/api/auth/logout', { method: 'POST' }),
|
||
// Telegram 是 Login Widget:官方脚本回调里直接带着签名字段,没有 code 可换
|
||
telegram: (payload) => request('/api/auth/telegram', { method: 'POST', body: payload }),
|
||
|
||
comments: (postId, { sort = 'default', page = 1, size = 10 } = {}) =>
|
||
request('/api/comments?' + new URLSearchParams({ post_id: postId, sort, page, size })),
|
||
thread: (rootId, cursor = '', size = 10) =>
|
||
request(`/api/comments/${rootId}/thread?` + new URLSearchParams({ cursor, size })),
|
||
create: (postId, bodyMd, parentId = 0) =>
|
||
request('/api/comments', {
|
||
method: 'POST',
|
||
body: { post_id: postId, parent_id: parentId, body_md: bodyMd }
|
||
}),
|
||
edit: (id, bodyMd) =>
|
||
request(`/api/comments/${id}`, { method: 'PUT', body: { body_md: bodyMd } }),
|
||
remove: (id) => request(`/api/comments/${id}`, { method: 'DELETE' })
|
||
}
|
||
|
||
// 后台登录态:cookie 由服务端下发(httpOnly),这里只存一份展示用的用户名
|
||
const USER_KEY = 'one.admin.user'
|
||
|
||
export const session = {
|
||
get user() {
|
||
return localStorage.getItem(USER_KEY) || ''
|
||
},
|
||
set user(v) {
|
||
if (v) localStorage.setItem(USER_KEY, v)
|
||
else localStorage.removeItem(USER_KEY)
|
||
},
|
||
clear() {
|
||
localStorage.removeItem(USER_KEY)
|
||
}
|
||
}
|