feat(M1): 认证与用户管理 — 双通道登录、cookie 会话、用户 CRUD 与真实系统账号对接

认证:
- 图形验证码 GET /auth/captcha(内置 PNG 渲染,零第三方依赖)
- 外部用户 OTP 双通道:DB 存储(otp_codes)使邮件与 CLI 共用同一验证码/冷却/失败限速
- 管理员 bcrypt 登录 + 连续失败限速锁定;admin/forgot + admin/reset 邮件重置(SMTP 或日志)
- cookie 会话(HttpOnly/SameSite)、me/logout、admin/user 鉴权中间件

用户管理(admin):
- CRUD + disable/enable/extend/delete,对接 system 层真实 useradd/usermod/userdel/passwd
- system 层三执行模式:dry-run(默认,安全)/ direct(容器/测试用户)/ sudo(生产 sudoers 白名单)
- Exists 系统账号一致性检查;deploy/sudoers.example 白名单模板
- 关键操作接入 append-only 审计

其他:
- CLI user otp 改 DB store,与邮件通道真正对齐
- 容器镜像补 shadow(alpine 无 useradd);Makefile VERSION 0.2.0-m1
- 测试:auth/service 单测 + api httptest 集成 + 容器内真实系统账号端到端验证
This commit is contained in:
2026-08-29 23:40:20 +08:00
parent ae45aba607
commit 630d240dc0
32 changed files with 2923 additions and 188 deletions
+40
View File
@@ -0,0 +1,40 @@
package api
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
)
// SessionCookieName 会话 cookie 名称。
const SessionCookieName = "usernode_session"
// SessionContextKey 会话在 gin context 中的键(router 中间件注入)。
const SessionContextKey = "auth_session"
// setSessionCookie 写入会话 cookieHttpOnly/SameSite=LaxmaxAge<=0 时清除)。
func setSessionCookie(c *gin.Context, sid string, ttl time.Duration, secure bool) {
maxAge := int(ttl.Seconds())
if sid == "" {
maxAge = -1
}
http.SetCookie(c.Writer, &http.Cookie{
Name: SessionCookieName,
Value: sid,
Path: "/",
HttpOnly: true,
SameSite: http.SameSiteLaxMode,
MaxAge: maxAge,
Secure: secure,
})
}
// SessionIDFromCookie 从请求 cookie 读取会话 ID。
func SessionIDFromCookie(c *gin.Context) string {
v, err := c.Cookie(SessionCookieName)
if err != nil {
return ""
}
return v
}