package admin import ( "database/sql" "errors" "net/http" "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{}) }