From e5c860afec86575d2c9908a051dfc7396bd39085 Mon Sep 17 00:00:00 2001 From: kolaente Date: Thu, 24 Jul 2025 15:55:15 +0200 Subject: [PATCH] feat(plugins): allow plugins to register routes --- examples/plugins/example/main.go | 54 ++++++++++++++++++++++++++++++++ pkg/plugins/interfaces.go | 13 ++++++++ pkg/plugins/manager.go | 31 ++++++++++++++++-- pkg/routes/routes.go | 12 +++++++ 4 files changed, 108 insertions(+), 2 deletions(-) diff --git a/examples/plugins/example/main.go b/examples/plugins/example/main.go index 694162c1b..81ca34b1e 100644 --- a/examples/plugins/example/main.go +++ b/examples/plugins/example/main.go @@ -17,12 +17,17 @@ package main import ( + "net/http" + + "code.vikunja.io/api/pkg/db" "code.vikunja.io/api/pkg/events" "code.vikunja.io/api/pkg/log" "code.vikunja.io/api/pkg/models" "code.vikunja.io/api/pkg/plugins" + "code.vikunja.io/api/pkg/user" "github.com/ThreeDotsLabs/watermill/message" + "github.com/labstack/echo/v4" ) type ExamplePlugin struct{} @@ -38,6 +43,55 @@ func (p *ExamplePlugin) Init() error { } func (p *ExamplePlugin) Shutdown() error { return nil } +// RegisterAuthenticatedRoutes implements the AuthenticatedRouterPlugin interface +func (p *ExamplePlugin) RegisterAuthenticatedRoutes(g *echo.Group) { + g.GET("/user-info", handleUserInfo) + + log.Infof("example plugin authenticated routes registered") +} + +// RegisterUnauthenticatedRoutes implements the UnauthenticatedRouterPlugin interface +func (p *ExamplePlugin) RegisterUnauthenticatedRoutes(g *echo.Group) { + g.GET("/status", handleStatus) + + log.Infof("example plugin unauthenticated routes registered") +} + +// Authenticated route handlers +func handleUserInfo(c echo.Context) error { + + s := db.NewSession() + defer s.Close() + + // Get the authenticated user from context + u, err := user.GetCurrentUserFromDB(s, c) + if err != nil { + return echo.NewHTTPError(http.StatusUnauthorized, "User not found") + } + + p := &ExamplePlugin{} + + return c.JSON(http.StatusOK, map[string]interface{}{ + "message": "Hello from example plugin!", + "user": u, + "plugin": p.Name(), + "version": p.Version(), + }) +} + +// Unauthenticated route handlers +func handleStatus(c echo.Context) error { + + p := &ExamplePlugin{} + + return c.JSON(http.StatusOK, map[string]interface{}{ + "status": "ok", + "plugin": p.Name(), + "version": p.Version(), + "message": "Example plugin is running", + }) +} + func NewPlugin() plugins.Plugin { return &ExamplePlugin{} } type TestListener struct{} diff --git a/pkg/plugins/interfaces.go b/pkg/plugins/interfaces.go index 597cec3eb..aef6cc8f7 100644 --- a/pkg/plugins/interfaces.go +++ b/pkg/plugins/interfaces.go @@ -17,6 +17,7 @@ package plugins import ( + "github.com/labstack/echo/v4" "src.techknowlogick.com/xormigrate" ) @@ -33,3 +34,15 @@ type MigrationPlugin interface { Plugin Migrations() []*xormigrate.Migration } + +// AuthenticatedRouterPlugin lets a plugin register authenticated web handlers and routes. +type AuthenticatedRouterPlugin interface { + Plugin + RegisterAuthenticatedRoutes(g *echo.Group) +} + +// UnauthenticatedRouterPlugin lets a plugin register unauthenticated web handlers and routes. +type UnauthenticatedRouterPlugin interface { + Plugin + RegisterUnauthenticatedRoutes(g *echo.Group) +} diff --git a/pkg/plugins/manager.go b/pkg/plugins/manager.go index d5c2337b1..50cde6a73 100644 --- a/pkg/plugins/manager.go +++ b/pkg/plugins/manager.go @@ -25,12 +25,16 @@ import ( "code.vikunja.io/api/pkg/config" "code.vikunja.io/api/pkg/log" "code.vikunja.io/api/pkg/migration" + + "github.com/labstack/echo/v4" ) // Manager handles loading and managing plugins. type Manager struct { - plugins []Plugin - migrationPlugs []MigrationPlugin + plugins []Plugin + migrationPlugs []MigrationPlugin + authenticatedRouterPlugs []AuthenticatedRouterPlugin + unauthenticatedRouterPlugs []UnauthenticatedRouterPlugin } var manager = &Manager{} @@ -70,6 +74,21 @@ func Shutdown() { } } +// RegisterPluginRoutes registers routes from all router plugins. +func RegisterPluginRoutes(authenticated *echo.Group, unauthenticated *echo.Group) { + // Register authenticated routes + for _, p := range manager.authenticatedRouterPlugs { + p.RegisterAuthenticatedRoutes(authenticated) + log.Debugf("Registered authenticated routes for plugin %s", p.Name()) + } + + // Register unauthenticated routes + for _, p := range manager.unauthenticatedRouterPlugs { + p.RegisterUnauthenticatedRoutes(unauthenticated) + log.Debugf("Registered unauthenticated routes for plugin %s", p.Name()) + } +} + func (m *Manager) loadPlugins(paths []string) error { for _, p := range paths { entries, err := os.ReadDir(p) @@ -113,6 +132,14 @@ func (m *Manager) loadPlugin(path string) error { migration.AddPluginMigrations(mp.Migrations()) } + if arp, ok := p.(AuthenticatedRouterPlugin); ok { + m.authenticatedRouterPlugs = append(m.authenticatedRouterPlugs, arp) + } + + if urp, ok := p.(UnauthenticatedRouterPlugin); ok { + m.unauthenticatedRouterPlugs = append(m.unauthenticatedRouterPlugs, urp) + } + log.Infof("Loaded plugin %s", p.Name()) return nil diff --git a/pkg/routes/routes.go b/pkg/routes/routes.go index 828e0ec5c..9d6cbac74 100644 --- a/pkg/routes/routes.go +++ b/pkg/routes/routes.go @@ -73,6 +73,7 @@ import ( "code.vikunja.io/api/pkg/modules/migration/todoist" "code.vikunja.io/api/pkg/modules/migration/trello" vikunja_file "code.vikunja.io/api/pkg/modules/migration/vikunja-file" + "code.vikunja.io/api/pkg/plugins" apiv1 "code.vikunja.io/api/pkg/routes/api/v1" "code.vikunja.io/api/pkg/routes/caldav" "code.vikunja.io/api/pkg/version" @@ -644,6 +645,17 @@ func registerAPIRoutes(a *echo.Group) { }, } a.POST("/projects/:project/views/:view/buckets/:bucket/tasks", taskBucketProvider.UpdateWeb) + + // Plugin routes + if config.PluginsEnabled.GetBool() { + // Authenticated plugin routes + authenticatedPluginGroup := a.Group("/plugins") + + // Unauthenticated plugin routes (with basic IP rate limiting) + unauthenticatedPluginGroup := n.Group("/plugins") + + plugins.RegisterPluginRoutes(authenticatedPluginGroup, unauthenticatedPluginGroup) + } } func registerMigrations(m *echo.Group) {