170 lines
5.3 KiB
Go
170 lines
5.3 KiB
Go
package admin
|
|
|
|
import (
|
|
"database/sql"
|
|
"errors"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"git.asio.asia/nixevol/NixMsg/internal/auth"
|
|
"git.asio.asia/nixevol/NixMsg/internal/httpx"
|
|
)
|
|
|
|
func (h *Handler) setSessionCookie(w http.ResponseWriter, r *http.Request, value string, maxAge int) {
|
|
secure := h.forceSec || httpx.IsHTTPS(r, h.trusted)
|
|
http.SetCookie(w, &http.Cookie{
|
|
Name: cookieName,
|
|
Value: value,
|
|
Path: "/",
|
|
HttpOnly: true,
|
|
SameSite: http.SameSiteLaxMode,
|
|
Secure: secure,
|
|
MaxAge: maxAge,
|
|
})
|
|
}
|
|
|
|
func (h *Handler) handleLogin(w http.ResponseWriter, r *http.Request) {
|
|
ip := httpx.ClientIP(r, h.trusted)
|
|
if locked, retry := h.locks.Check(auth.LockKey{Kind: auth.LockAdminIP, IP: ip}); locked {
|
|
w.Header().Set("Retry-After", formatRetryAfter(retry))
|
|
httpx.WriteError(w, http.StatusTooManyRequests, "rate_limited", "登录已锁定,请稍后再试")
|
|
return
|
|
}
|
|
|
|
var req struct {
|
|
Username string `json:"username"`
|
|
Password string `json:"password"`
|
|
}
|
|
if err := httpx.DecodeJSON(r, &req); err != nil {
|
|
httpx.WriteError(w, http.StatusBadRequest, "bad_request", "请求体无效")
|
|
return
|
|
}
|
|
if req.Username != adminUsername {
|
|
h.failLogin(w, ip)
|
|
return
|
|
}
|
|
|
|
phc, err := h.getAdminPasswordHash(r.Context())
|
|
if err != nil {
|
|
if errors.Is(err, sql.ErrNoRows) {
|
|
h.failLogin(w, ip)
|
|
return
|
|
}
|
|
httpx.WriteError(w, http.StatusInternalServerError, "internal", "内部错误")
|
|
return
|
|
}
|
|
ok, err := h.hash.Verify(r.Context(), auth.PasswordAdmin, req.Password, phc)
|
|
if err != nil || !ok {
|
|
h.failLogin(w, ip)
|
|
return
|
|
}
|
|
|
|
h.locks.Clear(auth.LockKey{Kind: auth.LockAdminIP, IP: ip})
|
|
|
|
plain, hashHex, err := newSessionToken()
|
|
if err != nil {
|
|
httpx.WriteError(w, http.StatusInternalServerError, "internal", "内部错误")
|
|
return
|
|
}
|
|
if err := h.createSession(r.Context(), hashHex, h.ttl); err != nil {
|
|
httpx.WriteError(w, http.StatusInternalServerError, "internal", "内部错误")
|
|
return
|
|
}
|
|
h.setSessionCookie(w, r, plain, int(h.ttl.Seconds()))
|
|
h.audit("admin", "login", "", "ok", ip)
|
|
httpx.WriteOK(w, map[string]any{"username": adminUsername})
|
|
}
|
|
|
|
func (h *Handler) failLogin(w http.ResponseWriter, ip string) {
|
|
locked, retry := h.locks.Fail(auth.LockKey{Kind: auth.LockAdminIP, IP: ip})
|
|
if locked {
|
|
w.Header().Set("Retry-After", formatRetryAfter(retry))
|
|
httpx.WriteError(w, http.StatusTooManyRequests, "rate_limited", "登录已锁定,请稍后再试")
|
|
return
|
|
}
|
|
httpx.WriteError(w, http.StatusUnauthorized, "unauthorized", "用户名或密码错误")
|
|
}
|
|
|
|
func (h *Handler) handleLogout(w http.ResponseWriter, r *http.Request) {
|
|
p, _ := principalFrom(r.Context())
|
|
ip := httpx.ClientIP(r, h.trusted)
|
|
if p.Kind == authCookie && p.Session != "" {
|
|
_ = h.deleteSession(r.Context(), hashSessionHex(p.Session))
|
|
}
|
|
h.setSessionCookie(w, r, "", -1)
|
|
h.audit(actorString(p), "logout", "", "ok", ip)
|
|
httpx.WriteOK(w, map[string]any{})
|
|
}
|
|
|
|
func (h *Handler) handleMe(w http.ResponseWriter, r *http.Request) {
|
|
p, _ := principalFrom(r.Context())
|
|
authMode := "cookie"
|
|
if p.Kind == authToken {
|
|
authMode = "token"
|
|
}
|
|
httpx.WriteOK(w, map[string]any{
|
|
"username": adminUsername,
|
|
"auth": authMode,
|
|
})
|
|
}
|
|
|
|
func (h *Handler) handlePassword(w http.ResponseWriter, r *http.Request) {
|
|
p, _ := principalFrom(r.Context())
|
|
ip := httpx.ClientIP(r, h.trusted)
|
|
|
|
var req struct {
|
|
OldPassword string `json:"old_password"`
|
|
NewPassword string `json:"new_password"`
|
|
}
|
|
if err := httpx.DecodeJSON(r, &req); err != nil {
|
|
h.audit(actorString(p), "password_change", "", "bad_request", ip)
|
|
httpx.WriteError(w, http.StatusBadRequest, "bad_request", "请求体无效")
|
|
return
|
|
}
|
|
if len(req.NewPassword) < minPasswordLen {
|
|
h.audit(actorString(p), "password_change", "", "bad_request", ip)
|
|
httpx.WriteError(w, http.StatusBadRequest, "bad_request", "新密码至少 12 位")
|
|
return
|
|
}
|
|
|
|
phc, err := h.getAdminPasswordHash(r.Context())
|
|
if err != nil {
|
|
h.audit(actorString(p), "password_change", "", "error", ip)
|
|
httpx.WriteError(w, http.StatusInternalServerError, "internal", "内部错误")
|
|
return
|
|
}
|
|
ok, err := h.hash.Verify(r.Context(), auth.PasswordAdmin, req.OldPassword, phc)
|
|
if err != nil || !ok {
|
|
h.audit(actorString(p), "password_change", "", "unauthorized", ip)
|
|
httpx.WriteError(w, http.StatusUnauthorized, "unauthorized", "旧密码错误")
|
|
return
|
|
}
|
|
newPHC, err := h.hash.Hash(r.Context(), auth.PasswordAdmin, req.NewPassword)
|
|
if err != nil {
|
|
h.audit(actorString(p), "password_change", "", "error", ip)
|
|
httpx.WriteError(w, http.StatusInternalServerError, "internal", "内部错误")
|
|
return
|
|
}
|
|
if err := h.setAdminPasswordHash(r.Context(), newPHC); err != nil {
|
|
h.audit(actorString(p), "password_change", "", "error", ip)
|
|
httpx.WriteError(w, http.StatusInternalServerError, "internal", "内部错误")
|
|
return
|
|
}
|
|
// 保留当前会话,作废其它会话
|
|
if p.Session != "" {
|
|
_ = h.deleteOtherSessions(r.Context(), hashSessionHex(p.Session))
|
|
}
|
|
h.audit(actorString(p), "password_change", "", "ok", ip)
|
|
httpx.WriteOK(w, map[string]any{})
|
|
}
|
|
|
|
func (h *Handler) handleNotImplemented(w http.ResponseWriter, r *http.Request) {
|
|
p, _ := principalFrom(r.Context())
|
|
ip := httpx.ClientIP(r, h.trusted)
|
|
action := strings.ToLower(r.Method) + " " + r.URL.Path
|
|
if isMutating(r.Method) {
|
|
h.audit(actorString(p), action, "", "not_implemented", ip)
|
|
}
|
|
httpx.WriteError(w, http.StatusNotImplemented, "not_implemented", "接口尚未实现")
|
|
}
|