internal/httpd/account.go

353f68a2e57ac692964b73ae1f9acd91fcdcae20
gitbay/internal/httpd/account.go history · blame · raw

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