package web import ( "encoding/base64" "errors" "net/http" "strings" "time" "gardomatic.kleiax.de/lib/client" ) type signInForm struct { CSRFToken string `form:"csrf_token"` Email string `form:"email"` Password string `form:"password"` RememberEmail bool `form:"remember_email"` ReturnTo string `form:"return_to"` Errors map[string]string Message string } type activationForm struct { CSRFToken string `form:"csrf_token"` Token string `form:"token"` Password string `form:"password"` PasswordConfirm string `form:"password_confirm"` SetPassword bool `form:"set_password"` Errors map[string]string Message string } func (app *application) signIn(w http.ResponseWriter, r *http.Request) { returnTo := safeReturnPath(r.URL.Query().Get("return_to")) if app.isAuthenticated(r) { if returnTo != "" { http.Redirect(w, r, returnTo, http.StatusSeeOther) return } http.Redirect(w, r, app.authenticatedLandingPage(r), http.StatusSeeOther) return } data := app.newTemplateData(r) form := signInForm{ReturnTo: returnTo, Errors: make(map[string]string)} if cookie, err := r.Cookie("gardomatic_remembered_email"); err == nil { if decoded, decodeErr := base64.RawURLEncoding.DecodeString(cookie.Value); decodeErr == nil { form.Email, form.RememberEmail = string(decoded), true } } data.Form = form app.render(w, http.StatusOK, "signin.tmpl", data) } func (app *application) signInPost(w http.ResponseWriter, r *http.Request) { var form signInForm if err := app.decodePostForm(r, &form); err != nil { app.clientError(w, http.StatusBadRequest) return } form.Email = strings.TrimSpace(form.Email) form.ReturnTo = safeReturnPath(form.ReturnTo) form.Errors = make(map[string]string) if form.Email == "" { form.Errors["email"] = "E-Mail-Adresse ist erforderlich." } if form.Password == "" { form.Errors["password"] = "Passwort ist erforderlich." } if len(form.Errors) != 0 { data := app.newTemplateData(r) data.Form = form app.render(w, http.StatusUnprocessableEntity, "signin.tmpl", data) return } apiClient := client.FromContext(r.Context()) user, response, err := apiClient.CreateSession(r.Context(), client.Credentials{Email: form.Email, Password: form.Password}) if err != nil { var apiError *client.APIError if errors.As(err, &apiError) && (apiError.StatusCode == http.StatusUnauthorized || apiError.StatusCode == http.StatusUnprocessableEntity) { form.Message = "E-Mail-Adresse oder Passwort ist ungültig." data := app.newTemplateData(r) data.Form = form app.render(w, http.StatusUnprocessableEntity, "signin.tmpl", data) return } app.serverError(w, err) return } client.ForwardCookies(w, response) remembered := &http.Cookie{Name: "gardomatic_remembered_email", Path: webPath("login"), HttpOnly: true, Secure: app.config.CookieSecure, SameSite: http.SameSiteLaxMode} if form.RememberEmail { remembered.Value = base64.RawURLEncoding.EncodeToString([]byte(form.Email)) remembered.Expires = time.Now().Add(365 * 24 * time.Hour) remembered.MaxAge = 365 * 24 * 60 * 60 } else if _, err := r.Cookie("gardomatic_remembered_email"); err == nil { remembered.Expires = time.Unix(1, 0) remembered.MaxAge = -1 } else { remembered = nil } if remembered != nil { http.SetCookie(w, remembered) } if !user.Activated { http.Redirect(w, r, webPath("activate"), http.StatusSeeOther) return } if form.ReturnTo != "" { http.Redirect(w, r, form.ReturnTo, http.StatusSeeOther) return } http.Redirect(w, r, pathWithQuery(webPath("gardens"), "auto", 1), http.StatusSeeOther) } func (app *application) activateUser(w http.ResponseWriter, r *http.Request) { if user, ok := userFromContext(r.Context()); ok && user.Activated { http.Redirect(w, r, webPath("gardens"), http.StatusSeeOther) return } data := app.newTemplateData(r) data.Form = activationForm{ Token: strings.TrimSpace(r.URL.Query().Get("token")), SetPassword: r.URL.Query().Get("set-password") == "1", Errors: make(map[string]string), } app.render(w, http.StatusOK, "activate.tmpl", data) } func (app *application) activateUserPost(w http.ResponseWriter, r *http.Request) { var form activationForm if err := app.decodePostForm(r, &form); err != nil { app.clientError(w, http.StatusBadRequest) return } form.Token = strings.TrimSpace(form.Token) form.Errors = make(map[string]string) if form.Token == "" { form.Errors["token"] = "Aktivierungstoken ist erforderlich." } if form.SetPassword { if len(form.Password) < 8 { form.Errors["password"] = "Das Passwort muss mindestens 8 Zeichen lang sein." } else if len(form.Password) > 72 { form.Errors["password"] = "Das Passwort darf höchstens 72 Zeichen lang sein." } if form.Password != form.PasswordConfirm { form.Errors["password_confirm"] = "Die Passwörter stimmen nicht überein." } } if len(form.Errors) == 0 { apiClient := client.FromContext(r.Context()) var err error if form.SetPassword { _, _, err = apiClient.ActivateInvitedUser(r.Context(), form.Token, form.Password) } else { _, _, err = apiClient.ActivateUser(r.Context(), form.Token) } if err == nil { http.Redirect(w, r, webPath("gardens"), http.StatusSeeOther) return } var apiError *client.APIError if errors.As(err, &apiError) && apiError.StatusCode == http.StatusUnprocessableEntity { if _, ok := apiError.Validation["token"]; ok { form.Errors["token"] = "Aktivierungstoken ist ungültig oder abgelaufen." } else { form.Errors = apiError.Validation form.Message = "Der Account konnte nicht aktiviert werden." } } else { app.handleAPIError(w, r, err) return } } data := app.newTemplateData(r) data.Form = form app.render(w, http.StatusUnprocessableEntity, "activate.tmpl", data) } func (app *application) authenticatedLandingPage(r *http.Request) string { if user, ok := userFromContext(r.Context()); ok && !user.Activated { return webPath("activate") } return webPath("gardens") } func (app *application) signOutPost(w http.ResponseWriter, r *http.Request) { apiClient := client.FromContext(r.Context()) response, err := apiClient.DeleteSession(r.Context()) if err != nil { app.handleAPIError(w, r, err) return } client.ForwardCookies(w, response) http.Redirect(w, r, webPath("home"), http.StatusSeeOther) }