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