142 lines
3.9 KiB
Go
142 lines
3.9 KiB
Go
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
|
|
}
|