feat: list and show quizzes over MCP for their owner

- quizzes_list_quizzes and quizzes_show_quiz bound to GET /api/v1/quizzes
  and /api/v1/quizzes/{slug} behind user.api_token, the per-user
  golem15.quizzes.manage throttle, a scope and the access_quizzes permission
- the permission is grantable through pact.HasFrontendPermissions
- owner JSON shapes with solutions in manage_json.go, never used by a
  student route
This commit is contained in:
Jakub Zych
2026-11-09 14:25:07 +01:00
parent 174be3bcbd
commit e615cc88db
10 changed files with 645 additions and 2 deletions

View File

@@ -4,14 +4,19 @@ import (
"context"
"embed"
"io/fs"
"net/http"
"strconv"
"time"
"git.golem15.com/golem15/sm-quizzes-plugin/console"
"git.golem15.com/golem15/sm-quizzes-plugin/models"
"git.golem15.com/golem15/summercms/modules/backpack"
"git.golem15.com/golem15/summercms/modules/bonfire"
"git.golem15.com/golem15/summercms/modules/bouncer"
"git.golem15.com/golem15/summercms/modules/lagoon"
"git.golem15.com/golem15/summercms/modules/pact"
"git.golem15.com/golem15/summercms/modules/party"
"git.golem15.com/golem15/summercms/modules/surf"
"github.com/go-gormigrate/gormigrate/v2"
"gorm.io/gorm"
)
@@ -28,6 +33,9 @@ var (
_ pact.AdminAssets = (*Plugin)(nil)
_ pact.HasPermissions = (*Plugin)(nil)
_ pact.HasNavigation = (*Plugin)(nil)
_ pact.HasFrontendPermissions = (*Plugin)(nil)
_ surf.BucketProvider = (*Plugin)(nil)
)
//go:embed config
@@ -115,11 +123,43 @@ func (p *Plugin) AdminFS() fs.FS { return adminFS }
// Permissions declares the admin permissions.
func (p *Plugin) Permissions() []pact.Permission {
return []pact.Permission{
{Code: "golem15.quizzes.access_quizzes", Tab: "golem15.quizzes::lang.plugin.name", Label: "golem15.quizzes::lang.permissions.access_quizzes"},
{Code: permQuizzes, Tab: "golem15.quizzes::lang.plugin.name", Label: "golem15.quizzes::lang.permissions.access_quizzes"},
{Code: "golem15.quizzes.access_attempts", Tab: "golem15.quizzes::lang.plugin.name", Label: "golem15.quizzes::lang.permissions.access_attempts"},
}
}
// FrontendPermissions declares the code the owner manage routes check (the
// MCP quiz tools), so golem15.user's User Groups editor can grant it.
// Declaring it grants nothing by itself.
func (p *Plugin) FrontendPermissions() []pact.FrontendPermission {
return []pact.FrontendPermission{{
Code: permQuizzes,
Label: "golem15.quizzes::lang.permissions.access_quizzes",
Tab: "golem15.quizzes::lang.plugin.name",
}}
}
// Buckets declares the per-user rate limit of the owner manage routes:
// building a quiz question by question is chatty, so 120 calls a minute.
// A request without a user (never after user.api_token) keys by client IP.
func (p *Plugin) Buckets() map[string]surf.Bucket {
trusted := surf.TrustedProxies(nil)
if p != nil && p.app != nil {
trusted = surf.TrustedProxies(p.app.Config)
}
byUser := func(prefix string) func(*http.Request) string {
return func(r *http.Request) string {
if u, ok := bouncer.User(r.Context()); ok && u.ID != 0 {
return prefix + "|u:" + strconv.FormatUint(uint64(u.ID), 10)
}
return prefix + "|ip:" + surf.ClientIP(r, trusted)
}
}
return map[string]surf.Bucket{
bucketManage: {Max: 120, Decay: time.Minute, Key: byUser(bucketManage)},
}
}
// Navigation declares the "Quizzes" admin menu with its side menu.
func (p *Plugin) Navigation() []pact.NavigationItem {
return []pact.NavigationItem{{