internal/httpd/account.go

v1.18.0
gitbay/internal/httpd/account.go history · blame · raw

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}