Files
2026-09-16 05:36:36 +02:00

203 lines
6.3 KiB
Go

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)
}