Files
sm-user-plugin/classes/subject_events.go
Jakub Zych 7ac80c9aea feat: verified frontend account link for administrators
- golem15_user_backend_links (1:1, both sides unique) and one-shot hashed
  golem15_user_backend_link_requests (golem15.user.backend_links.request_ttl, 10m)
- classes.BackendLinks: CreateLinkRequest, ConfirmLinkRequest (row lock,
  JWT iat not before the request), SetLink, RemoveLink, LinkedBackendUser;
  published as *classes.BackendLinks and pact.MCPLinkedBackendResolver
- GET /_user/api/v1/backend-link/{code} and POST /backend-link/confirm (jwt.auth)
- My account section frontend_account with link and unlink
- BackendLinkedEvent, BackendUnlinkedEvent and pact.MCPSubjectChanged after commit;
  deleting or deactivating a frontend user removes its link
- golem15.user.frontend_url (falls back to app.url)
2026-10-23 12:03:26 +02:00

56 lines
2.1 KiB
Go

package classes
import (
"context"
"git.golem15.com/golem15/summercms/modules/backpack"
"git.golem15.com/golem15/summercms/modules/lagoon"
"git.golem15.com/golem15/summercms/modules/pact"
"gorm.io/gorm"
)
// Guard names the plugin's pact.MCPSubjectChanged events carry: the frontend
// user guard this plugin provides and the admin's guard (for the backend
// side of an account link).
const (
SubjectGuardUser = "user"
SubjectGuardBackend = "backend"
)
// FireSubjectChanged fires one *pact.MCPSubjectChanged (guard, user id,
// kind) through the application's event bus at once, so the MCP endpoint
// re-lists the subject's tools or ends its sessions. A missing application
// or bus, or a zero id, is a no-op; a listener's error is logged and never
// returned, so the write that caused the event never fails because of it
// (every MCP request re-checks anyway).
func FireSubjectChanged(ctx context.Context, app *backpack.App, guard string, userID uint, kind string) {
if app == nil || app.Events == nil || userID == 0 || kind == "" {
return
}
if ctx == nil {
ctx = context.Background()
}
event := &pact.MCPSubjectChanged{Guard: guard, UserID: userID, Kind: kind}
if err := app.Events.Fire(ctx, event); err != nil {
pluginLog.WarnContext(ctx, "golem15.user: subject changed listener failed", "guard", guard, "user_id", userID, "kind", kind, "err", err)
}
}
// FireSubjectChangedAfterCommit is FireSubjectChanged once the write
// transaction tx commits (lagoon.AfterCommit), so a listener that reads the
// database sees the change; nothing is fired when the transaction rolls
// back. A nil tx fires at once. ctx must be the context the transaction
// runs under (the one cabana hands a hook or an action).
func FireSubjectChangedAfterCommit(ctx context.Context, app *backpack.App, tx *gorm.DB, guard string, userID uint, kind string) {
if app == nil || app.Events == nil || userID == 0 || kind == "" {
return
}
if tx == nil {
FireSubjectChanged(ctx, app, guard, userID, kind)
return
}
lagoon.AfterCommit(ctx, tx, func(ctx context.Context, _ *gorm.DB) {
FireSubjectChanged(ctx, app, guard, userID, kind)
})
}