internal/httpd/account.go
232 lines · 6770 bytes
1package httpd
2
3import (
4 "encoding/json"
5 "fmt"
6 "io"
7 "net/http"
8 "net/url"
9 "strings"
10
11 "gitbay.org/gitbay/internal/control"
12 "gitbay.org/gitbay/internal/protocol"
13 "gitbay.org/gitbay/internal/store"
14)
15
16// accountKey is one SSH key as the settings page shows it: enough to
17// recognise which key this is without printing the whole blob.
18type accountKey struct {
19 Fingerprint string
20 Algo string
21 Scope string
22 Label string
23}
24
25type accountPGP struct {
26 Fingerprint string
27 UIDs []string
28 Expired bool
29 Revoked bool
30}
31
32// accountForm renders the account's own settings: keys, addresses, and the
33// commands for everything that stays on SSH.
34func (s *Server) accountForm(w http.ResponseWriter, r *http.Request, u store.User) {
35 var keys []accountKey
36 if list, err := s.st.ListSSHKeys(u.ID); err == nil {
37 for _, k := range list {
38 keys = append(keys, accountKey{Fingerprint: k.Fingerprint, Algo: k.Algo, Scope: k.Scope, Label: k.Label})
39 }
40 }
41 var pgp []accountPGP
42 if list, err := s.st.ListPGPKeys(u.ID); err == nil {
43 for _, k := range list {
44 var uids []string
45 json.Unmarshal([]byte(k.UIDsJSON), &uids)
46 pgp = append(pgp, accountPGP{
47 Fingerprint: k.Fingerprint, UIDs: uids,
48 Expired: k.ExpiresAt != nil, Revoked: k.RevokedAt != nil,
49 })
50 }
51 }
52 emails, _ := s.st.ListEmails(u.ID)
53
54 var profile control.ProfileOut
55 s.runControlInto(u, []string{"profile", "show"}, &profile)
56 mailOn, _ := s.st.MailEnabled(u.ID)
57
58 s.render(w, "account.html", struct {
59 basePage
60 Tab string // marks the rail's Settings row as current
61 Keys []accountKey
62 PGP []accountPGP
63 Emails []store.Email
64 Profile control.ProfileOut
65 LinksText string
66 Host string
67 Notice string
68 Message string
69 MailOn bool
70 }{s.baseFor(u), "account", keys, pgp, emails, profile, profileLinksText(profile.Links), s.cfg.SiteHost(),
71 s.takeFlash(w, r), r.URL.Query().Get("m"), mailOn})
72}
73
74// accountExport hands the browser the same bundle `account export`
75// writes. The command is ReadOnly, so a GET is enough; the response is an
76// attachment rather than a page because the bundle is a file to keep.
77func (s *Server) accountExport(w http.ResponseWriter, r *http.Request, u store.User) {
78 out, msg, code := s.runControlCode(u, []string{"account", "export"})
79 if code != protocol.ExitOK {
80 s.setFlash(w, msg)
81 http.Redirect(w, r, "/settings", http.StatusSeeOther)
82 return
83 }
84 w.Header().Set("Content-Type", "application/json")
85 w.Header().Set("X-Content-Type-Options", "nosniff")
86 w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", u.Username+".bundle"))
87 io.WriteString(w, out)
88}
89
90// profileLinksText turns a profile's links into the form the textarea
91// shows and reads back: one per line, "label|url" when there is a label
92// and the bare url otherwise.
93func profileLinksText(links []store.ProfileLink) string {
94 lines := make([]string, len(links))
95 for i, l := range links {
96 if l.Label != "" {
97 lines[i] = l.Label + "|" + l.URL
98 } else {
99 lines[i] = l.URL
100 }
101 }
102 return strings.Join(lines, "\n")
103}
104
105// profileLinkArgs turns the textarea back into the --link values profile
106// set expects: one per non-blank line, or a single empty one to clear the
107// list when the field was emptied.
108func profileLinkArgs(raw string) []string {
109 var links []string
110 for _, line := range strings.Split(raw, "\n") {
111 if line = strings.TrimSpace(line); line != "" {
112 links = append(links, line)
113 }
114 }
115 if links == nil {
116 return []string{""}
117 }
118 return links
119}
120
121// accountSubmit routes the account forms to their commands. Keys,
122// addresses and the profile are the whole surface — no secret is accepted
123// over the web.
124func (s *Server) accountSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
125 back := func(msg, note string) {
126 q := ""
127 if note != "" {
128 q = "?m=" + url.QueryEscape(note)
129 }
130 s.setFlash(w, msg)
131 http.Redirect(w, r, "/settings"+q, http.StatusSeeOther)
132 }
133
134 switch r.FormValue("field") {
135 case "key-add":
136 body := strings.TrimSpace(r.FormValue("key"))
137 if body == "" {
138 back("paste a public key in authorized_keys format", "")
139 return
140 }
141 argv := []string{"keys", "add"}
142 if scope := r.FormValue("scope"); scope == "git" {
143 argv = append(argv, "--scope", "git")
144 }
145 if label := strings.TrimSpace(r.FormValue("label")); label != "" {
146 argv = append(argv, "--label", label)
147 }
148 if msg, ok := s.runControlStdin(u, argv, body+"\n"); !ok {
149 back(msg, "")
150 return
151 }
152 back("", "key registered")
153 case "key-remove":
154 if _, msg, ok := s.runControl(u, []string{"keys", "remove", r.FormValue("fingerprint")}); !ok {
155 back(msg, "")
156 return
157 }
158 back("", "key removed")
159 case "pgp-add":
160 body := strings.TrimSpace(r.FormValue("key"))
161 if body == "" {
162 back("paste an armored OpenPGP public key", "")
163 return
164 }
165 if msg, ok := s.runControlStdin(u, []string{"pgp", "add"}, body+"\n"); !ok {
166 back(msg, "")
167 return
168 }
169 back("", "PGP key registered")
170 case "pgp-remove":
171 if _, msg, ok := s.runControl(u, []string{"pgp", "remove", r.FormValue("fingerprint")}); !ok {
172 back(msg, "")
173 return
174 }
175 back("", "PGP key removed")
176 case "email-add":
177 if _, msg, ok := s.runControl(u, []string{"email", "add", strings.TrimSpace(r.FormValue("address"))}); !ok {
178 back(msg, "")
179 return
180 }
181 back("", "check that inbox for a verification code")
182 case "email-verify":
183 if _, msg, ok := s.runControl(u, []string{"email", "verify", strings.TrimSpace(r.FormValue("code"))}); !ok {
184 back(msg, "")
185 return
186 }
187 back("", "address verified")
188 case "email-remove":
189 if _, msg, ok := s.runControl(u, []string{"email", "remove", r.FormValue("address")}); !ok {
190 back(msg, "")
191 return
192 }
193 back("", "address removed")
194 case "email-primary":
195 if _, msg, ok := s.runControl(u, []string{"email", "primary", r.FormValue("address")}); !ok {
196 back(msg, "")
197 return
198 }
199 back("", "primary address changed")
200 case "notify-mail":
201 state := "off"
202 if r.FormValue("mail") == "on" {
203 state = "on"
204 }
205 if _, msg, ok := s.runControl(u, []string{"notifications", "settings", "mail", state}); !ok {
206 back(msg, "")
207 return
208 }
209 back("", "notification preferences saved")
210 case "profile":
211 format := r.FormValue("format")
212 if format != "org" {
213 format = "md"
214 }
215 argv := []string{"profile", "set",
216 "--description", r.FormValue("description"),
217 "--website", r.FormValue("website"),
218 "--about-format", format,
219 "--file", "-",
220 }
221 for _, link := range profileLinkArgs(r.FormValue("links")) {
222 argv = append(argv, "--link", link)
223 }
224 if msg, ok := s.runControlStdin(u, argv, r.FormValue("about")); !ok {
225 back(msg, "")
226 return
227 }
228 back("", "profile updated")
229 default:
230 back("unknown form", "")
231 }
232}