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 nothing // else. 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. func (organisationsAdminController) AdminRelationContracts() []cabana.RelationContract { return []cabana.RelationContract{{ Name: membersRelation, Kind: cabana.RelationHasMany, NewRelated: func() any { return &models.User{} }, ForeignKey: "organisation_id", 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 }