gitcat
1package server
2
3import (
4 "fmt"
5 "io"
6 "net/http"
7 "os"
8 "path/filepath"
9 "strings"
10 "time"
11)
12
13func (s *Server) handleProfilePage(w http.ResponseWriter, r *http.Request) {
14 u := s.requireLogin(w, r)
15 if u == nil {
16 return
17 }
18 fresh, err := s.st.UserByID(u.ID)
19 if err != nil {
20 s.renderError(w, r, http.StatusInternalServerError, "读取账号信息失败")
21 return
22 }
23 myPushes, _ := s.st.PushesByUser(u.ID)
24 repos, _ := s.st.ListRepos("")
25 mine := 0
26 for _, rc := range repos {
27 if rc.OwnerID != nil && *rc.OwnerID == u.ID {
28 mine++
29 }
30 }
31 pushes, _ := s.st.RecentPushes(80)
32 var mineRecent []any
33 for _, p := range pushes {
34 if p.UserID != nil && *p.UserID == u.ID {
35 mineRecent = append(mineRecent, p)
36 }
37 if len(mineRecent) >= 5 {
38 break
39 }
40 }
41 p := s.page(r, "账号设置")
42 p.Active = "settings"
43 p.Data["Profile"] = fresh
44 p.Data["MyPushCount"] = myPushes
45 p.Data["MyRepoCount"] = mine
46 p.Data["MyRecent"] = mineRecent
47 s.render(w, r, "profile", p)
48}
49
50func (s *Server) handleProfileSave(w http.ResponseWriter, r *http.Request) {
51 u := s.requireLogin(w, r)
52 if u == nil {
53 return
54 }
55 if !s.checkCSRF(r) {
56 s.renderError(w, r, http.StatusForbidden, "表单已过期,请重试")
57 return
58 }
59 display := strings.TrimSpace(r.FormValue("display_name"))
60 email := strings.TrimSpace(r.FormValue("email"))
61 bio := strings.TrimSpace(r.FormValue("bio"))
62 switch {
63 case len(display) > 40:
64 s.renderError(w, r, http.StatusBadRequest, "昵称不能超过 40 个字符")
65 return
66 case len(email) > 120 || (email != "" && !strings.Contains(email, "@")):
67 s.renderError(w, r, http.StatusBadRequest, "邮箱格式不正确")
68 return
69 case len(bio) > 300:
70 s.renderError(w, r, http.StatusBadRequest, "个人简介不能超过 300 个字符")
71 return
72 }
73 if err := s.st.UpdateProfile(u.ID, display, email, bio, u.Avatar); err != nil {
74 s.renderError(w, r, http.StatusInternalServerError, "保存失败:"+err.Error())
75 return
76 }
77 setFlash(w, "账号信息已更新")
78 http.Redirect(w, r, "/settings", http.StatusFound)
79}
80
81func (s *Server) handlePasswordChange(w http.ResponseWriter, r *http.Request) {
82 u := s.requireLogin(w, r)
83 if u == nil {
84 return
85 }
86 if !s.checkCSRF(r) {
87 s.renderError(w, r, http.StatusForbidden, "表单已过期,请重试")
88 return
89 }
90 current := r.FormValue("current_password")
91 next := r.FormValue("new_password")
92 confirm := r.FormValue("new_password2")
93 fresh, err := s.st.UserByID(u.ID)
94 if err != nil {
95 s.renderError(w, r, http.StatusInternalServerError, "读取账号信息失败")
96 return
97 }
98 switch {
99 case !checkPassword(fresh.PasswordHash, current):
100 s.renderError(w, r, http.StatusBadRequest, "当前密码不正确")
101 return
102 case len(next) < 6:
103 s.renderError(w, r, http.StatusBadRequest, "新密码至少需要 6 位")
104 return
105 case next != confirm:
106 s.renderError(w, r, http.StatusBadRequest, "两次输入的新密码不一致")
107 return
108 }
109 hash, err := hashPassword(next)
110 if err != nil {
111 s.renderError(w, r, http.StatusInternalServerError, "密码处理失败")
112 return
113 }
114 if err := s.st.SetPassword(u.ID, hash); err != nil {
115 s.renderError(w, r, http.StatusInternalServerError, "保存失败:"+err.Error())
116 return
117 }
118 setFlash(w, "密码已更新,下次 git 操作请使用新密码")
119 http.Redirect(w, r, "/settings", http.StatusFound)
120}
121
122// handleAvatarUpload 上传头像(png / jpg / gif / webp,最大 2MB)。
123func (s *Server) handleAvatarUpload(w http.ResponseWriter, r *http.Request) {
124 u := s.requireLogin(w, r)
125 if u == nil {
126 return
127 }
128 if !s.checkCSRF(r) {
129 s.renderError(w, r, http.StatusForbidden, "表单已过期,请重试")
130 return
131 }
132 if err := r.ParseMultipartForm(4 << 20); err != nil {
133 s.renderError(w, r, http.StatusBadRequest, "上传内容过大或格式错误")
134 return
135 }
136 if r.FormValue("remove_avatar") == "on" {
137 s.removeAvatarFile(u.Avatar)
138 _ = s.st.UpdateProfile(u.ID, u.DisplayName, u.Email, u.Bio, "")
139 setFlash(w, "头像已移除")
140 http.Redirect(w, r, "/settings", http.StatusFound)
141 return
142 }
143 file, header, err := r.FormFile("avatar")
144 if err != nil {
145 s.renderError(w, r, http.StatusBadRequest, "请选择要上传的图片")
146 return
147 }
148 defer file.Close()
149 data, err := io.ReadAll(io.LimitReader(file, 2<<20+1))
150 if err != nil {
151 s.renderError(w, r, http.StatusInternalServerError, "读取上传文件失败")
152 return
153 }
154 if len(data) > 2<<20 {
155 s.renderError(w, r, http.StatusBadRequest, "头像不能超过 2MB")
156 return
157 }
158 ext := ""
159 switch http.DetectContentType(data) {
160 case "image/png":
161 ext = ".png"
162 case "image/jpeg":
163 ext = ".jpg"
164 case "image/gif":
165 ext = ".gif"
166 case "image/webp":
167 ext = ".webp"
168 default:
169 s.renderError(w, r, http.StatusBadRequest, "仅支持 PNG / JPG / GIF / WebP 图片")
170 return
171 }
172 name := fmt.Sprintf("%d-%d%s", u.ID, time.Now().Unix(), ext)
173 full := filepath.Join(s.avatarDir, name)
174 if err := os.WriteFile(full, data, 0o644); err != nil {
175 s.renderError(w, r, http.StatusInternalServerError, "保存头像失败:"+err.Error())
176 return
177 }
178 s.removeAvatarFile(u.Avatar)
179 if err := s.st.UpdateProfile(u.ID, u.DisplayName, u.Email, u.Bio, name); err != nil {
180 s.renderError(w, r, http.StatusInternalServerError, "保存失败:"+err.Error())
181 return
182 }
183 setFlash(w, "头像已更新(原文件名:"+header.Filename+")")
184 http.Redirect(w, r, "/settings", http.StatusFound)
185}
186
187func (s *Server) removeAvatarFile(name string) {
188 if name == "" || name != filepath.Base(name) {
189 return
190 }
191 _ = os.Remove(filepath.Join(s.avatarDir, name))
192}