package auth import ( "context" "encoding/json" "errors" "net/http" "strconv" "strings" "time" "git.ilapage.cn/ila/yovision/Bell/server/app/audit" "git.ilapage.cn/ila/yovision/Bell/server/app/rbac" ) const CookieName = "bell_session" type HTTP struct { Service Service Store Store Audit audit.Store SecureCookie bool } func (h HTTP) Register(mux *http.ServeMux) { mux.HandleFunc("POST /api/v1/auth/login", h.login) mux.Handle("GET /api/v1/auth/me", h.authenticated(http.HandlerFunc(h.me))) mux.Handle("POST /api/v1/auth/logout", h.authenticated(http.HandlerFunc(h.logout))) mux.Handle("GET /api/v1/users", h.Require(rbac.UsersRead, http.HandlerFunc(h.users))) mux.Handle("POST /api/v1/users", h.Require(rbac.UsersWrite, http.HandlerFunc(h.createUser))) mux.Handle("PUT /api/v1/users/{id}/roles", h.Require(rbac.UsersWrite, http.HandlerFunc(h.replaceRoles))) mux.Handle("GET /api/v1/audit", h.Require(rbac.AuditRead, http.HandlerFunc(h.auditEntries))) } func (h HTTP) login(w http.ResponseWriter, r *http.Request) { var input LoginInput if err := decodeJSON(w, r, &input); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } result, err := h.Service.Login(r.Context(), input) if err != nil { status := http.StatusInternalServerError if errors.Is(err, ErrInvalidCredentials) { status = http.StatusUnauthorized } writeJSON(w, status, map[string]string{"error": err.Error()}) return } h.setCookie(w, result.Token, time.Now().Add(8*time.Hour)) result.User.PasswordHash = "" writeJSON(w, http.StatusOK, map[string]any{"user": result.User, "expires_at": result.ExpiresAt}) } func (h HTTP) me(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, principal(r.Context())) } func (h HTTP) logout(w http.ResponseWriter, r *http.Request) { cookie, _ := r.Cookie(CookieName) token := "" if cookie != nil { token = cookie.Value } user := principal(r.Context()) if err := h.Service.Logout(r.Context(), token, user); err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "退出失败"}) return } h.setCookie(w, "", time.Unix(0, 0)) w.WriteHeader(http.StatusNoContent) } func (h HTTP) users(w http.ResponseWriter, r *http.Request) { users, err := h.Store.ListUsers(r.Context()) if err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "读取用户失败"}) return } writeJSON(w, http.StatusOK, map[string]any{"items": users}) } func (h HTTP) createUser(w http.ResponseWriter, r *http.Request) { var input struct { Username string `json:"username"` DisplayName string `json:"display_name"` Password string `json:"password"` Role string `json:"role"` } if err := decodeJSON(w, r, &input); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } input.Username = strings.ToLower(strings.TrimSpace(input.Username)) if input.Username == "" || input.DisplayName == "" { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "用户名和显示名称必填"}) return } hash, err := hashPassword(input.Password) if err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": err.Error()}) return } id, err := h.Store.CreateUser(r.Context(), input.Username, input.DisplayName, hash, input.Role) if err != nil { writeJSON(w, http.StatusConflict, map[string]string{"error": "用户或角色无效"}) return } actor := principal(r.Context()) _ = h.Audit.Record(r.Context(), &actor.ID, "rbac.user_created", "user", &id, "success", map[string]any{"role": input.Role}) writeJSON(w, http.StatusCreated, map[string]string{"id": id}) } func (h HTTP) replaceRoles(w http.ResponseWriter, r *http.Request) { var input struct { Roles []string `json:"roles"` } if err := decodeJSON(w, r, &input); err != nil || len(input.Roles) == 0 { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "至少选择一个角色"}) return } id := r.PathValue("id") if err := h.Store.ReplaceRoles(r.Context(), id, input.Roles); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{"error": "角色更新失败"}) return } actor := principal(r.Context()) _ = h.Audit.Record(r.Context(), &actor.ID, "rbac.roles_replaced", "user", &id, "success", map[string]any{"roles": input.Roles}) w.WriteHeader(http.StatusNoContent) } func (h HTTP) auditEntries(w http.ResponseWriter, r *http.Request) { limit, _ := strconv.Atoi(r.URL.Query().Get("limit")) entries, err := h.Audit.List(r.Context(), limit) if err != nil { writeJSON(w, http.StatusInternalServerError, map[string]string{"error": "读取审计失败"}) return } writeJSON(w, http.StatusOK, map[string]any{"items": entries}) } func (h HTTP) authenticated(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { cookie, err := r.Cookie(CookieName) if err != nil { writeJSON(w, http.StatusUnauthorized, map[string]string{"error": ErrUnauthorized.Error()}) return } user, err := h.Service.Authenticate(r.Context(), cookie.Value) if err != nil { writeJSON(w, http.StatusUnauthorized, map[string]string{"error": ErrUnauthorized.Error()}) return } next.ServeHTTP(w, r.WithContext(context.WithValue(r.Context(), principalKey{}, user))) }) } func (h HTTP) Require(permission string, next http.Handler) http.Handler { return h.authenticated(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { user := principal(r.Context()) if !user.Permissions.Has(permission) { _ = h.Audit.Record(r.Context(), &user.ID, "rbac.denied", "permission", &permission, "denied", map[string]any{}) writeJSON(w, http.StatusForbidden, map[string]string{"error": "没有执行此操作的权限"}) return } next.ServeHTTP(w, r) })) } func (h HTTP) setCookie(w http.ResponseWriter, value string, expires time.Time) { http.SetCookie(w, &http.Cookie{Name: CookieName, Value: value, Path: "/", HttpOnly: true, Secure: h.SecureCookie, SameSite: http.SameSiteStrictMode, Expires: expires, MaxAge: int(time.Until(expires).Seconds())}) } func principal(ctx context.Context) User { user, _ := ctx.Value(principalKey{}).(User); return user } func Principal(ctx context.Context) User { return principal(ctx) } func decodeJSON(w http.ResponseWriter, r *http.Request, value any) error { r.Body = http.MaxBytesReader(w, r.Body, 1<<20) decoder := json.NewDecoder(r.Body) decoder.DisallowUnknownFields() return decoder.Decode(value) } func writeJSON(w http.ResponseWriter, status int, value any) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(value) }