package server import ( "fmt" "io" "net/http" "os" "path/filepath" "strings" "time" ) func (s *Server) handleProfilePage(w http.ResponseWriter, r *http.Request) { u := s.requireLogin(w, r) if u == nil { return } fresh, err := s.st.UserByID(u.ID) if err != nil { s.renderError(w, r, http.StatusInternalServerError, "读取账号信息失败") return } myPushes, _ := s.st.PushesByUser(u.ID) repos, _ := s.st.ListRepos("") mine := 0 for _, rc := range repos { if rc.OwnerID != nil && *rc.OwnerID == u.ID { mine++ } } pushes, _ := s.st.RecentPushes(80) var mineRecent []any for _, p := range pushes { if p.UserID != nil && *p.UserID == u.ID { mineRecent = append(mineRecent, p) } if len(mineRecent) >= 5 { break } } p := s.page(r, "账号设置") p.Active = "settings" p.Data["Profile"] = fresh p.Data["MyPushCount"] = myPushes p.Data["MyRepoCount"] = mine p.Data["MyRecent"] = mineRecent s.render(w, r, "profile", p) } func (s *Server) handleProfileSave(w http.ResponseWriter, r *http.Request) { u := s.requireLogin(w, r) if u == nil { return } if !s.checkCSRF(r) { s.renderError(w, r, http.StatusForbidden, "表单已过期,请重试") return } display := strings.TrimSpace(r.FormValue("display_name")) email := strings.TrimSpace(r.FormValue("email")) bio := strings.TrimSpace(r.FormValue("bio")) switch { case len(display) > 40: s.renderError(w, r, http.StatusBadRequest, "昵称不能超过 40 个字符") return case len(email) > 120 || (email != "" && !strings.Contains(email, "@")): s.renderError(w, r, http.StatusBadRequest, "邮箱格式不正确") return case len(bio) > 300: s.renderError(w, r, http.StatusBadRequest, "个人简介不能超过 300 个字符") return } if err := s.st.UpdateProfile(u.ID, display, email, bio, u.Avatar); err != nil { s.renderError(w, r, http.StatusInternalServerError, "保存失败:"+err.Error()) return } setFlash(w, "账号信息已更新") http.Redirect(w, r, "/settings", http.StatusFound) } func (s *Server) handlePasswordChange(w http.ResponseWriter, r *http.Request) { u := s.requireLogin(w, r) if u == nil { return } if !s.checkCSRF(r) { s.renderError(w, r, http.StatusForbidden, "表单已过期,请重试") return } current := r.FormValue("current_password") next := r.FormValue("new_password") confirm := r.FormValue("new_password2") fresh, err := s.st.UserByID(u.ID) if err != nil { s.renderError(w, r, http.StatusInternalServerError, "读取账号信息失败") return } switch { case !checkPassword(fresh.PasswordHash, current): s.renderError(w, r, http.StatusBadRequest, "当前密码不正确") return case len(next) < 6: s.renderError(w, r, http.StatusBadRequest, "新密码至少需要 6 位") return case next != confirm: s.renderError(w, r, http.StatusBadRequest, "两次输入的新密码不一致") return } hash, err := hashPassword(next) if err != nil { s.renderError(w, r, http.StatusInternalServerError, "密码处理失败") return } if err := s.st.SetPassword(u.ID, hash); err != nil { s.renderError(w, r, http.StatusInternalServerError, "保存失败:"+err.Error()) return } setFlash(w, "密码已更新,下次 git 操作请使用新密码") http.Redirect(w, r, "/settings", http.StatusFound) } // handleAvatarUpload 上传头像(png / jpg / gif / webp,最大 2MB)。 func (s *Server) handleAvatarUpload(w http.ResponseWriter, r *http.Request) { u := s.requireLogin(w, r) if u == nil { return } if !s.checkCSRF(r) { s.renderError(w, r, http.StatusForbidden, "表单已过期,请重试") return } if err := r.ParseMultipartForm(4 << 20); err != nil { s.renderError(w, r, http.StatusBadRequest, "上传内容过大或格式错误") return } if r.FormValue("remove_avatar") == "on" { s.removeAvatarFile(u.Avatar) _ = s.st.UpdateProfile(u.ID, u.DisplayName, u.Email, u.Bio, "") setFlash(w, "头像已移除") http.Redirect(w, r, "/settings", http.StatusFound) return } file, header, err := r.FormFile("avatar") if err != nil { s.renderError(w, r, http.StatusBadRequest, "请选择要上传的图片") return } defer file.Close() data, err := io.ReadAll(io.LimitReader(file, 2<<20+1)) if err != nil { s.renderError(w, r, http.StatusInternalServerError, "读取上传文件失败") return } if len(data) > 2<<20 { s.renderError(w, r, http.StatusBadRequest, "头像不能超过 2MB") return } ext := "" switch http.DetectContentType(data) { case "image/png": ext = ".png" case "image/jpeg": ext = ".jpg" case "image/gif": ext = ".gif" case "image/webp": ext = ".webp" default: s.renderError(w, r, http.StatusBadRequest, "仅支持 PNG / JPG / GIF / WebP 图片") return } name := fmt.Sprintf("%d-%d%s", u.ID, time.Now().Unix(), ext) full := filepath.Join(s.avatarDir, name) if err := os.WriteFile(full, data, 0o644); err != nil { s.renderError(w, r, http.StatusInternalServerError, "保存头像失败:"+err.Error()) return } s.removeAvatarFile(u.Avatar) if err := s.st.UpdateProfile(u.ID, u.DisplayName, u.Email, u.Bio, name); err != nil { s.renderError(w, r, http.StatusInternalServerError, "保存失败:"+err.Error()) return } setFlash(w, "头像已更新(原文件名:"+header.Filename+")") http.Redirect(w, r, "/settings", http.StatusFound) } func (s *Server) removeAvatarFile(name string) { if name == "" || name != filepath.Base(name) { return } _ = os.Remove(filepath.Join(s.avatarDir, name)) }