internal/httpd/account.go

aa03e8c8e02357cfe5f94188fdab3c79846cebae
gitbay/internal/httpd/account.go history · blame · raw

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}