feat: link and unlink an administrator's frontend account on the Administrators screen
- golem15.users.manage_backend_links (no default role), en and pl labels - record actions on summercms.cabana.users: link_frontend_account asks for the frontend user's email (exact, case-insensitive, active users only), records linked_by and fires the link events; unlink_frontend_account (danger) removes the link - a taken or unknown account answers 422 on the prompt
This commit is contained in:
128
backend_links_admin.go
Normal file
128
backend_links_admin.go
Normal file
@@ -0,0 +1,128 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user