Files
ONE/README.md
T
Sakurasan 4bb2ff4145 多用户与角色:owner / admin / reader 三级,用户管理页 + 密码上库
- users 表加 password_hash 列;后台账号(owner+admin)密码 bcrypt 存行内,
  首次登录把 env / settings 引导凭据自迁移成行哈希
- 会话 token 从用户名改为携带用户 ID,角色与停用状态每请求查库,
  改角色 / 停用账号即时生效(存量会话立即 401)
- 登录:先查 users 表,再走 settings 哈希 / env 引导链;
  admin/admin 开发模式在任何账号设过密码后失效
- 权限:系统设置、用户管理仅 owner;内容管理 admin+owner;
  admin 后台新增 用户 页(创建 / 重置密码 / 停用 / 删除),
  设置页「登录与存储」tab 对管理员隐藏
- 账户页加修改密码表单(旧密码校验,OAuth/Passkey 首设免旧密码);
  评论区管理员身份跟随各自账号,不再统一挂站主名下
- 修复:providers 为 nil 时账户页白屏(Go nil slice 序列化成 null)
2026-10-01 22:35:37 +08:00

245 lines
12 KiB
Markdown
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.
# ONE · 一个博客
一个能跑通全流程的博客 MVP:后台登录 → 写文章(长文 / 短文)→ 发布 → 前台按「07 融合 + Twitter 信息流」风格展示。
- 后端:Go(`net/http` + `database/sql`,SQLite / PostgreSQL 双支持),在 `backend/`
- 前台 + 后台:Vue 3 + Vue Router + Vite,在 `frontend/`(pnpm 管理依赖)
- Markdown:服务端用 `goldmark` 渲染入库,编辑器实时预览用 `marked` + `DOMPurify`
## 一条命令跑起来
```bash
make dev # 后端 8080 + 前端 3000(前端带热更新,已配好 /api 代理)
```
打开 http://localhost:3000 是前台,http://localhost:3000/admin 是后台。
停掉:`make stop`
想要单端口(前端构建产物由 Go 直接托管,接近生产形态):
```bash
make start # 先 pnpm run build,再起 Go,只开 8080
```
打开 http://localhost:8080 。
其它:`make web` 只构建前端、`make server` 只起后端、`make test` 跑后端测试、`make clean` 清构建产物、`make db-reset` 清空本地 SQLite 数据。
## 后台登录
默认账号密码来自环境变量,未设置时是 `admin` / `admin`(启动日志会打印出来):
```bash
ONE_ADMIN_USER=admin ONE_ADMIN_PASSWORD=换一个 make start
```
登录态是服务端签发的 httpOnly cookie(HMAC-SHA256,7 天有效),同时支持 `Authorization: Bearer <token>`,方便用 curl / 脚本写文章。
## 设置:后台优先,环境变量兜底
站点地址、评论区第三方登录(GitHub / Google / Telegram)凭据、R2 对象存储、
管理员用户名与密码,都在后台 **设置 → 登录与存储**(站点地址在 **基础信息**)里配置,
保存后立即生效,无需重启。这些值存在 `settings` 表里,生效规则是
「后台填了用后台的,没填回落到环境变量」——老部署不改 env 也能照常跑。
秘密项(client secret、Bot token、R2 密钥)在后台只显示
「是否已配置、来自哪里」,永不回显明文;留空保存 = 保持现值。
**多用户与角色**:站主(owner,全库唯一)可在后台 **用户** 页创建内容管理员
(admin),管理员能写文章、管理评论与文件,但系统设置、用户管理与第三方凭据
只有站主动。密码 bcrypt 存在 users 表上,各账号在「账户」页自行修改;
显式设置过的 `ONE_ADMIN_PASSWORD` 保留作站主的解锁后路,
admin/admin 开发模式在任何账号设置过密码后立即失效。
环境变量只剩启动期必需项:
| 变量 | 默认 | 说明 |
| --- | --- | --- |
| `ONE_ADDR` | `:8080` | 监听地址 |
| `ONE_DB_DRIVER` | `sqlite` | `sqlite` 或 `postgres` |
| `ONE_DB_DSN` | `./data/one.db` | 数据库连接串(Postgres 示例:`postgres://user:pass@localhost/one?sslmode=disable`) |
| `ONE_SECRET` | 随机生成 | 会话签名密钥;不设的话重启后登录态失效 |
| `ONE_WEB_DIST` | `./frontend/dist` | 前端构建产物目录 |
| `ONE_DATA_DIR` | `./data` | SQLite 数据目录 |
| `ONE_ADMIN_USER` | `admin` | 初始后台用户名(后台可改) |
| `ONE_ADMIN_PASSWORD` | `admin` | 初始后台密码(后台可改;不设即 admin/admin 的开发模式) |
| `ONE_SITE_URL` | `http://localhost:8080` | 站点地址兜底值(后台可改) |
| `ONE_GITHUB_CLIENT_ID` / `_SECRET` | — | GitHub 登录兜底值(后台可改) |
| `ONE_GOOGLE_CLIENT_ID` / `_SECRET` | — | Google 登录兜底值(后台可改) |
| `ONE_TELEGRAM_BOT` / `_TOKEN` | — | Telegram 登录兜底值(后台可改) |
| `S3Api` / `Bucket` / `AccessKey` / `SecretAccessKey` / `PublicURL` | — | R2 存储兜底值(后台可改) |
| `ONE_WEBAUTHN_RP_ID` / `_ORIGINS` | — | Passkey 登录;显式配 origins 才启用 |
## 界面:classic / vivid
前台有两套完整界面,都保留,在后台 **设置 → 界面与自定义** 里整站切换(站点级设置,所有访客同款)。
后台自身的外观不受影响。
| `ui_id` | 说明 |
| --- | --- |
| `classic`(默认) | 原来的极简版:三栏骨架(左导航 / 正文 / 右栏)、纸感分区线、衬线标题、首字下沉 |
| `vivid` | 顶部粘性导航 + 单栏卡片流、渐变配色与背景网格、悬浮卡片、首页 hero;支持站主自定义 CSS |
实现要点,改这套东西前先读:
- `ui_id` 写在 `<html data-ui>` 上,由 `frontend/src/site.js` 的 `applyTheme()` 设置。
`index.html` 里有一小段 inline script 先读 `localStorage['one.ui']` 设好,避免首帧闪一下 classic。
- vivid 的皮肤**不复用** paper/sage/rose,而是让 `data-theme` 取 `vivid` / `vivid-dark`
(明暗仍由访客的 themeMode 决定)。这样两套调色板不会互相打架。
- `frontend/src/ui/vivid.css` 是**纯覆盖层**,在 `main.ts` 里排在 `styles.css` 之后。
**每个选择器都必须以 `html[data-ui="vivid"]` 开头**:scoped 样式编译后是 `.foo[data-v-HASH]`
(特指度 0,2,0),而 `html[data-ui="vivid"] .foo` 是 0,2,1 才能稳定取胜;scoped 样式是懒加载 chunk,
在入口 CSS 之后才到位,漏了前缀会变成「有时生效有时不生效」。复合选择器要**整条链**镜像
(如 `.card:hover .layer-front` 要写全,不能只前缀第一段)。该文件里不允许出现 `!important`。
- vivid 把 classic 那 12 个 token 重新赋值(`--paper/--card/--ink/--accent/...`),
所以读 token 的共享组件零改动就换了皮。vivid 自己的变量以 `--v-` 开头。
- 顶部导航与首页 hero 是新增组件(`VividNav.vue` / `VividHero.vue`),由 `App.vue` 在
`ui_id === 'vivid' && !isAdmin` 时渲染;`LeftNav.vue` / `RightRail.vue` 在 vivid 下自身不渲染,
7 个公开视图没有任何改动。
### 站主自定义 CSS
按分区分段写,存在 settings 的 `custom_css`(一行 JSON,`{分区: CSS}`)。
```
global 全局 —— 每个前台页面都会加载,放变量和通用微调
home 首页(含标签筛选)
post 文章详情(含短文)
archive 归档
tags 标签云 + 单个标签下的列表
projects 作品
about 关于
```
分区 id 必须与 `backend/internal/store/store.go` 的 `ValidCSSSections`
和 `frontend/src/ui/sections.js` 的 `SECTIONS` 三处保持一致,否则会出现「保存了但不生效」。
- **只在 `ui_id = vivid` 时注入**,并且 `/admin` 下一律清空 —— 写坏样式影响不到后台,classic 也完全不受影响。
- 注入方式是两个 `<style>`(`#one-css-global` 常驻、`#one-css-section` 跟路由换),只写 `textContent`,
不是 `innerHTML`,所以 CSS 里写 `</style>` 也逃不出去,不需要 CSS 清洗库。
- 上限:单个分区 16 KiB,全部合计 64 KiB。超限的分区会被**静默丢弃**(写的时候就不落库)。
- 后台保存是**整对象 PUT**:请求里没带的字段会变成零值。用 curl 写的时候记得把其他字段一起带上。
可覆盖的变量(写进「全局」段即可整站生效):`--v-accent`、`--v-accent-2`、`--v-accent-3`、
`--v-bg`、`--v-surface`、`--v-surface-2`、`--v-text`、`--v-text-soft`、`--v-muted`、`--v-line`、
`--v-radius`、`--v-radius-pill`、`--v-content-width`、`--v-measure`、`--v-motion`(0 = 关掉全部动效)。
后台「界面与自定义」里内置了几个一键插入的片段(换主色 / 方正圆角 / 关渐变背景 / 首页单列),
它们只是往文本框里塞文本,不落库,插进去还能接着改。
> 注意:目前若将来给站点加 CSP,运行时的 `<style>` 注入需要 `style-src 'unsafe-inline'` 或一个 nonce。
## 目录结构
```
backend/ Go 后端(模块名 oneblog)
main.go 路由装配、静态托管 SPA、优雅退出
internal/config 环境变量
internal/db SQLite / PostgreSQL 连接与 `?` → `$n` 占位符重写
internal/model 数据结构
internal/store schema 迁移 + 全部 SQL
internal/render goldmark 渲染、阅读时长估算、摘要截取
internal/api 公开接口 + RSS(/api/*、/rss.xml)
internal/admin 后台接口与登录鉴权(/api/admin/*)
internal/httpx JSON 读写助手
frontend/ Vue 3 前端(pnpm)
src/styles.css classic 的设计 token 与正文排版(07 风格)
src/ui/vivid.css 第二套 UI(vivid)的覆盖层,全部以 html[data-ui="vivid"] 前缀收敛
src/ui/sections.js 自定义 CSS 的分区清单(与后端 ValidCSSSections 对应)
src/customCss.js 自定义 CSS 的注入管线(按路由切分区,/admin 下清空)
src/site.js 站点设置 + 明暗 / 皮肤 / UI 三个轴的运行时
src/views 前台:时间线 / 详情 / 归档 / 标签 / 作品 / 关于
src/admin 后台:登录 / 列表 / 编辑器 / 标签 / 作品 / 设置
src/components 左栏导航(classic)、右栏卡片(classic)、时间线行、vivid 导航与 hero
```
原有那一版 Vue 前端(07 风格重写之前)保留在 git 历史的第一个 commit `edee708` 里,
需要对照或回滚:`git checkout edee708 -- frontend/src`。
## 接口
公开(无需登录):
```
GET /api/site 站点设置
GET /api/posts?kind=&tag=&q=&page=&size= 已发布文章(kind: long|short)
GET /api/posts/:slug 文章详情
GET /api/archive 按年 → 月分组
GET /api/tags 标签与计数
GET /rss.xml (/feed 同) RSS 2.0
```
后台(需登录):
```
POST /api/admin/login {username,password} → {token}
POST /api/admin/logout
GET /api/admin/me
GET /api/admin/posts?status=&kind=&q=&page=&size=
POST /api/admin/posts 新建
GET /api/admin/posts/:id
PUT /api/admin/posts/:id 更新(编辑器自动保存走这里)
DELETE /api/admin/posts/:id
GET /api/admin/tags
POST /api/admin/tags {name}
PUT /api/admin/tags/:id {name}
DELETE /api/admin/tags/:id
GET /api/admin/settings
PUT /api/admin/settings
```
`Post` 的关键字段:`kind`(`long` / `short`)、`title`、`slug`、`summary`、`content_md`、`content_html`、`status`、`published_at`、`reading_minutes`、`tags[]`。
短文没有可见标题,但 `title` 仍会由正文首句生成,供归档与标签列表索引。
## 部署
生产形态是「一个二进制 + 一个静态目录」:
```bash
make build # 产出 ./one-server,前端已打进 web/dist
ONE_ADDR=:8080 \
ONE_DB_DRIVER=postgres \
ONE_DB_DSN="postgres://user:pass@127.0.0.1/one?sslmode=disable" \
ONE_SECRET=一串随机长字符串 \
ONE_ADMIN_PASSWORD=强密码 \
./one-server
# 首次启动后登录后台,把站点地址、第三方登录、对象存储在「设置」页里配好,
# 之后改这些就不再需要碰环境变量或重启。
```
要点:
- **静态资源**:Go 直接托管 `web/dist`,未知路径回落到 `index.html`,所以 `/post/xxx`、`/admin` 刷新都不会 404。
- **反向代理**:前面挂 Nginx / Caddy 时把 `/` 反代到 `ONE_ADDR` 即可;如果只在内网监听,可以不挂。
- **SQLite 部署**:把 `data/` 挂到持久卷;并发写很低的博客足够用(已开 WAL + busy_timeout)。
- **PostgreSQL 部署**:改 `ONE_DB_DRIVER` 与 `ONE_DB_DSN`,schema 会在启动时自动创建,无需手动迁移。
- **systemd 示例**:
```ini
[Unit]
Description=ONE blog
After=network.target
[Service]
WorkingDirectory=/opt/one
Environment=ONE_ADDR=:8080 ONE_SECRET=xxx ONE_ADMIN_PASSWORD=xxx
ExecStart=/opt/one/one-server
Restart=always
[Install]
WantedBy=multi-user.target
```
- **备份**:SQLite 直接拷 `data/one.db`(停写更稳);Postgres 用 `pg_dump`。
## 已验证
`make start` 后:后台登录 → 写长文与短文 → 发布 → 前台时间线(长文标题 + 摘要 / 短文正文铺开)、详情、归档、标签、RSS 全部可见;
编辑器改正文 1.5 秒后自动保存到服务端;桌面与移动宽度无横向滚动;SQLite 路径全流程跑通,PostgreSQL 的占位符重写有单元测试覆盖(本机没有 Postgres 实例,未做端到端验证)。
## 待确认
- 仓库地址:本仓库是在本地 `/Users/cjun/Code/one` 新建的 git 仓库,还没有 remote。给个地址就能推上去并绑进工作区。
- 鉴权方式:目前是账号密码 + 签名 cookie。想改成纯 token 也可以,接口已经兼容 `Authorization: Bearer`。
- 图片上传:MVP 未做,正文里先用图片外链。