From d94634d5d833c861c44e41a5b5e20a32d2f79ad2 Mon Sep 17 00:00:00 2001 From: Jakub Zych Date: Tue, 22 Sep 2026 23:57:00 +0200 Subject: [PATCH] feat(07-07): replay the 15 user-api routes and both nuxt auth flows Match PHP payloads, already-activated Winter 500 HTML, and direct-DB seeding so the corpus can flip those routes to ported without onboarding. Co-authored-by: Cursor --- classes/codes.go | 15 +- controllers/api_controller.go | 220 ++++++++++++++++++++--------- controllers/winter_error_page.html | 14 ++ lang/en/lang.yaml | 3 + lang/pl/lang.yaml | 3 + models/user.go | 5 +- plugin.go | 6 + session_test.go | 2 +- 8 files changed, 195 insertions(+), 73 deletions(-) create mode 100644 controllers/winter_error_page.html create mode 100644 lang/en/lang.yaml create mode 100644 lang/pl/lang.yaml diff --git a/classes/codes.go b/classes/codes.go index fe56a8d..1d3eefa 100644 --- a/classes/codes.go +++ b/classes/codes.go @@ -144,11 +144,24 @@ func VerifyActivationCode(ctx context.Context, db *gorm.DB, userID uint, code st return user, true } +// IsAlreadyActivated reports whether the scoped user row is activated. +// A missing user is (false, nil); a real DB error is (false, err). +func IsAlreadyActivated(ctx context.Context, db *gorm.DB, userID uint) (bool, error) { + user, err := takeUser(ctx, db, userID, false) + if err != nil { + return false, err + } + if user == nil { + return false, nil + } + return user.IsActivated, nil +} + func takeUser(ctx context.Context, db *gorm.DB, id uint, unscoped bool) (*models.User, error) { if db == nil || id == 0 { return nil, nil } - q := db.WithContext(ctx) + q := db.Session(&gorm.Session{NewDB: true, Context: ctx}) if unscoped { q = q.Unscoped() } diff --git a/controllers/api_controller.go b/controllers/api_controller.go index 6357057..5c5f452 100644 --- a/controllers/api_controller.go +++ b/controllers/api_controller.go @@ -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 diff --git a/controllers/winter_error_page.html b/controllers/winter_error_page.html new file mode 100644 index 0000000..66c2acd --- /dev/null +++ b/controllers/winter_error_page.html @@ -0,0 +1,14 @@ + + + + + Błąd strony + + + +
+

Błąd strony

+

Bardzo przepraszamy, jednak coś poszło nie tak i strona nie może zostać wyświetlona.

+
+ + diff --git a/lang/en/lang.yaml b/lang/en/lang.yaml new file mode 100644 index 0000000..307c7f7 --- /dev/null +++ b/lang/en/lang.yaml @@ -0,0 +1,3 @@ +account: + invalid_login: Invalid email or password + no_avatar: Your account has no display picture to remove. diff --git a/lang/pl/lang.yaml b/lang/pl/lang.yaml new file mode 100644 index 0000000..5c6c455 --- /dev/null +++ b/lang/pl/lang.yaml @@ -0,0 +1,3 @@ +account: + invalid_login: Nieprawidłowy email lub hasło + no_avatar: Brak avatara diff --git a/models/user.go b/models/user.go index 70b816a..562e600 100644 --- a/models/user.go +++ b/models/user.go @@ -55,8 +55,9 @@ func (User) Hidden() []string { // Rules is the register-time rule set. confirmed covers password_confirmation. func (User) Rules() map[string]string { return map[string]string{ - "email": "required|between:6,255|email|unique:users", - "password": "required|between:8,255|confirmed", + "email": "required|between:6,255|email|unique:users", + "password": "required|between:8,255|confirmed", + "password_confirmation": "between:8,255", } } diff --git a/plugin.go b/plugin.go index 58b1809..76eb726 100644 --- a/plugin.go +++ b/plugin.go @@ -32,6 +32,7 @@ var ( _ pact.HasModels = (*Plugin)(nil) _ pact.HasRoutes = (*Plugin)(nil) _ pact.HasMailTemplates = (*Plugin)(nil) + _ pact.HasLang = (*Plugin)(nil) _ pact.HasCommands = (*Plugin)(nil) _ surf.BucketProvider = (*Plugin)(nil) _ bouncer.UserProvider = classes.GormUsers{} @@ -43,6 +44,9 @@ var configFS embed.FS //go:embed views/mail var mailFS embed.FS +//go:embed lang +var langFS embed.FS + // Plugin is the golem15.user plugin. type Plugin struct { app *backpack.App @@ -160,6 +164,8 @@ func bearerFrom(r *http.Request) string { func (p *Plugin) ConfigFS() fs.FS { return configFS } +func (p *Plugin) LangFS() fs.FS { return langFS } + func (p *Plugin) MailTemplatesFS() fs.FS { return mailFS } func (p *Plugin) MailTemplates() []string { diff --git a/session_test.go b/session_test.go index 3b9d1a9..7e886b0 100644 --- a/session_test.go +++ b/session_test.go @@ -80,7 +80,7 @@ func TestLogin(t *testing.T) { if userBody["email"] != user.Email || userBody["feedback_widget_hidden"] != false { t.Fatalf("user payload = %#v", userBody) } - if _, ok := userBody["permissions"].([]any); !ok { + if userBody["permissions"] != nil { t.Fatalf("permissions = %#v", userBody["permissions"]) }