多用户与角色: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:
1 parent
e5dee4daf1
commit
4bb2ff4145
23 files changed
+917
-222
No files matched your search
@@ -11,8 +11,10 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
// Sessions are stateless: base64("user:expiryUnix") + "." + HMAC-SHA256.
|
||||
// They survive restarts as long as ONE_SECRET stays the same.
|
||||
// Sessions are stateless: base64("user:<id>:expiryUnix") + "." + HMAC-SHA256.
|
||||
// They survive restarts as long as ONE_SECRET stays the same. The token only
|
||||
// carries the user's DB id — role / ban state is read fresh from the database
|
||||
// on every request, so demotions and bans take effect immediately.
|
||||
type Sessions struct {
|
||||
secret []byte
|
||||
ttl time.Duration
|
||||
@@ -27,37 +29,41 @@ func NewSessions(secret string, ttl time.Duration) *Sessions {
|
||||
|
||||
var ErrBadSession = errors.New("invalid session")
|
||||
|
||||
func (s *Sessions) Issue(user string) (string, time.Time) {
|
||||
func (s *Sessions) Issue(userID int64) (string, time.Time) {
|
||||
exp := time.Now().Add(s.ttl)
|
||||
payload := base64.RawURLEncoding.EncodeToString([]byte(user + ":" + strconv.FormatInt(exp.Unix(), 10)))
|
||||
payload := base64.RawURLEncoding.EncodeToString([]byte(fmt.Sprintf("user:%d:%d", userID, exp.Unix())))
|
||||
return payload + "." + s.sign(payload), exp
|
||||
}
|
||||
|
||||
func (s *Sessions) Verify(token string) (string, error) {
|
||||
func (s *Sessions) Verify(token string) (int64, error) {
|
||||
parts := strings.Split(token, ".")
|
||||
if len(parts) != 2 {
|
||||
return "", ErrBadSession
|
||||
return 0, ErrBadSession
|
||||
}
|
||||
if !hmac.Equal([]byte(s.sign(parts[0])), []byte(parts[1])) {
|
||||
return "", ErrBadSession
|
||||
return 0, ErrBadSession
|
||||
}
|
||||
raw, err := base64.RawURLEncoding.DecodeString(parts[0])
|
||||
if err != nil {
|
||||
return "", ErrBadSession
|
||||
return 0, ErrBadSession
|
||||
}
|
||||
i := strings.LastIndex(string(raw), ":")
|
||||
if i <= 0 {
|
||||
return "", ErrBadSession
|
||||
// user:<id>:<exp>
|
||||
f := strings.Split(string(raw), ":")
|
||||
if len(f) != 3 || f[0] != "user" {
|
||||
return 0, ErrBadSession
|
||||
}
|
||||
user := string(raw)[:i]
|
||||
expUnix, err := strconv.ParseInt(string(raw)[i+1:], 10, 64)
|
||||
id, err := strconv.ParseInt(f[1], 10, 64)
|
||||
if err != nil || id <= 0 {
|
||||
return 0, ErrBadSession
|
||||
}
|
||||
expUnix, err := strconv.ParseInt(f[2], 10, 64)
|
||||
if err != nil {
|
||||
return "", ErrBadSession
|
||||
return 0, ErrBadSession
|
||||
}
|
||||
if time.Now().After(time.Unix(expUnix, 0)) {
|
||||
return "", ErrBadSession
|
||||
return 0, ErrBadSession
|
||||
}
|
||||
return user, nil
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (s *Sessions) sign(payload string) string {
|
||||
|
||||
Reference in new issue
Block a user