golem15.quizzes now serves its own import at /api/v1/quizzes/import behind the same shared API token.
155 lines
5.4 KiB
Go
155 lines
5.4 KiB
Go
package bm
|
|
|
|
import (
|
|
"net/http"
|
|
"time"
|
|
|
|
userclasses "git.golem15.com/golem15/sm-user-plugin/classes"
|
|
usermiddleware "git.golem15.com/golem15/sm-user-plugin/middleware"
|
|
"git.golem15.com/golem15/summercms/modules/pact"
|
|
"git.golem15.com/golem15/summercms/modules/phrasebook"
|
|
"git.golem15.com/golem15/summercms/modules/surf"
|
|
"git.golem15.com/jakub/sm-bm-plugin/controllers"
|
|
"git.golem15.com/jakub/sm-bm-plugin/controllers/api"
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
var (
|
|
_ pact.HasRoutes = (*Plugin)(nil)
|
|
_ surf.BucketProvider = (*Plugin)(nil)
|
|
)
|
|
|
|
// The throttles of the public calendar feed: per client IP, and per token
|
|
// so that one leaked URL cannot be hammered from many addresses.
|
|
const (
|
|
bucketCalendarIP = "throttle:jz.bm.calendar-ip"
|
|
bucketCalendarToken = "throttle:jz.bm.calendar-token"
|
|
// bucketManage throttles the management API per token, or per client IP
|
|
// for a request without one.
|
|
bucketManage = "throttle:jz.bm.manage"
|
|
)
|
|
|
|
// Buckets declares the rate limits of the public calendar feed.
|
|
func (p *Plugin) Buckets() map[string]surf.Bucket {
|
|
trusted := surf.TrustedProxies(nil)
|
|
if p.app != nil {
|
|
trusted = surf.TrustedProxies(p.app.Config)
|
|
}
|
|
return map[string]surf.Bucket{
|
|
"jz.bm.calendar-ip": {
|
|
Max: 60,
|
|
Decay: time.Minute,
|
|
Key: func(r *http.Request) string { return "jz.bm.calendar.ip|" + surf.ClientIP(r, trusted) },
|
|
},
|
|
"jz.bm.calendar-token": {
|
|
Max: 20,
|
|
Decay: time.Minute,
|
|
Key: func(r *http.Request) string { return "jz.bm.calendar.token|" + r.PathValue("token") },
|
|
},
|
|
"jz.bm.manage": {
|
|
Max: 120,
|
|
Decay: time.Minute,
|
|
Key: func(r *http.Request) string {
|
|
if secret := usermiddleware.BearerToken(r); secret != "" {
|
|
return "jz.bm.manage.token|" + userclasses.HashAPIToken(secret)
|
|
}
|
|
return "jz.bm.manage.ip|" + surf.ClientIP(r, trusted)
|
|
},
|
|
},
|
|
}
|
|
}
|
|
|
|
// APIPrefix is the path prefix of the student API.
|
|
const APIPrefix = "/_bm/api/v1"
|
|
|
|
// ManagePrefix is the path prefix of the management API.
|
|
const ManagePrefix = "/api/v1/bm"
|
|
|
|
// Routes declares the student API and the management API. Every student
|
|
// route needs a golem15.user JWT (jwt.auth); students only read, except for
|
|
// their own reading progress.
|
|
func (p *Plugin) Routes(r pact.Router) error {
|
|
h := p.handlers()
|
|
r.Group(APIPrefix, surf.Use("jwt.auth", "locale.from-principal"), func(g pact.Router) {
|
|
g.Get("/dashboard", h.Dashboard)
|
|
g.Get("/courses", h.Courses)
|
|
g.Get("/courses/{id}", h.Course)
|
|
g.Where("id", "[0-9]+")
|
|
g.Get("/schedule", h.Schedule)
|
|
g.Get("/meetings", h.Meetings)
|
|
g.Get("/deadlines", h.Deadlines)
|
|
g.Get("/readings", h.Readings)
|
|
g.Put("/readings/{id}/progress", h.MarkRead)
|
|
g.Where("id", "[0-9]+")
|
|
g.Delete("/readings/{id}/progress", h.UnmarkRead)
|
|
g.Where("id", "[0-9]+")
|
|
g.Get("/materials", h.Materials)
|
|
g.Get("/materials/{id}", h.Material)
|
|
g.Where("id", "[0-9]+")
|
|
g.Get("/calendar-feed", h.CalendarFeed)
|
|
g.Put("/calendar-feed", h.UpdateCalendarFeed)
|
|
g.Post("/calendar-feed/rotate", h.RotateCalendarFeed, "throttle:10,1")
|
|
g.Delete("/calendar-feed", h.DeleteCalendarFeed)
|
|
})
|
|
// The management API: what bm:seed does, plus materials,
|
|
// behind the application-wide user API token. Scope limits delegation;
|
|
// user.permission checks the owner's current group permissions.
|
|
r.Group(ManagePrefix, surf.Use(bucketManage, "user.api_token"), func(g pact.Router) {
|
|
g.Get("/me", h.TokenInfo, "user.scope:read")
|
|
g.Get("/courses", h.ManagedCourses, "user.scope:read", "user.permission:"+controllers.PermCourses)
|
|
g.Get("/materials", h.ManagedMaterials, "user.scope:read", "user.permission:"+controllers.PermMaterials)
|
|
g.Post("/materials", h.UpsertMaterial, "user.scope:write", "user.permission:"+controllers.PermMaterials)
|
|
g.Delete("/materials/{id}", h.DeleteMaterial, "user.scope:write", "user.permission:"+controllers.PermMaterials)
|
|
g.Where("id", "[0-9]+")
|
|
g.Post("/seed", h.ApplySeed, "user.scope:write",
|
|
"user.permission:"+controllers.PermCourses,
|
|
"user.permission:"+controllers.PermSchedule,
|
|
"user.permission:"+controllers.PermMaterials)
|
|
})
|
|
// The calendar feed: public, because calendar clients fetch it without
|
|
// credentials; the token in the path is the credential. ServeMux cannot
|
|
// match a suffix inside a segment, so {token} carries the ".ics" and the
|
|
// handler strips it.
|
|
r.Group(api.CalendarPath, surf.Use(bucketCalendarIP, bucketCalendarToken), func(g pact.Router) {
|
|
g.Get("/{token}", h.CalendarICS)
|
|
})
|
|
// The student app: the least specific pattern, so every route above and
|
|
// the admin's keep precedence. It also answers (404) the reserved paths.
|
|
static, err := newStaticHandler(p.app, p.publicFS)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
r.GroupRaw("", nil, func(g pact.Router) {
|
|
g.Get("/{path...}", static.ServeHTTP)
|
|
})
|
|
return nil
|
|
}
|
|
|
|
// handlers builds the API handlers on the application. The database is
|
|
// looked up per request, because the binary boots plugins before serve
|
|
// publishes it.
|
|
func (p *Plugin) handlers() *api.Handlers {
|
|
h := &api.Handlers{
|
|
DB: func() *gorm.DB {
|
|
if p.app == nil {
|
|
return nil
|
|
}
|
|
db, _ := p.app.Lookup[*gorm.DB]()
|
|
return db
|
|
},
|
|
Translator: func() *phrasebook.Translator {
|
|
if p.app == nil {
|
|
return nil
|
|
}
|
|
tr, _ := p.app.Lookup[*phrasebook.Translator]()
|
|
return tr
|
|
},
|
|
}
|
|
if p.app != nil && p.app.Config != nil {
|
|
h.PerPage = p.app.Config.Int("jz.bm.materials_per_page")
|
|
h.DashboardLimit = p.app.Config.Int("jz.bm.dashboard_limit")
|
|
h.PublicURL = p.app.Config.String("jz.bm.public_url")
|
|
}
|
|
return h
|
|
}
|