Files
sm-user-plugin/controllers/organisations_admin_controller.go
Jakub Zych aa8396916e fix: clear a stale organisation_role when a user's organisation changes in the admin
The members relation manager and the organisation field of the Users form
declare cabana ClearOnChange on organisation_role: adding or removing a
member, and a form save that moves a user to another organisation or to
none, clear the role unless the same save writes it. Needs a SummerCMS
release with ClearOnChange.
2026-10-22 03:33:12 +02:00

122 lines
4.6 KiB
Go

package controllers
import (
"context"
"errors"
"regexp"
"git.golem15.com/golem15/sm-user-plugin/models"
"git.golem15.com/golem15/summercms/modules/cabana"
"gorm.io/gorm"
)
// organisationSlugPattern is the PHP alpha_dash rule on the slug, kept to
// ASCII. lagoon.Validate has no alpha_dash token, so the Form before-hooks
// check it.
var organisationSlugPattern = regexp.MustCompile(`^[a-zA-Z0-9_-]+$`)
// membersRelation is the organisation's hasMany relation to its users.
const membersRelation = "members"
// organisationsAdminController is the Winter Organisations backend
// controller: the list, the form with the avatar, and the members relation
// manager. The screen and every write need golem15.users.access_users, as in
// PHP.
//
// The members manager sets and clears users.organisation_id, and clears
// users.organisation_role with it (a role belongs to one organisation). It
// never writes users_groups.
type organisationsAdminController struct {
app appAccess
}
func (organisationsAdminController) ID() string { return "golem15.user.organisations" }
func (organisationsAdminController) ModelName() string { return `Golem15\User\Models\Organisation` }
func (organisationsAdminController) ConfigDir() string { return "controllers/organisations" }
func (organisationsAdminController) RequiredPermissions() []string {
return []string{PermissionAccessUsers}
}
func (organisationsAdminController) NewRecord() any { return &models.Organisation{} }
// FormBeforeCreate and FormBeforeUpdate check the slug's characters.
func (c organisationsAdminController) FormBeforeCreate(ctx context.Context, model any) error {
return c.requireSlugFormat(ctx, model)
}
func (c organisationsAdminController) FormBeforeUpdate(ctx context.Context, model any) error {
return c.requireSlugFormat(ctx, model)
}
// requireSlugFormat answers 422 on slug when it holds anything but letters,
// digits, underscore and hyphen. An empty slug is left to the required rule.
func (c organisationsAdminController) requireSlugFormat(ctx context.Context, model any) error {
organisation, ok := model.(*models.Organisation)
if !ok || organisation == nil {
return errors.New("organisations admin: form model is not an organisation")
}
if organisation.Slug == "" || organisationSlugPattern.MatchString(organisation.Slug) {
return nil
}
message := "The slug may only contain letters, numbers, dashes and underscores."
if tr := c.app.translator(); tr != nil {
const key = "golem15.user::lang.organisation.slug_invalid"
if text := tr.Get(ctx, key, nil); text != "" && text != key {
message = text
}
}
return &cabana.ValidationError{Details: map[string]any{"slug": []string{message}}}
}
// AdminRelationContracts declares the members relation manager: the users
// whose organisation_id is the organisation. Linking sets the column and
// unlinking clears it; the column is nullable, so a user is never deleted.
// Both also clear organisation_role (ClearOnChange), so a role never moves
// to another organisation or outlives the membership.
func (organisationsAdminController) AdminRelationContracts() []cabana.RelationContract {
return []cabana.RelationContract{{
Name: membersRelation,
Kind: cabana.RelationHasMany,
NewRelated: func() any { return &models.User{} },
ForeignKey: "organisation_id",
ClearOnChange: []string{"organisation_role"},
Columns: map[string]string{
"name": "name",
"email": "email",
},
}}
}
// RelationExtendManageQuery fails closed: any relation name other than
// members has no candidates.
func (organisationsAdminController) RelationExtendManageQuery(_ context.Context, relation string, db *gorm.DB) *gorm.DB {
if db == nil {
return db
}
if relation != membersRelation {
return db.Where("1 = 0")
}
return db
}
// FormBeforeDelete releases the members of the organisation in the delete's
// transaction: organisation_id and organisation_role are cleared, on
// deactivated users too, so the delete does not fail on the users foreign key
// and no user is deleted.
func (organisationsAdminController) FormBeforeDelete(ctx context.Context, model any) error {
organisation, ok := model.(*models.Organisation)
if !ok || organisation == nil {
return errors.New("organisations admin: form model is not an organisation")
}
tx, ok := cabana.TxFromContext(ctx)
if !ok {
return errors.New("organisations admin: a delete needs its transaction")
}
return tx.Session(&gorm.Session{NewDB: true}).WithContext(ctx).Unscoped().
Model(&models.User{}).Where("organisation_id = ?", organisation.ID).
Updates(map[string]any{"organisation_id": nil, "organisation_role": nil}).Error
}