internal/httpd/account.go

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

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