- 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)
62 lines
1.6 KiB
Go
62 lines
1.6 KiB
Go
// 会话身份的解析与角色守卫。token 只带用户 ID(防篡改靠 HMAC),角色与
|
|
// 停用状态每请求从 users 表现读——后台改角色 / 停用账号即时生效,
|
|
// 不用等 7 天会话过期。
|
|
package admin
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
|
|
"oneblog/internal/httpx"
|
|
"oneblog/internal/model"
|
|
)
|
|
|
|
type actor struct {
|
|
ID int64
|
|
Handle string
|
|
Role string
|
|
}
|
|
|
|
type actorKey struct{}
|
|
|
|
func (a *API) guard(next http.HandlerFunc) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
token := bearer(r)
|
|
if token == "" {
|
|
if c, err := r.Cookie(cookieName); err == nil {
|
|
token = c.Value
|
|
}
|
|
}
|
|
id, err := a.Sessions.Verify(token)
|
|
if err != nil {
|
|
httpx.Unauthorized(w)
|
|
return
|
|
}
|
|
u, err := a.Store.GetUserByID(id)
|
|
// 只认后台账号行:token 是我们签发的,理论上不会指到 reader,
|
|
// 但防御式校验一层;停用的账号立即失效。
|
|
if err != nil || u.Provider != "admin" || u.Banned {
|
|
httpx.Unauthorized(w)
|
|
return
|
|
}
|
|
act := actor{ID: u.ID, Handle: u.Handle, Role: u.Role}
|
|
next(w, r.WithContext(context.WithValue(r.Context(), actorKey{}, act)))
|
|
}
|
|
}
|
|
|
|
// guardOwner 在 guard 之上加角色门槛:系统设置、用户管理只属于站主。
|
|
func (a *API) guardOwner(next http.HandlerFunc) http.HandlerFunc {
|
|
return a.guard(func(w http.ResponseWriter, r *http.Request) {
|
|
if actorFrom(r).Role != model.RoleOwner {
|
|
httpx.Error(w, http.StatusForbidden, "需要站主权限")
|
|
return
|
|
}
|
|
next(w, r)
|
|
})
|
|
}
|
|
|
|
func actorFrom(r *http.Request) actor {
|
|
act, _ := r.Context().Value(actorKey{}).(actor)
|
|
return act
|
|
}
|