多用户与角色: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)
This commit is contained in:
Sakurasan committed 2026-10-01 22:35:37 +08:00
1 parent e5dee4daf1
commit 4bb2ff4145
23 files changed
+917 -222

No files matched your search

+13 -4
View File
@@ -42,7 +42,8 @@ func (a *API) readerID(r *http.Request) (int64, bool) {
}
// resolveReader 解出当前访客的读者身份:读者会话优先,
// 其次是后台管理员会话(自动 upsert 一个 provider=admin 的站主读者)。
// 其次是后台管理员会话——管理员用自己 users 行上的身份发言
// (多用户后 owner / admin 各自署名,不再都挂在站主名下)。
func (a *API) resolveReader(r *http.Request) (model.Reader, bool, error) {
if ck, err := r.Cookie(auth.ReaderCookie); err == nil && ck.Value != "" {
if id, verr := a.ReaderSessions.Verify(ck.Value); verr == nil {
@@ -54,9 +55,17 @@ func (a *API) resolveReader(r *http.Request) (model.Reader, bool, error) {
}
if a.AdminSessions != nil {
if ck, err := r.Cookie("one_session"); err == nil && ck.Value != "" {
if _, verr := a.AdminSessions.Verify(ck.Value); verr == nil {
rd, oerr := a.ownerReader()
if oerr == nil {
if id, verr := a.AdminSessions.Verify(ck.Value); verr == nil {
rd, gerr := a.Store.GetReader(id)
// 行被删 / 被停用的后台账号:会话当作不存在,
// 不能落到站主身份上顶名发言
if gerr == nil && rd.Provider == "admin" && !rd.Banned {
if rd.Name == "" {
// 首次发言补一次署名(站点作者名)
if filled, ferr := a.ownerReader(); ferr == nil {
return filled, true, nil
}
}
return rd, true, nil
}
}