camper/pkg/app/user.go

183 lines
5.2 KiB
Go

/*
* SPDX-FileCopyrightText: 2023 jordi fita mas <jfita@peritasoft.com>
* SPDX-License-Identifier: AGPL-3.0-only
*/
package app
import (
"context"
"net/http"
"golang.org/x/text/language"
"dev.tandem.ws/tandem/camper/pkg/auth"
"dev.tandem.ws/tandem/camper/pkg/database"
"dev.tandem.ws/tandem/camper/pkg/form"
httplib "dev.tandem.ws/tandem/camper/pkg/http"
"dev.tandem.ws/tandem/camper/pkg/locale"
"dev.tandem.ws/tandem/camper/pkg/template"
)
func (h *App) getUser(r *http.Request, conn *database.Conn) (*auth.User, error) {
cookie := auth.GetSessionCookie(r)
if _, err := conn.Exec(r.Context(), "select set_cookie($1)", cookie); err != nil {
return nil, err
}
user := &auth.User{
Email: "",
LoggedIn: false,
Role: "guest",
}
row := conn.QueryRow(r.Context(), "select coalesce(email, ''), email is not null, role, lang_tag, csrf_token from user_profile")
var langTag string
if err := row.Scan(&user.Email, &user.LoggedIn, &user.Role, &langTag, &user.CSRFToken); err != nil {
return nil, err
}
if lang, err := language.Parse(langTag); err == nil {
user.Language = lang
} else {
return nil, err
}
user.Locale = h.locales[user.Language]
if user.Locale == nil {
user.Locale = h.matchLocale(r)
}
return user, nil
}
func (h *App) matchLocale(r *http.Request) *locale.Locale {
l := locale.Match(r.Header.Get("Accept-Language"), h.locales, h.languageMatcher)
if l == nil {
l = h.defaultLocale
}
return l
}
func profileHandler(user *auth.User, conn *database.Conn) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var head string
head, r.URL.Path = shiftPath(r.URL.Path)
switch head {
case "session":
switch r.Method {
case http.MethodDelete:
handleLogout(w, r, user, conn)
default:
methodNotAllowed(w, r, http.MethodDelete)
}
case "":
switch r.Method {
case http.MethodGet:
serveProfileForm(w, r, user, conn)
case http.MethodPut:
updateProfile(w, r, user, conn)
default:
methodNotAllowed(w, r, http.MethodGet, http.MethodPut)
}
default:
http.NotFound(w, r)
}
}
}
func serveProfileForm(w http.ResponseWriter, r *http.Request, user *auth.User, conn *database.Conn) {
profile := newProfileForm(r.Context(), user.Locale, conn)
profile.Name.Val = conn.MustGetText(r.Context(), "select name from user_profile")
profile.Email.Val = user.Email
profile.Language.Selected = []string{user.Language.String()}
profile.MustRender(w, r, user)
}
func updateProfile(w http.ResponseWriter, r *http.Request, user *auth.User, conn *database.Conn) {
profile := newProfileForm(r.Context(), user.Locale, conn)
if err := profile.Parse(r); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
if err := user.VerifyCSRFToken(r); err != nil {
http.Error(w, err.Error(), http.StatusForbidden)
return
}
if !profile.Valid(user.Locale) {
if !httplib.IsHTMxRequest(r) {
w.WriteHeader(http.StatusUnprocessableEntity)
}
profile.MustRender(w, r, user)
return
}
//goland:noinspection SqlWithoutWhere
cookie := conn.MustGetText(r.Context(), "update user_profile set name = $1, email = $2, lang_tag = $3 returning build_cookie()", profile.Name, profile.Email, profile.Language)
auth.SetSessionCookie(w, cookie)
if profile.Password.Val != "" {
conn.MustExec(r.Context(), "select change_password($1)", profile.Password)
}
if user.Language.String() == profile.Language.String() {
httplib.Relocate(w, r, "/me", http.StatusSeeOther)
} else {
httplib.Redirect(w, r, "/me", http.StatusSeeOther)
}
}
type profileForm struct {
Name *form.Input
Email *form.Input
Password *form.Input
PasswordConfirm *form.Input
Language *form.Select
}
func newProfileForm(ctx context.Context, l *locale.Locale, conn *database.Conn) *profileForm {
automaticOption := l.Pgettext("Automatic", "language option")
languages := form.MustGetOptions(ctx, conn, "select 'und', $1 union all select lang_tag, endonym from language where selectable", automaticOption)
return &profileForm{
Name: &form.Input{
Name: "name",
},
Email: &form.Input{
Name: "email",
},
Password: &form.Input{
Name: "password",
},
PasswordConfirm: &form.Input{
Name: "password_confirm",
},
Language: &form.Select{
Name: "language",
Options: languages,
},
}
}
func (f *profileForm) Parse(r *http.Request) error {
if err := r.ParseForm(); err != nil {
return err
}
f.Email.FillValue(r)
f.Name.FillValue(r)
f.Password.FillValue(r)
f.PasswordConfirm.FillValue(r)
f.Language.FillValue(r)
return nil
}
func (f *profileForm) Valid(l *locale.Locale) bool {
v := form.NewValidator(l)
if v.CheckRequired(f.Email, l.GettextNoop("Email can not be empty.")) {
v.CheckValidEmail(f.Email, l.GettextNoop("This email is not valid. It should be like name@domain.com."))
}
v.CheckRequired(f.Name, l.GettextNoop("Name can not be empty."))
v.CheckPasswordConfirmation(f.Password, f.PasswordConfirm, l.GettextNoop("Confirmation does not match password."))
v.CheckSelectedOptions(f.Language, l.GettextNoop("Selected language is not valid."))
return v.AllOK
}
func (f *profileForm) MustRender(w http.ResponseWriter, r *http.Request, user *auth.User) {
template.MustRender(w, r, user, "profile.gohtml", f)
}