package controllers import ( "context" "errors" "strconv" "git.golem15.com/golem15/sm-journal-plugin/classes" "git.golem15.com/golem15/sm-journal-plugin/models" "git.golem15.com/golem15/summercms/modules/backpack" "git.golem15.com/golem15/summercms/modules/bouncer" "git.golem15.com/golem15/summercms/modules/cabana" "git.golem15.com/golem15/summercms/modules/pact" "gorm.io/gorm" ) const ( // PostsControllerID is the cabana controller id of the Posts screen. PostsControllerID = "golem15.journal.posts" // PermissionAccessPosts is Posts::$requiredPermissions. PermissionAccessPosts = "golem15.journal.access_posts" // PermissionAccessOtherPosts lets an administrator see every author's posts. PermissionAccessOtherPosts = "golem15.journal.access_other_posts" // PermissionAccessPublish is required to persist published=true or published_at. PermissionAccessPublish = "golem15.journal.access_publish" ) type postsAdminController struct { app func() *backpack.App } func (postsAdminController) ID() string { return PostsControllerID } func (postsAdminController) ModelName() string { return `Golem15\Journal\Models\Post` } func (postsAdminController) ConfigDir() string { return "controllers/posts" } func (postsAdminController) RequiredPermissions() []string { return []string{PermissionAccessPosts} } func (postsAdminController) NewRecord() any { return &models.Post{} } func (c postsAdminController) application() *backpack.App { if c.app == nil { return nil } return c.app() } func (c postsAdminController) pool() *gorm.DB { app := c.application() if app == nil { return nil } gdb, ok := app.Lookup[*gorm.DB]() if !ok { return nil } return gdb } func (c postsAdminController) db(ctx context.Context) *gorm.DB { if tx, ok := cabana.TxFromContext(ctx); ok { return tx } return c.pool() } func (postsAdminController) ListExtendQuery(ctx context.Context, db *gorm.DB) *gorm.DB { return restrictToOwnPosts(ctx, db) } func (postsAdminController) FormExtendQuery(ctx context.Context, db *gorm.DB) *gorm.DB { return restrictToOwnPosts(ctx, db) } func restrictToOwnPosts(ctx context.Context, db *gorm.DB) *gorm.DB { if db == nil { return db } principal, ok := bouncer.User(ctx) if !ok || principal == nil || !principal.Backend { return db.Where("1 = 0") } if cabana.Allows(principal, []string{PermissionAccessOtherPosts}) { return db } return db.Where("user_id = ?", principal.ID) } func (postsAdminController) ListRowStates(_ context.Context, _ *gorm.DB, records []any) ([][]pact.RowState, error) { out := make([][]pact.RowState, len(records)) for i, record := range records { post, ok := record.(*models.Post) if !ok || post == nil { return nil, errors.New("posts admin: list record is not a post") } if !post.Published { out[i] = append(out[i], pact.RowStateDisabled) } } return out, nil } func (c postsAdminController) FilterOptions(scope string) []pact.Option { if scope != models.FilterCategories { return nil } db := c.pool() if db == nil { return nil } var cats []models.Category if err := db.Session(&gorm.Session{NewDB: true}).Order("name ASC, id ASC").Find(&cats).Error; err != nil { return nil } out := make([]pact.Option, 0, len(cats)) for _, cat := range cats { out = append(out, pact.Option{Value: strconv.FormatUint(uint64(cat.ID), 10), Label: cat.Name}) } return out } func (postsAdminController) AdminFieldRelations() []cabana.FieldRelationContract { return []cabana.FieldRelationContract{ { Field: "user", Kind: "belongsTo", NewRelated: func() any { return &cabana.BackendUser{} }, ForeignKey: "user_id", LabelColumn: "login", WritableForeignKey: true, }, { Field: "categories", Kind: "belongsToMany", NewRelated: func() any { return &models.Category{} }, NewPivot: func() any { return &models.PostCategory{} }, ParentForeignKey: "post_id", RelatedForeignKey: "category_id", LabelColumn: "name", }, { Field: "tags", Kind: "belongsToMany", NewRelated: func() any { return &models.Tag{} }, NewPivot: func() any { return &models.PostTag{} }, ParentForeignKey: "post_id", RelatedForeignKey: "tag_id", LabelColumn: "name", }, } } func (c postsAdminController) FormBeforeCreate(ctx context.Context, model any) error { post, ok := model.(*models.Post) if !ok || post == nil { return errors.New("posts admin: form model is not a post") } if post.UserID == nil || *post.UserID == 0 { principal, ok := bouncer.User(ctx) if ok && principal != nil && principal.Backend { id := principal.ID post.UserID = &id } } return preparePostWrite(ctx, post) } func (c postsAdminController) FormBeforeUpdate(ctx context.Context, model any) error { post, ok := model.(*models.Post) if !ok || post == nil { return errors.New("posts admin: form model is not a post") } if err := refuseOtherAuthor(ctx, post); err != nil { return err } return preparePostWrite(ctx, post) } func (c postsAdminController) FormBeforeDelete(ctx context.Context, model any) error { post, ok := model.(*models.Post) if !ok || post == nil { return errors.New("posts admin: form model is not a post") } return refuseOtherAuthor(ctx, post) } func refuseOtherAuthor(ctx context.Context, post *models.Post) error { principal, _ := bouncer.User(ctx) if post.CanEdit(principal) { return nil } return &cabana.ForbiddenError{Message: "golem15.journal::lang.journal.access_other_posts"} } func preparePostWrite(ctx context.Context, post *models.Post) error { if err := refuseUnpublishedWrite(ctx, post); err != nil { return err } if post.Published && post.PublishedAt == nil { return &cabana.ValidationError{Details: map[string]any{ "published_at": []string{"golem15.journal::lang.post.published_validation"}, }} } html, err := classes.FormatHTML(post.Content) if err != nil { return &cabana.ValidationError{Details: map[string]any{ "content": []string{err.Error()}, }} } post.ContentHTML = html return nil } func refuseUnpublishedWrite(ctx context.Context, post *models.Post) error { if !post.Published && post.PublishedAt == nil { return nil } principal, _ := bouncer.User(ctx) if cabana.Allows(principal, []string{PermissionAccessPublish}) { return nil } return &cabana.ForbiddenError{Message: "golem15.journal::lang.journal.access_publish"} }