Files
ONE/frontend/src/api.js
T
Sakurasan cfd6948987 评论阶段二:GitHub OAuth 登录 + 评论表/用户表 + 先审后显开关 + 禁言 + 后台评论管理页
- auth 包:读者会话(one_reader,与后台令牌互斥)+ GitHub OAuth 客户端;Verify 校验 HMAC 与 reader: 前缀
- 公开 API:auth 五端点、评论列表(顶层可见+自己待审、回复内嵌)、发表(登录/禁言/开关/500 字校验)、10 分钟编辑窗、软删墓碑
- 管理端:评论列表(待审/已通过/全部)、通过、软删、读者列表、禁言切换
- 前台:登录卡/禁言卡;后台:评论管理页 + 设置页审核开关
2026-09-27 23:41:40 +08:00

161 lines
6.5 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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)
}
}