package classes import ( "context" "encoding/json" "errors" "fmt" "strings" "git.golem15.com/golem15/sm-user-plugin/models" "git.golem15.com/golem15/summercms/modules/backpack" "git.golem15.com/golem15/summercms/modules/bouncer" "gorm.io/gorm" ) var _ bouncer.UserProvider = GormUsers{} // GormUsers loads JWT principals from the users table. type GormUsers struct { App *backpack.App } func (g GormUsers) FindByID(ctx context.Context, id uint) (*bouncer.Principal, error) { if g.App == nil { return nil, fmt.Errorf("golem15.user: app is nil") } gdb, ok := g.App.Lookup[*gorm.DB]() if !ok || gdb == nil { return nil, fmt.Errorf("golem15.user: gorm handle is missing") } return PrincipalByID(ctx, gdb, id) } // PrincipalByID loads one front-end user and their current group permissions. func PrincipalByID(ctx context.Context, gdb *gorm.DB, id uint) (*bouncer.Principal, error) { if gdb == nil { return nil, fmt.Errorf("golem15.user: gorm handle is missing") } var row models.User err := gdb.WithContext(ctx).Where("id = ?", id).Take(&row).Error if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil } if err != nil { return nil, err } principal := &bouncer.Principal{ID: row.ID, MustChangePassword: row.MustChangePassword} if row.PreferredLocale != nil { principal.PreferredLocale = *row.PreferredLocale } if row.TokensValidAfter != nil { principal.TokensValidAfter = *row.TokensValidAfter } grants, err := UserPermissionGrants(ctx, gdb, row.ID) if err != nil { return nil, err } principal.PermissionGrants = grants return principal, nil } // UserPermissionGrants returns the enabled permissions of all groups to which // userID belongs. A false/zero value is not a grant; another group may still // grant the same code. func UserPermissionGrants(ctx context.Context, db *gorm.DB, userID uint) (map[string]bool, error) { if db == nil { return nil, fmt.Errorf("golem15.user: gorm handle is missing") } var rows []struct{ Permissions *string } err := db.WithContext(ctx). Table("user_groups"). Select("user_groups.permissions"). Joins("JOIN users_groups ON users_groups.user_group_id = user_groups.id"). Where("users_groups.user_id = ? AND user_groups.permissions IS NOT NULL", userID). Order("user_groups.id"). Scan(&rows).Error if err != nil { return nil, err } grants := map[string]bool{} for _, row := range rows { if row.Permissions == nil || strings.TrimSpace(*row.Permissions) == "" { continue } var decoded map[string]any if json.Unmarshal([]byte(*row.Permissions), &decoded) != nil { continue } for code, value := range decoded { if permissionEnabled(value) { grants[code] = true } } } if len(grants) == 0 { return nil, nil } return grants, nil } func permissionEnabled(value any) bool { switch v := value.(type) { case bool: return v case float64: return v == 1 case string: return v == "1" || strings.EqualFold(v, "true") default: return false } } // LookupByEmail returns the user row, including a soft-deleted one. // A missing email is (nil, nil). func LookupByEmail(ctx context.Context, db *gorm.DB, email string) (*models.User, error) { if db == nil { return nil, fmt.Errorf("golem15.user: gorm handle is missing") } var row models.User err := db.WithContext(ctx).Unscoped().Where("email = ?", email).Take(&row).Error if errors.Is(err, gorm.ErrRecordNotFound) { return nil, nil } if err != nil { return nil, err } return &row, nil } // JWTSecret reads golem15.user.jwt.secret and fails loudly when it is empty. func JWTSecret(app *backpack.App) (string, error) { if app == nil || app.Config == nil { return "", fmt.Errorf("golem15.user: jwt.secret is empty (set SUMMER_GOLEM15__USER__JWT__SECRET)") } secret := strings.TrimSpace(app.Config.String("golem15.user.jwt.secret")) if secret == "" { return "", fmt.Errorf("golem15.user: jwt.secret is empty (set SUMMER_GOLEM15__USER__JWT__SECRET)") } return secret, nil }