package user import ( "context" "errors" "strings" "git.golem15.com/golem15/sm-user-plugin/classes" "git.golem15.com/golem15/sm-user-plugin/models" "git.golem15.com/golem15/summercms/modules/bouncer" "git.golem15.com/golem15/summercms/modules/cabana" "git.golem15.com/golem15/summercms/modules/pact" "gorm.io/gorm" ) var _ pact.HasAdminActionExtensions = (*Plugin)(nil) // AdminActionExtensions adds two record actions to the admin's // Administrators screen (D-07, D-34), both behind // golem15.users.manage_backend_links: link_frontend_account asks for the // email of an active frontend user and links that user to the // administrator shown (exact, case-insensitive match; no search and no // list), recording who set it; unlink_frontend_account removes the // administrator's link. cabana's outranking check runs before either. func (p *Plugin) AdminActionExtensions() []pact.AdminActionExtension { return []pact.AdminActionExtension{{ Controller: cabana.AdministratorsControllerID, RecordActions: []pact.AdminRecordAction{ { Name: "link_frontend_account", Label: backendLinkLang + "set", Confirm: backendLinkLang + "set_confirm", Permissions: []string{classes.PermissionManageBackendLinks}, Prompt: &pact.AdminActionPrompt{ Label: backendLinkLang + "prompt_email", Comment: backendLinkLang + "prompt_comment", MaxLength: 254, }, Applies: func(ctx context.Context, record any) (bool, error) { link, err := p.adminRecordLink(ctx, record) return link == nil, err }, Run: p.setFrontendLink, }, { Name: "unlink_frontend_account", Label: backendLinkLang + "remove", Confirm: backendLinkLang + "remove_confirm", Danger: true, Permissions: []string{classes.PermissionManageBackendLinks}, Applies: func(ctx context.Context, record any) (bool, error) { link, err := p.adminRecordLink(ctx, record) return link != nil, err }, Run: p.removeFrontendLink, }, }, }} } // adminRecordLink returns the link of the administrator record, or nil. func (p *Plugin) adminRecordLink(ctx context.Context, record any) (*models.BackendLink, error) { admin, ok := record.(*cabana.BackendUser) if !ok || admin == nil { return nil, errors.New("golem15.user: the record is not an administrator") } return p.backendLinks().LinkFor(ctx, admin.ID) } // payloadError is the 422 on the prompt's input. func payloadError(key string) error { return &cabana.ValidationError{Details: map[string]any{"payload": []string{backendLinkLang + key}}} } // setFrontendLink links the active frontend user whose email is the prompt // value to the administrator record. func (p *Plugin) setFrontendLink(ctx context.Context, in pact.AdminRecordActionInput) (pact.AdminRecordActionResult, error) { admin, ok := in.Record.(*cabana.BackendUser) if !ok || admin == nil { return pact.AdminRecordActionResult{}, errors.New("golem15.user: the record is not an administrator") } tx, ok := cabana.TxFromContext(ctx) if !ok { return pact.AdminRecordActionResult{}, errors.New("golem15.user: linking needs the action's transaction") } email := strings.TrimSpace(in.Value) var ids []uint if err := tx.Session(&gorm.Session{NewDB: true}).WithContext(ctx).Model(&models.User{}). Where("lower(email) = lower(?) AND is_activated = TRUE", email).Limit(2).Pluck("id", &ids).Error; err != nil { return pact.AdminRecordActionResult{}, err } if email == "" || len(ids) != 1 { return pact.AdminRecordActionResult{}, payloadError("unknown_user") } var by uint if caller, ok := bouncer.User(ctx); ok && caller != nil { by = caller.ID } _, err := p.backendLinks().SetLink(ctx, tx, ids[0], admin.ID, by) switch { case err == nil: case errors.Is(err, classes.ErrAlreadyLinked): return pact.AdminRecordActionResult{}, payloadError("already_linked") case errors.Is(err, classes.ErrFrontendUserUnavailable): return pact.AdminRecordActionResult{}, payloadError("unknown_user") case errors.Is(err, classes.ErrBackendUserUnavailable): return pact.AdminRecordActionResult{}, &cabana.ForbiddenError{} default: return pact.AdminRecordActionResult{}, err } return pact.AdminRecordActionResult{Message: backendLinkLang + "set_success"}, nil } // removeFrontendLink removes the administrator record's link. func (p *Plugin) removeFrontendLink(ctx context.Context, in pact.AdminRecordActionInput) (pact.AdminRecordActionResult, error) { tx, ok := cabana.TxFromContext(ctx) if !ok { return pact.AdminRecordActionResult{}, errors.New("golem15.user: unlinking needs the action's transaction") } link, err := p.adminRecordLink(ctx, in.Record) if err != nil { return pact.AdminRecordActionResult{}, err } if err := p.backendLinks().RemoveLink(ctx, tx, link); err != nil { return pact.AdminRecordActionResult{}, err } return pact.AdminRecordActionResult{Message: backendLinkLang + "remove_success"}, nil }