internal/httpd/account.go
235 lines · 6909 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 watchOn, _ := s.st.WatchEnabled(u.ID)
58
59 s.render(w, "account.html", struct {
60 basePage
61 Tab string // marks the rail's Settings row as current
62 Keys []accountKey
63 PGP []accountPGP
64 Emails []store.Email
65 Profile control.ProfileOut
66 LinksText string
67 Host string
68 Notice string
69 Message string
70 MailOn bool
71 WatchOn bool
72 }{s.baseFor(u), "account", keys, pgp, emails, profile, profileLinksText(profile.Links), s.cfg.SiteHost(),
73 s.takeFlash(w, r), r.URL.Query().Get("m"), mailOn, watchOn})
74}
75
76// accountExport hands the browser the same bundle `account export`
77// writes. The command is ReadOnly, so a GET is enough; the response is an
78// attachment rather than a page because the bundle is a file to keep.
79func (s *Server) accountExport(w http.ResponseWriter, r *http.Request, u store.User) {
80 out, msg, code := s.runControlCode(u, []string{"account", "export"})
81 if code != protocol.ExitOK {
82 s.setFlash(w, msg)
83 http.Redirect(w, r, "/settings", http.StatusSeeOther)
84 return
85 }
86 w.Header().Set("Content-Type", "application/json")
87 w.Header().Set("X-Content-Type-Options", "nosniff")
88 w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", u.Username+".bundle"))
89 io.WriteString(w, out)
90}
91
92// profileLinksText turns a profile's links into the form the textarea
93// shows and reads back: one per line, "label|url" when there is a label
94// and the bare url otherwise.
95func profileLinksText(links []store.ProfileLink) string {
96 lines := make([]string, len(links))
97 for i, l := range links {
98 if l.Label != "" {
99 lines[i] = l.Label + "|" + l.URL
100 } else {
101 lines[i] = l.URL
102 }
103 }
104 return strings.Join(lines, "\n")
105}
106
107// profileLinkArgs turns the textarea back into the --link values profile
108// set expects: one per non-blank line, or a single empty one to clear the
109// list when the field was emptied.
110func profileLinkArgs(raw string) []string {
111 var links []string
112 for _, line := range strings.Split(raw, "\n") {
113 if line = strings.TrimSpace(line); line != "" {
114 links = append(links, line)
115 }
116 }
117 if links == nil {
118 return []string{""}
119 }
120 return links
121}
122
123// accountSubmit routes the account forms to their commands. Keys,
124// addresses and the profile are the whole surface — no secret is accepted
125// over the web.
126func (s *Server) accountSubmit(w http.ResponseWriter, r *http.Request, u store.User) {
127 back := func(msg, note string) {
128 q := ""
129 if note != "" {
130 q = "?m=" + url.QueryEscape(note)
131 }
132 s.setFlash(w, msg)
133 http.Redirect(w, r, "/settings"+q, http.StatusSeeOther)
134 }
135
136 switch r.FormValue("field") {
137 case "key-add":
138 body := strings.TrimSpace(r.FormValue("key"))
139 if body == "" {
140 back("paste a public key in authorized_keys format", "")
141 return
142 }
143 argv := []string{"keys", "add"}
144 if scope := r.FormValue("scope"); scope == "git" {
145 argv = append(argv, "--scope", "git")
146 }
147 if label := strings.TrimSpace(r.FormValue("label")); label != "" {
148 argv = append(argv, "--label", label)
149 }
150 if msg, ok := s.runControlStdin(u, argv, body+"\n"); !ok {
151 back(msg, "")
152 return
153 }
154 back("", "key registered")
155 case "key-remove":
156 if _, msg, ok := s.runControl(u, []string{"keys", "remove", r.FormValue("fingerprint")}); !ok {
157 back(msg, "")
158 return
159 }
160 back("", "key removed")
161 case "pgp-add":
162 body := strings.TrimSpace(r.FormValue("key"))
163 if body == "" {
164 back("paste an armored OpenPGP public key", "")
165 return
166 }
167 if msg, ok := s.runControlStdin(u, []string{"pgp", "add"}, body+"\n"); !ok {
168 back(msg, "")
169 return
170 }
171 back("", "PGP key registered")
172 case "pgp-remove":
173 if _, msg, ok := s.runControl(u, []string{"pgp", "remove", r.FormValue("fingerprint")}); !ok {
174 back(msg, "")
175 return
176 }
177 back("", "PGP key removed")
178 case "email-add":
179 if _, msg, ok := s.runControl(u, []string{"email", "add", strings.TrimSpace(r.FormValue("address"))}); !ok {
180 back(msg, "")
181 return
182 }
183 back("", "check that inbox for a verification code")
184 case "email-verify":
185 if _, msg, ok := s.runControl(u, []string{"email", "verify", strings.TrimSpace(r.FormValue("code"))}); !ok {
186 back(msg, "")
187 return
188 }
189 back("", "address verified")
190 case "email-remove":
191 if _, msg, ok := s.runControl(u, []string{"email", "remove", r.FormValue("address")}); !ok {
192 back(msg, "")
193 return
194 }
195 back("", "address removed")
196 case "email-primary":
197 if _, msg, ok := s.runControl(u, []string{"email", "primary", r.FormValue("address")}); !ok {
198 back(msg, "")
199 return
200 }
201 back("", "primary address changed")
202 case "notify-mail", "notify-watch":
203 pref := strings.TrimPrefix(r.FormValue("field"), "notify-")
204 state := "off"
205 if r.FormValue(pref) == "on" {
206 state = "on"
207 }
208 if _, msg, ok := s.runControl(u, []string{"notifications", "settings", pref, state}); !ok {
209 back(msg, "")
210 return
211 }
212 back("", "notification preferences saved")
213 case "profile":
214 format := r.FormValue("format")
215 if format != "org" {
216 format = "md"
217 }
218 argv := []string{"profile", "set",
219 "--description", r.FormValue("description"),
220 "--website", r.FormValue("website"),
221 "--about-format", format,
222 "--file", "-",
223 }
224 for _, link := range profileLinkArgs(r.FormValue("links")) {
225 argv = append(argv, "--link", link)
226 }
227 if msg, ok := s.runControlStdin(u, argv, r.FormValue("about")); !ok {
228 back(msg, "")
229 return
230 }
231 back("", "profile updated")
232 default:
233 back("unknown form", "")
234 }
235}