login.go

  1package pages
  2
  3import (
  4	"encoding/json"
  5	errs "errors"
  6	"fmt"
  7	"net/http"
  8	"net/url"
  9
 10	"git.kilimanjaro.io/rtw/httperr"
 11	"git.kilimanjaro.io/rtw/pkg/id"
 12	"git.kilimanjaro.io/rtw/pkg/log"
 13	"git.kilimanjaro.io/rtw/pkg/session"
 14	"git.kilimanjaro.io/rtw/user"
 15)
 16
 17func NewLoginSignupHandler(us *user.Service, eh *httperr.Handler, isSignUp bool) http.Handler {
 18	return &loginHandler{
 19		us:     us,
 20		eh:     eh,
 21		signup: isSignUp,
 22	}
 23}
 24
 25type loginHandler struct {
 26	us     *user.Service
 27	eh     *httperr.Handler
 28	signup bool
 29}
 30
 31func (h *loginHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
 32	l := log.FromContext(r.Context())
 33	switch r.Method {
 34	case "GET", "":
 35		next := r.URL.Query().Get("next")
 36		userInfo, ok := h.us.HasValidRegisteredSession(r)
 37		if ok {
 38			// already logged in, go to user home
 39			// TODO: determine if there's a case where next could be set
 40			http.Redirect(w, r, h.us.HomeURL(userInfo.ID), http.StatusFound)
 41			return
 42		}
 43		if !h.signup {
 44			l.Debug("render login page", "next", next)
 45			Login("").Render(r.Context(), w)
 46			return
 47		}
 48		l.Debug("render signup page", "next", next)
 49		Signup("").Render(r.Context(), w)
 50		return
 51	case "POST":
 52		// Detect if there is an existing anonymous session to migrate
 53		var anonUserID id.Key = id.NotExist
 54		if userInfo, ok := h.us.HasValidSession(r); ok && userInfo.Type == session.Anonymous {
 55			anonUserID = userInfo.ID
 56		}
 57		if anonUserID == id.NotExist {
 58			l.Debug("login handler: no existing user session")
 59		} else {
 60			l.Debug("login handler: existing user session", "user_id", anonUserID)
 61		}
 62
 63		var userID id.Key
 64		if !h.signup {
 65			// logging in
 66			var emailOrHandle, password string
 67			ctype := r.Header.Get("Content-Type")
 68			if ctype == "application/json" {
 69				// for client requests from JS frontend
 70				defer r.Body.Close()
 71				var body struct {
 72					User     string
 73					Password string
 74				}
 75				if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
 76					l.Error("invalid login JSON format", "error", err)
 77					writeJSONError(http.StatusBadRequest, w, r, "invalid request")
 78					return
 79				}
 80				emailOrHandle = body.User
 81				password = body.Password
 82			} else {
 83				// for server-rendered pages
 84				emailOrHandle = r.FormValue("user")
 85				password = r.FormValue("password")
 86			}
 87			l.Info("login attempt", "user", emailOrHandle, "password_len", len(password))
 88			if emailOrHandle == "" || password == "" {
 89				if ctype == "application/json" {
 90					writeJSONError(http.StatusBadRequest, w, r, "Email/handle and password are required")
 91					return
 92				}
 93				e := "Email/handle and password are required"
 94				Login(e).Render(r.Context(), w)
 95				return
 96			}
 97			var err error
 98			if anonUserID != id.NotExist {
 99				userID, err = h.us.LoginWithExistingSession(emailOrHandle, password, anonUserID)
100			} else {
101				userID, err = h.us.Login(emailOrHandle, password)
102			}
103			if err != nil {
104				if errs.Is(err, user.ErrLoginFailed) {
105					if ctype == "application/json" {
106						writeJSONError(http.StatusUnauthorized, w, r, "User or password is incorrect")
107						return
108					}
109					e := "User or password is incorrect"
110					Login(e).Render(r.Context(), w)
111					return
112				}
113				// System error from LoginWithExistingSession (migration, deletion, etc.)
114				if ctype == "application/json" {
115					writeJSONError(http.StatusInternalServerError, w, r, "system error")
116					return
117				}
118				h.eh.Handle(http.StatusInternalServerError, w, r, fmt.Errorf("login: %w", err))
119				return
120			}
121			if _, err := h.us.CreateNewUserSession(userID, w); err != nil {
122				if ctype == "application/json" {
123					writeJSONError(http.StatusInternalServerError, w, r, "system error")
124					return
125				}
126				h.eh.Handle(http.StatusInternalServerError, w, r, err)
127				return
128			}
129		} else {
130			// signing up
131			ctype := r.Header.Get("Content-Type")
132			var userP, handleP *string = nil, nil
133			var password string
134			if ctype == "application/json" {
135				// for requests from the client JS frontend
136				var body struct {
137					User     string
138					Handle   string
139					Password string
140				}
141				defer r.Body.Close()
142				if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
143					writeJSONError(http.StatusBadRequest, w, r, "invalid request body")
144					return
145				}
146				if body.User != "" {
147					userP = &body.User
148				}
149				if body.Handle != "" {
150					handleP = &body.Handle
151				}
152				password = body.Password
153			} else {
154				// for server-rendered page
155				us := r.FormValue("user")
156				if us != "" {
157					userP = &us
158				}
159				handle := r.FormValue("handle")
160				if handle != "" {
161					handleP = &handle
162				}
163				password = r.FormValue("password")
164			}
165			if handleP == nil && userP == nil || password == "" {
166				if ctype == "application/json" {
167					writeJSONError(http.StatusBadRequest, w, r, "A handle or an email and a password are required")
168					return
169				}
170				e := "A handle or an email and a password are required"
171				Signup(e).Render(r.Context(), w)
172				return
173			}
174
175			var err error
176			if anonUserID != id.NotExist {
177				userID, err = h.us.SignupWithExistingSession(userP, handleP, password, anonUserID)
178			} else {
179				userID, err = h.us.Signup(userP, handleP, password)
180			}
181			if err != nil && ctype != "application/json" {
182				switch {
183				case errs.Is(err, user.ErrInvalidHandle):
184					e := "Not a valid handle"
185					Signup(e).Render(r.Context(), w)
186					return
187				case errs.Is(err, user.ErrHandleExists):
188					e := "Sorry someone has that handle already. Try another."
189					Signup(e).Render(r.Context(), w)
190					return
191				case errs.Is(err, user.ErrAccountExists):
192					e := "Account already exists with that email"
193					Signup(e).Render(r.Context(), w)
194					return
195				default:
196					h.eh.Handle(http.StatusInternalServerError, w, r, fmt.Errorf("signup form: %w", err))
197					return
198				}
199			}
200			if err != nil && ctype == "application/json" {
201				switch {
202				case errs.Is(err, user.ErrInvalidHandle):
203					e := "Not a valid handle"
204					writeJSONError(http.StatusBadRequest, w, r, e)
205					return
206				case errs.Is(err, user.ErrHandleExists):
207					e := "Sorry someone has that handle already. Try another."
208					writeJSONError(http.StatusBadRequest, w, r, e)
209					return
210				case errs.Is(err, user.ErrAccountExists):
211					e := "Account already exists with that email"
212					writeJSONError(http.StatusBadRequest, w, r, e)
213					return
214				default:
215					writeJSONError(http.StatusInternalServerError, w, r, fmt.Sprintf("system error: %s", err))
216					return
217				}
218			}
219			if _, err := h.us.CreateNewUserSession(userID, w); err != nil {
220				if ctype == "application/json" {
221					writeJSONError(http.StatusInternalServerError, w, r, fmt.Sprintf("system error: %s", err))
222					return
223				}
224				h.eh.Handle(http.StatusInternalServerError, w, r, err)
225				return
226			}
227		}
228		// for client requests, return 200 OK with user data
229		if r.Header.Get("Content-Type") == "application/json" {
230			user, err := h.us.GetUserByID(userID)
231			if err != nil {
232				writeJSONError(http.StatusInternalServerError, w, r, err.Error())
233				return
234			}
235			w.Header().Set("Content-Type", "application/json")
236			w.WriteHeader(http.StatusOK)
237			if err := json.NewEncoder(w).Encode(user); err != nil {
238				writeJSONError(http.StatusInternalServerError, w, r, err.Error())
239			}
240			return
241		}
242		// redirect to value of next or user home
243		next := r.URL.Query().Get("next")
244		if next != "" {
245			nextURL, err := url.QueryUnescape(next)
246			if err != nil {
247				nextURL = h.us.HomeURL(userID)
248			}
249			if r.Header.Get("HX-Request") == "true" {
250				w.Header().Set("HX-Redirect", nextURL)
251				w.WriteHeader(http.StatusOK)
252				return
253			}
254			http.Redirect(w, r, nextURL, http.StatusSeeOther)
255			return
256		}
257		homeURL := h.us.HomeURL(userID)
258		if r.Header.Get("HX-Request") == "true" {
259			w.Header().Set("HX-Redirect", homeURL)
260			w.WriteHeader(http.StatusOK)
261			return
262		}
263		http.Redirect(w, r, homeURL, http.StatusSeeOther)
264		return
265	default:
266		h.eh.Handle(http.StatusInternalServerError, w, r, fmt.Errorf("login page: got unexpected request %s", r.Method))
267	}
268}
269
270func writeJSONError(status int, w http.ResponseWriter, _ *http.Request, message string) {
271	w.Header().Set("Content-Type", "application/json")
272	w.WriteHeader(status)
273	json.NewEncoder(w).Encode(map[string]string{"error": message})
274}