|
|
|
|
@@ -2,10 +2,13 @@ package controllers
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"crypto/md5"
|
|
|
|
|
"crypto/rand"
|
|
|
|
|
_ "embed"
|
|
|
|
|
"encoding/hex"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"errors"
|
|
|
|
|
"fmt"
|
|
|
|
|
"io"
|
|
|
|
|
"log/slog"
|
|
|
|
|
"net"
|
|
|
|
|
@@ -23,6 +26,7 @@ import (
|
|
|
|
|
"git.golem15.com/golem15/summercms/compass"
|
|
|
|
|
"git.golem15.com/golem15/summercms/lagoon"
|
|
|
|
|
"git.golem15.com/golem15/summercms/lagoon/attach"
|
|
|
|
|
"git.golem15.com/golem15/summercms/phrasebook"
|
|
|
|
|
"git.golem15.com/golem15/summercms/postcard"
|
|
|
|
|
"git.golem15.com/golem15/summercms/surf"
|
|
|
|
|
"git.golem15.com/golem15/summercms/wire"
|
|
|
|
|
@@ -32,6 +36,9 @@ import (
|
|
|
|
|
|
|
|
|
|
const avatarMaxBytes = 4_096_000
|
|
|
|
|
|
|
|
|
|
//go:embed winter_error_page.html
|
|
|
|
|
var winterErrorPage []byte
|
|
|
|
|
|
|
|
|
|
var errNoAvatar = errors.New("golem15.user: no avatar")
|
|
|
|
|
|
|
|
|
|
const invalidLoginBody = "Invalid email or password"
|
|
|
|
|
@@ -60,7 +67,7 @@ func Login(app *backpack.App) http.HandlerFunc {
|
|
|
|
|
if user != nil {
|
|
|
|
|
if err := classes.RejectIfThrottled(ctx, gdb, user, ip); err != nil {
|
|
|
|
|
if classes.IsLoginRejected(err) {
|
|
|
|
|
writeInvalidLogin(w)
|
|
|
|
|
writeInvalidLogin(w, r, app)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
writeOpaque500(w)
|
|
|
|
|
@@ -74,7 +81,7 @@ func Login(app *backpack.App) http.HandlerFunc {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
writeInvalidLogin(w)
|
|
|
|
|
writeInvalidLogin(w, r, app)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if user.DeletedAt.Valid {
|
|
|
|
|
@@ -86,7 +93,7 @@ func Login(app *backpack.App) http.HandlerFunc {
|
|
|
|
|
}
|
|
|
|
|
if err := classes.CheckAndRecordLogin(ctx, gdb, user, ip, true); err != nil {
|
|
|
|
|
if classes.IsLoginRejected(err) {
|
|
|
|
|
writeInvalidLogin(w)
|
|
|
|
|
writeInvalidLogin(w, r, app)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
writeOpaque500(w)
|
|
|
|
|
@@ -111,7 +118,7 @@ func Login(app *backpack.App) http.HandlerFunc {
|
|
|
|
|
writeOpaque500(w)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
wire.WriteJSON(w, http.StatusOK, map[string]any{"token": token, "user": payload})
|
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{"token": token, "user": payload})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -144,7 +151,7 @@ func Logout(app *backpack.App) http.HandlerFunc {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
wire.WriteJSON(w, http.StatusOK, map[string]any{"message": "Logged out"})
|
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{"message": "Logged out"})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -160,7 +167,7 @@ func Fetch(app *backpack.App) http.HandlerFunc {
|
|
|
|
|
writeOpaque500(w)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
wire.WriteJSON(w, http.StatusOK, map[string]any{"user": payload})
|
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{"user": payload})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -173,7 +180,7 @@ func Refresh(app *backpack.App) http.HandlerFunc {
|
|
|
|
|
}
|
|
|
|
|
raw := bearerToken(r)
|
|
|
|
|
if raw == "" {
|
|
|
|
|
wire.WriteJSON(w, http.StatusUnauthorized, map[string]any{"error": "Token not found"})
|
|
|
|
|
writeJSON(w, http.StatusUnauthorized, map[string]any{"error": "Token not found"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
var bl bouncer.BlacklistStore
|
|
|
|
|
@@ -182,20 +189,20 @@ func Refresh(app *backpack.App) http.HandlerFunc {
|
|
|
|
|
}
|
|
|
|
|
next, err := bouncer.Refresh(secret, raw, refreshTTL(app), bl, blacklistGrace(app), requestURL(r))
|
|
|
|
|
if err != nil {
|
|
|
|
|
wire.WriteJSON(w, http.StatusUnauthorized, map[string]any{
|
|
|
|
|
writeJSON(w, http.StatusUnauthorized, map[string]any{
|
|
|
|
|
"error": "Could not refresh token",
|
|
|
|
|
"msg": err.Error(),
|
|
|
|
|
})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
wire.WriteJSON(w, http.StatusOK, map[string]any{"token": next})
|
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{"token": next})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// OAuthProviders reports that this app configures no social providers.
|
|
|
|
|
func OAuthProviders(app *backpack.App) http.HandlerFunc {
|
|
|
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
wire.WriteJSON(w, http.StatusOK, map[string]any{
|
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
|
|
|
"success": true,
|
|
|
|
|
"providers": wire.Slice([]any{}),
|
|
|
|
|
})
|
|
|
|
|
@@ -218,7 +225,7 @@ func ForgotPassword(app *backpack.App) http.HandlerFunc {
|
|
|
|
|
}
|
|
|
|
|
errs, err := lagoon.Validate(r.Context(), gdb, models.User{}, map[string]string{
|
|
|
|
|
"email": "required|email|between:6,255",
|
|
|
|
|
}, fields, nil)
|
|
|
|
|
}, fields, appTranslator(app))
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeOpaque500(w)
|
|
|
|
|
return
|
|
|
|
|
@@ -231,7 +238,7 @@ func ForgotPassword(app *backpack.App) http.HandlerFunc {
|
|
|
|
|
user, err := classes.LookupByEmail(ctx, gdb, strField(fields, "email"))
|
|
|
|
|
if err != nil {
|
|
|
|
|
slog.Default().Error("golem15.user: forgot-password lookup", "err", err)
|
|
|
|
|
wire.WriteJSON(w, http.StatusOK, map[string]any{"message": forgotPasswordBody})
|
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{"message": forgotPasswordBody})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if user != nil {
|
|
|
|
|
@@ -243,7 +250,7 @@ func ForgotPassword(app *backpack.App) http.HandlerFunc {
|
|
|
|
|
sendTemplateVars(ctx, app, mailName(ctx, user, "golem15.user::mail.restore"), user.Email, mailVars(displayName(user), resetLink(app, user.ID, code), packed))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
wire.WriteJSON(w, http.StatusOK, map[string]any{"message": forgotPasswordBody})
|
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{"message": forgotPasswordBody})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -262,7 +269,7 @@ func ResetPassword(app *backpack.App) http.HandlerFunc {
|
|
|
|
|
errs, err := lagoon.Validate(r.Context(), gdb, models.User{}, map[string]string{
|
|
|
|
|
"code": "required",
|
|
|
|
|
"password": "required|between:8,255|confirmed",
|
|
|
|
|
}, fields, nil)
|
|
|
|
|
}, fields, appTranslator(app))
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeOpaque500(w)
|
|
|
|
|
return
|
|
|
|
|
@@ -273,12 +280,12 @@ func ResetPassword(app *backpack.App) http.HandlerFunc {
|
|
|
|
|
}
|
|
|
|
|
id, code, ok := splitCode(strField(fields, "code"))
|
|
|
|
|
if !ok {
|
|
|
|
|
wire.WriteJSON(w, http.StatusUnprocessableEntity, map[string]any{"error": "Invalid reset code"})
|
|
|
|
|
writeJSON(w, http.StatusUnprocessableEntity, map[string]any{"error": "Invalid reset code"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
user, verified := classes.VerifyResetCode(codeContext(r.Context(), app), gdb, id, code)
|
|
|
|
|
if !verified || user == nil {
|
|
|
|
|
wire.WriteJSON(w, http.StatusUnprocessableEntity, map[string]any{"error": "Invalid or expired reset code"})
|
|
|
|
|
writeJSON(w, http.StatusUnprocessableEntity, map[string]any{"error": "Invalid or expired reset code"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
cost := 10
|
|
|
|
|
@@ -298,7 +305,7 @@ func ResetPassword(app *backpack.App) http.HandlerFunc {
|
|
|
|
|
writeOpaque500(w)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
wire.WriteJSON(w, http.StatusOK, map[string]any{"message": "Password has been reset"})
|
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{"message": "Password has been reset"})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -314,6 +321,10 @@ func Activate(app *backpack.App) http.HandlerFunc {
|
|
|
|
|
if !ok {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if user.IsActivated {
|
|
|
|
|
writeWinterErrorPage(w)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
fields, err := readFields(r)
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeOpaque500(w)
|
|
|
|
|
@@ -329,7 +340,7 @@ func Activate(app *backpack.App) http.HandlerFunc {
|
|
|
|
|
writeOpaque500(w)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
wire.WriteJSON(w, http.StatusOK, map[string]any{"user": payload})
|
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{"user": payload})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -347,7 +358,7 @@ func ActivateByCode(app *backpack.App) http.HandlerFunc {
|
|
|
|
|
}
|
|
|
|
|
errs, err := lagoon.Validate(r.Context(), gdb, models.User{}, map[string]string{
|
|
|
|
|
"code": "required",
|
|
|
|
|
}, fields, nil)
|
|
|
|
|
}, fields, appTranslator(app))
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeOpaque500(w)
|
|
|
|
|
return
|
|
|
|
|
@@ -358,12 +369,21 @@ func ActivateByCode(app *backpack.App) http.HandlerFunc {
|
|
|
|
|
}
|
|
|
|
|
id, code, ok := splitCode(strField(fields, "code"))
|
|
|
|
|
if !ok {
|
|
|
|
|
wire.WriteJSON(w, http.StatusUnprocessableEntity, map[string]any{"error": "Invalid activation code"})
|
|
|
|
|
writeJSON(w, http.StatusUnprocessableEntity, map[string]any{"error": "Invalid activation code"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
already, err := classes.IsAlreadyActivated(codeContext(r.Context(), app), gdb, id)
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeOpaque500(w)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if already {
|
|
|
|
|
writeWinterErrorPage(w)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
user, verified := classes.VerifyActivationCode(codeContext(r.Context(), app), gdb, id, code)
|
|
|
|
|
if !verified || user == nil {
|
|
|
|
|
wire.WriteJSON(w, http.StatusUnprocessableEntity, map[string]any{"error": "This activation link is invalid or has expired"})
|
|
|
|
|
writeJSON(w, http.StatusUnprocessableEntity, map[string]any{"error": "This activation link is invalid or has expired"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
token, err := mintFor(app, secret, user, requestURL(r))
|
|
|
|
|
@@ -376,7 +396,7 @@ func ActivateByCode(app *backpack.App) http.HandlerFunc {
|
|
|
|
|
writeOpaque500(w)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
wire.WriteJSON(w, http.StatusOK, map[string]any{
|
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{
|
|
|
|
|
"message": "Account activated",
|
|
|
|
|
"token": token,
|
|
|
|
|
"user": payload,
|
|
|
|
|
@@ -404,7 +424,7 @@ func Update(app *backpack.App) http.HandlerFunc {
|
|
|
|
|
"name": "required|between:2,255",
|
|
|
|
|
"surname": "required|between:2,255",
|
|
|
|
|
"email": "required|between:6,255|email|unique:users",
|
|
|
|
|
}, fields, nil)
|
|
|
|
|
}, fields, appTranslator(app))
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeOpaque500(w)
|
|
|
|
|
return
|
|
|
|
|
@@ -431,7 +451,7 @@ func Update(app *backpack.App) http.HandlerFunc {
|
|
|
|
|
writeOpaque500(w)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
wire.WriteJSON(w, http.StatusOK, map[string]any{"message": "Profile updated", "user": payload})
|
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{"message": "Profile updated", "user": payload})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -484,7 +504,7 @@ func ChangePassword(app *backpack.App) http.HandlerFunc {
|
|
|
|
|
errs, err := lagoon.Validate(r.Context(), gdb, &user, map[string]string{
|
|
|
|
|
"current_password": "required",
|
|
|
|
|
"password": "required|between:" + strconv.Itoa(minLen) + ",255|confirmed|different:current_password",
|
|
|
|
|
}, fields, nil)
|
|
|
|
|
}, fields, appTranslator(app))
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeOpaque500(w)
|
|
|
|
|
return
|
|
|
|
|
@@ -495,10 +515,11 @@ func ChangePassword(app *backpack.App) http.HandlerFunc {
|
|
|
|
|
}
|
|
|
|
|
current := strField(fields, "current_password")
|
|
|
|
|
if !bouncer.CheckPassword(user.Password, current) {
|
|
|
|
|
wire.WriteJSON(w, http.StatusUnprocessableEntity, map[string]any{
|
|
|
|
|
"error": "Invalid email or password",
|
|
|
|
|
msg := accountMessage(r.Context(), app, "invalid_login", invalidLoginBody)
|
|
|
|
|
writeJSON(w, http.StatusUnprocessableEntity, map[string]any{
|
|
|
|
|
"error": msg,
|
|
|
|
|
"errors": map[string][]string{
|
|
|
|
|
"current_password": {"Invalid email or password"},
|
|
|
|
|
"current_password": {msg},
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
return
|
|
|
|
|
@@ -531,7 +552,7 @@ func ChangePassword(app *backpack.App) http.HandlerFunc {
|
|
|
|
|
writeOpaque500(w)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
wire.WriteJSON(w, http.StatusOK, map[string]any{"message": "Password changed", "user": payload})
|
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{"message": "Password changed", "user": payload})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -551,20 +572,18 @@ func MarketingConsent(app *backpack.App) http.HandlerFunc {
|
|
|
|
|
writeOpaque500(w)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
raw, exists := fields["marketing_consent"]
|
|
|
|
|
if !exists || raw == nil {
|
|
|
|
|
writeValidation(w, map[string][]string{
|
|
|
|
|
"marketing_consent": {"The marketing_consent field is required."},
|
|
|
|
|
})
|
|
|
|
|
errs, err := lagoon.Validate(r.Context(), gdb, user, map[string]string{
|
|
|
|
|
"marketing_consent": "required|boolean",
|
|
|
|
|
}, fields, appTranslator(app))
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeOpaque500(w)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
consent, isBool := raw.(bool)
|
|
|
|
|
if !isBool {
|
|
|
|
|
writeValidation(w, map[string][]string{
|
|
|
|
|
"marketing_consent": {"The marketing_consent field must be true or false."},
|
|
|
|
|
})
|
|
|
|
|
if len(errs) > 0 {
|
|
|
|
|
writeValidation(w, errs)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
consent := laravelBool(fields["marketing_consent"])
|
|
|
|
|
if err := gdb.WithContext(r.Context()).Model(user).Update("marketing_consent", consent).Error; err != nil {
|
|
|
|
|
writeOpaque500(w)
|
|
|
|
|
return
|
|
|
|
|
@@ -575,7 +594,7 @@ func MarketingConsent(app *backpack.App) http.HandlerFunc {
|
|
|
|
|
writeOpaque500(w)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
wire.WriteJSON(w, http.StatusOK, map[string]any{"message": "Marketing consent updated", "user": payload})
|
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{"message": "Marketing consent updated", "user": payload})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -605,13 +624,13 @@ func Register(app *backpack.App) http.HandlerFunc {
|
|
|
|
|
fields["password_confirmation"] = fields["password"]
|
|
|
|
|
}
|
|
|
|
|
rules := (models.User{}).Rules()
|
|
|
|
|
errs, err := lagoon.Validate(r.Context(), gdb, models.User{}, rules, fields, nil)
|
|
|
|
|
errs, err := lagoon.Validate(r.Context(), gdb, models.User{}, rules, fields, appTranslator(app))
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeOpaque500(w)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if len(errs) > 0 {
|
|
|
|
|
wire.WriteJSON(w, http.StatusUnprocessableEntity, map[string]any{
|
|
|
|
|
writeJSON(w, http.StatusUnprocessableEntity, map[string]any{
|
|
|
|
|
"error": firstValidationMessage(errs),
|
|
|
|
|
"errors": errs,
|
|
|
|
|
})
|
|
|
|
|
@@ -662,7 +681,7 @@ func Register(app *backpack.App) http.HandlerFunc {
|
|
|
|
|
vars = mailVars(displayName(user), activationLink(app, user.ID, code), packed)
|
|
|
|
|
}
|
|
|
|
|
sendTemplateVars(ctx, app, mailName(ctx, user, "golem15.user::mail.activate"), user.Email, vars)
|
|
|
|
|
wire.WriteJSON(w, http.StatusOK, map[string]any{"message": "Activation email sent"})
|
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{"message": "Activation email sent"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if mode == "auto" || !require {
|
|
|
|
|
@@ -676,10 +695,10 @@ func Register(app *backpack.App) http.HandlerFunc {
|
|
|
|
|
writeOpaque500(w)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
wire.WriteJSON(w, http.StatusOK, map[string]any{"token": token, "user": payload})
|
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{"token": token, "user": payload})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
wire.WriteJSON(w, http.StatusOK, map[string]any{})
|
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -728,10 +747,10 @@ func firstValidationMessage(errs map[string][]string) string {
|
|
|
|
|
|
|
|
|
|
func writeSafe500(w http.ResponseWriter, app *backpack.App, message string) {
|
|
|
|
|
if app != nil && app.Config != nil && app.Config.Bool("app.debug") {
|
|
|
|
|
wire.WriteJSON(w, http.StatusInternalServerError, map[string]any{"error": message})
|
|
|
|
|
writeJSON(w, http.StatusInternalServerError, map[string]any{"error": message})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
wire.WriteJSON(w, http.StatusInternalServerError, map[string]any{"error": "Internal server error"})
|
|
|
|
|
writeJSON(w, http.StatusInternalServerError, map[string]any{"error": "Internal server error"})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func appConfig(app *backpack.App) *compass.Config {
|
|
|
|
|
@@ -792,25 +811,18 @@ func sessionDeps(w http.ResponseWriter, app *backpack.App) (*gorm.DB, string, bo
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func apiArray(ctx context.Context, app *backpack.App, user *models.User) (map[string]any, error) {
|
|
|
|
|
name, surname := "", ""
|
|
|
|
|
if user.Name != nil {
|
|
|
|
|
name = *user.Name
|
|
|
|
|
}
|
|
|
|
|
if user.Surname != nil {
|
|
|
|
|
surname = *user.Surname
|
|
|
|
|
}
|
|
|
|
|
payload := map[string]any{
|
|
|
|
|
"id": user.ID,
|
|
|
|
|
"name": name,
|
|
|
|
|
"surname": surname,
|
|
|
|
|
"name": user.Name,
|
|
|
|
|
"surname": user.Surname,
|
|
|
|
|
"email": user.Email,
|
|
|
|
|
"is_activated": user.IsActivated,
|
|
|
|
|
"permissions": []string{},
|
|
|
|
|
"avatar": nil,
|
|
|
|
|
"avatar_url": nil,
|
|
|
|
|
"permissions": nil,
|
|
|
|
|
"avatar": gravatarURL(user.Email, 25),
|
|
|
|
|
"avatar_url": gravatarURL(user.Email, 128),
|
|
|
|
|
"has_avatar": false,
|
|
|
|
|
"marketing_consent": user.MarketingConsent,
|
|
|
|
|
"groups": map[string]string{},
|
|
|
|
|
"groups": []any{},
|
|
|
|
|
"role": nil,
|
|
|
|
|
"is_onboarded": user.IsOnboarded,
|
|
|
|
|
"has_self_set_password": user.HasSelfSetPassword,
|
|
|
|
|
@@ -893,7 +905,7 @@ func sendTemplateVars(ctx context.Context, app *backpack.App, template, to strin
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func writeValidation(w http.ResponseWriter, errs map[string][]string) {
|
|
|
|
|
wire.WriteJSON(w, http.StatusUnprocessableEntity, map[string]any{
|
|
|
|
|
writeJSON(w, http.StatusUnprocessableEntity, map[string]any{
|
|
|
|
|
"error": firstValidationMessage(errs),
|
|
|
|
|
"errors": errs,
|
|
|
|
|
})
|
|
|
|
|
@@ -1031,15 +1043,63 @@ func remoteIP(r *http.Request) string {
|
|
|
|
|
return host
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func writeInvalidLogin(w http.ResponseWriter) {
|
|
|
|
|
wire.WriteJSON(w, http.StatusUnauthorized, map[string]any{"error": true, "message": invalidLoginBody})
|
|
|
|
|
func writeJSON(w http.ResponseWriter, status int, v any) {
|
|
|
|
|
w.Header().Set("Cache-Control", "no-cache, private")
|
|
|
|
|
wire.WriteJSON(w, status, v)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func writeInvalidLogin(w http.ResponseWriter, r *http.Request, app *backpack.App) {
|
|
|
|
|
writeJSON(w, http.StatusUnauthorized, map[string]any{
|
|
|
|
|
"error": true,
|
|
|
|
|
"message": accountMessage(r.Context(), app, "invalid_login", invalidLoginBody),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func writeUnauthorized(w http.ResponseWriter) {
|
|
|
|
|
wire.WriteJSON(w, http.StatusUnauthorized, map[string]any{"error": true, "message": "Unauthorized"})
|
|
|
|
|
writeJSON(w, http.StatusUnauthorized, map[string]any{"error": true, "message": "Unauthorized"})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func appTranslator(app *backpack.App) *phrasebook.Translator {
|
|
|
|
|
if app == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
tr, _ := app.Lookup[*phrasebook.Translator]()
|
|
|
|
|
return tr
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func requiredField(ctx context.Context, app *backpack.App, field string) string {
|
|
|
|
|
tr := appTranslator(app)
|
|
|
|
|
attr := strings.ReplaceAll(field, "_", " ")
|
|
|
|
|
if tr != nil {
|
|
|
|
|
s := tr.Get(ctx, "lagoon::validate.required", map[string]string{"attribute": attr})
|
|
|
|
|
if s != "" && s != "lagoon::validate.required" {
|
|
|
|
|
return s
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return "The " + attr + " field is required."
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func accountMessage(ctx context.Context, app *backpack.App, key, fallback string) string {
|
|
|
|
|
tr := appTranslator(app)
|
|
|
|
|
if tr == nil {
|
|
|
|
|
return fallback
|
|
|
|
|
}
|
|
|
|
|
s := tr.Get(ctx, "golem15.user::lang.account."+key, nil)
|
|
|
|
|
if s == "" || s == "golem15.user::lang.account."+key {
|
|
|
|
|
return fallback
|
|
|
|
|
}
|
|
|
|
|
return s
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func writeWinterErrorPage(w http.ResponseWriter) {
|
|
|
|
|
w.Header().Set("Content-Type", "text/html; charset=UTF-8")
|
|
|
|
|
w.Header().Set("Cache-Control", "no-cache, private")
|
|
|
|
|
w.WriteHeader(http.StatusInternalServerError)
|
|
|
|
|
_, _ = w.Write(winterErrorPage)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func writeOpaque500(w http.ResponseWriter) {
|
|
|
|
|
w.Header().Set("Cache-Control", "no-cache, private")
|
|
|
|
|
wire.WriteOpaque500(w)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -1055,12 +1115,12 @@ func UploadAvatar(app *backpack.App) http.HandlerFunc {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if err := r.ParseMultipartForm(10 << 20); err != nil {
|
|
|
|
|
writeValidation(w, map[string][]string{"avatar": {"The avatar field is required."}})
|
|
|
|
|
writeValidation(w, map[string][]string{"avatar": {requiredField(r.Context(), app, "avatar")}})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
file, header, err := r.FormFile("avatar")
|
|
|
|
|
if err != nil {
|
|
|
|
|
writeValidation(w, map[string][]string{"avatar": {"The avatar field is required."}})
|
|
|
|
|
writeValidation(w, map[string][]string{"avatar": {requiredField(r.Context(), app, "avatar")}})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
defer file.Close()
|
|
|
|
|
@@ -1142,7 +1202,7 @@ func UploadAvatar(app *backpack.App) http.HandlerFunc {
|
|
|
|
|
writeOpaque500(w)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
wire.WriteJSON(w, http.StatusOK, map[string]any{"message": "Avatar updated", "user": payload})
|
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{"message": "Avatar updated", "user": payload})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -1160,7 +1220,7 @@ func RemoveAvatar(app *backpack.App) http.HandlerFunc {
|
|
|
|
|
if err := deleteAvatar(r.Context(), app, gdb, user); err != nil {
|
|
|
|
|
if errors.Is(err, errNoAvatar) {
|
|
|
|
|
writeValidation(w, map[string][]string{
|
|
|
|
|
"avatar": {"Your account has no display picture to remove."},
|
|
|
|
|
"avatar": {accountMessage(r.Context(), app, "no_avatar", "Your account has no display picture to remove.")},
|
|
|
|
|
})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
@@ -1172,7 +1232,7 @@ func RemoveAvatar(app *backpack.App) http.HandlerFunc {
|
|
|
|
|
writeOpaque500(w)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
wire.WriteJSON(w, http.StatusOK, map[string]any{"message": "Avatar removed", "user": payload})
|
|
|
|
|
writeJSON(w, http.StatusOK, map[string]any{"message": "Avatar removed", "user": payload})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -1204,6 +1264,28 @@ func deleteAvatar(ctx context.Context, app *backpack.App, gdb *gorm.DB, user *mo
|
|
|
|
|
return attach.DeleteKeys(ctx, bucket, keys)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func gravatarURL(email string, size int) string {
|
|
|
|
|
sum := md5.Sum([]byte(strings.ToLower(strings.TrimSpace(email))))
|
|
|
|
|
return fmt.Sprintf("//www.gravatar.com/avatar/%x?s=%d&d=mm", sum, size)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func laravelBool(v any) bool {
|
|
|
|
|
switch t := v.(type) {
|
|
|
|
|
case bool:
|
|
|
|
|
return t
|
|
|
|
|
case string:
|
|
|
|
|
s := strings.ToLower(strings.TrimSpace(t))
|
|
|
|
|
return s == "1" || s == "true"
|
|
|
|
|
case float64:
|
|
|
|
|
return t != 0
|
|
|
|
|
case int:
|
|
|
|
|
return t != 0
|
|
|
|
|
default:
|
|
|
|
|
s := strings.ToLower(strings.TrimSpace(fmt.Sprint(v)))
|
|
|
|
|
return s == "1" || s == "true"
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func applyAvatar(ctx context.Context, app *backpack.App, user *models.User, payload map[string]any) {
|
|
|
|
|
if app == nil || user == nil || user.ID == 0 {
|
|
|
|
|
return
|
|
|
|
|
|