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}