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