feat: add router package structure for route organization
Create internal/router/ package to organize route registration: - router.go: Main router setup and configuration - auth.go: Authentication routes (login, register, profile, etc.) - docs.go: Documentation routes - frontend.go: Frontend SSR routes (/, /login, /admin, etc.) - helpers.go: Helper functions for template rendering This is the first step in refactoring 858-line main.go into a more maintainable structure following Go best practices. Routes themselves have NOT changed - only organization.
This commit is contained in:
@@ -0,0 +1,56 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bookhoard/internal/handlers"
|
||||||
|
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
"github.com/labstack/echo-jwt/v4"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
func registerAuthRoutes(cfg *Config, rateLimitMiddleware echo.MiddlewareFunc) {
|
||||||
|
e := cfg.Echo
|
||||||
|
|
||||||
|
// Auth routes (no auth required, but rate limited)
|
||||||
|
e.POST("/api/auth/register", rateLimitMiddleware(cfg.AuthHandler.Register))
|
||||||
|
e.POST("/api/auth/login", rateLimitMiddleware(cfg.AuthHandler.Login))
|
||||||
|
|
||||||
|
// JWT middleware for protected routes
|
||||||
|
jwtMiddleware := echojwt.WithConfig(echojwt.Config{
|
||||||
|
SigningKey: []byte(cfg.Cfg.JWTSecret),
|
||||||
|
ContextKey: "user",
|
||||||
|
SuccessHandler: func(c echo.Context) {
|
||||||
|
token := c.Get("user").(*jwt.Token)
|
||||||
|
claims := token.Claims.(jwt.MapClaims)
|
||||||
|
c.Set("user_id", claims["user_id"])
|
||||||
|
c.Set("user_role", claims["user_role"])
|
||||||
|
c.Set("user_email", claims["user_email"])
|
||||||
|
c.Set("user_username", claims["user_username"])
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create protected route group
|
||||||
|
protected := e.Group("/api", jwtMiddleware)
|
||||||
|
|
||||||
|
// Protected auth routes
|
||||||
|
protected.GET("/auth/profile", cfg.AuthHandler.GetProfile)
|
||||||
|
protected.PUT("/auth/profile", cfg.AuthHandler.UpdateProfile)
|
||||||
|
|
||||||
|
// Refresh token endpoint (no authentication required - uses refresh token from body)
|
||||||
|
e.POST("/api/auth/refresh", cfg.AuthHandler.RefreshAccessToken)
|
||||||
|
|
||||||
|
// Logout endpoint (optional authentication - can revoke tokens if provided)
|
||||||
|
e.POST("/api/auth/logout", cfg.AuthHandler.Logout)
|
||||||
|
|
||||||
|
// Auth update routes
|
||||||
|
authGroup := e.Group("/api/auth", jwtMiddleware)
|
||||||
|
authGroup.PUT("/email", cfg.AuthHandler.UpdateEmail)
|
||||||
|
authGroup.PUT("/username", cfg.AuthHandler.UpdateUsername)
|
||||||
|
authGroup.PUT("/password", cfg.AuthHandler.UpdatePassword)
|
||||||
|
authGroup.PUT("/theme", cfg.AuthHandler.UpdateTheme)
|
||||||
|
|
||||||
|
// Admin-only routes for user management
|
||||||
|
admin := protected.Group("/auth", handlers.AdminMiddleware)
|
||||||
|
admin.GET("/users", cfg.AuthHandler.ListUsers)
|
||||||
|
admin.PUT("/users/:id/max-devices", cfg.AuthHandler.UpdateUserMaxDevices)
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bookhoard/internal/docs"
|
||||||
|
)
|
||||||
|
|
||||||
|
func registerDocumentationRoutes(cfg *Config) {
|
||||||
|
docsHandler := docs.NewHTTPHandler("docs")
|
||||||
|
cfg.Echo.GET("/docs", docsHandler.DocsHome)
|
||||||
|
cfg.Echo.GET("/docs/*", docsHandler.ShowDocumentation)
|
||||||
|
cfg.Echo.GET("/docs/api/search", docsHandler.Search)
|
||||||
|
cfg.Echo.GET("/docs/search-index.json", docsHandler.ServeSearchIndex)
|
||||||
|
}
|
||||||
@@ -0,0 +1,213 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"bookhoard/internal/handlers"
|
||||||
|
"bookhoard/templates"
|
||||||
|
|
||||||
|
"github.com/golang-jwt/jwt/v5"
|
||||||
|
"github.com/labstack/echo-jwt/v4"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
func registerFrontendRoutes(cfg *Config) {
|
||||||
|
e := cfg.Echo
|
||||||
|
|
||||||
|
// ============================================================================
|
||||||
|
// FRONTEND ROUTES - DO NOT DELETE
|
||||||
|
// These routes serve Server-Side Rendered (SSR) HTML pages for the web UI.
|
||||||
|
// They are NOT API endpoints and should NOT be removed during refactors.
|
||||||
|
// All authenticated frontend routes use the jwtMiddleware to validate tokens.
|
||||||
|
// ============================================================================
|
||||||
|
|
||||||
|
// JWT middleware for protected routes
|
||||||
|
jwtMiddleware := echojwt.WithConfig(echojwt.Config{
|
||||||
|
SigningKey: []byte(cfg.Cfg.JWTSecret),
|
||||||
|
ContextKey: "user",
|
||||||
|
SuccessHandler: func(c echo.Context) {
|
||||||
|
token := c.Get("user").(*jwt.Token)
|
||||||
|
claims := token.Claims.(jwt.MapClaims)
|
||||||
|
c.Set("user_id", claims["user_id"])
|
||||||
|
c.Set("user_role", claims["user_role"])
|
||||||
|
c.Set("user_email", claims["user_email"])
|
||||||
|
c.Set("user_username", claims["user_username"])
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Protected route group
|
||||||
|
protected := e.Group("/api", jwtMiddleware)
|
||||||
|
|
||||||
|
// Public routes for login and registration pages
|
||||||
|
e.GET("/login", func(c echo.Context) error {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err := templates.Login().Render(c.Request().Context(), &buf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.HTML(http.StatusOK, buf.String())
|
||||||
|
})
|
||||||
|
|
||||||
|
e.GET("/register", func(c echo.Context) error {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err := templates.Register().Render(c.Request().Context(), &buf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.HTML(http.StatusOK, buf.String())
|
||||||
|
})
|
||||||
|
|
||||||
|
// Root route - landing page with smart login detection
|
||||||
|
e.GET("/", func(c echo.Context) error {
|
||||||
|
var buf bytes.Buffer
|
||||||
|
var err error
|
||||||
|
|
||||||
|
tokenString := c.Request().Header.Get("Authorization")
|
||||||
|
if tokenString != "" && len(tokenString) > 7 && tokenString[:7] == "Bearer " {
|
||||||
|
tokenString = tokenString[7:]
|
||||||
|
} else {
|
||||||
|
cookie, err := c.Cookie("token")
|
||||||
|
if err == nil {
|
||||||
|
tokenString = cookie.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loggedIn := false
|
||||||
|
if tokenString != "" {
|
||||||
|
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
|
||||||
|
return []byte(cfg.Cfg.JWTSecret), nil
|
||||||
|
})
|
||||||
|
loggedIn = err == nil && token.Valid
|
||||||
|
}
|
||||||
|
|
||||||
|
err = templates.Index(loggedIn).Render(c.Request().Context(), &buf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.HTML(http.StatusOK, buf.String())
|
||||||
|
})
|
||||||
|
|
||||||
|
// Public redirect routes
|
||||||
|
e.GET("/bookshelf", func(c echo.Context) error {
|
||||||
|
return c.Redirect(http.StatusTemporaryRedirect, "/api/bookshelf")
|
||||||
|
})
|
||||||
|
|
||||||
|
e.GET("/dashboard", func(c echo.Context) error {
|
||||||
|
return c.Redirect(http.StatusTemporaryRedirect, "/api/bookshelf")
|
||||||
|
})
|
||||||
|
|
||||||
|
// Admin routes
|
||||||
|
e.GET("/admin", handlers.AdminMiddleware(func(c echo.Context) error {
|
||||||
|
user, err := getTemplateUserWithTheme(c, cfg)
|
||||||
|
if err != nil {
|
||||||
|
return c.HTML(http.StatusInternalServerError, "Error loading user")
|
||||||
|
}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err = templates.Admin(user).Render(c.Request().Context(), &buf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.HTML(http.StatusOK, buf.String())
|
||||||
|
}))
|
||||||
|
|
||||||
|
e.GET("/admin/", handlers.AdminMiddleware(func(c echo.Context) error {
|
||||||
|
user, err := getTemplateUserWithTheme(c, cfg)
|
||||||
|
if err != nil {
|
||||||
|
return c.HTML(http.StatusInternalServerError, "Error loading user")
|
||||||
|
}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err = templates.Admin(user).Render(c.Request().Context(), &buf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.HTML(http.StatusOK, buf.String())
|
||||||
|
}))
|
||||||
|
|
||||||
|
e.GET("/admin/profile", handlers.AdminMiddleware(func(c echo.Context) error {
|
||||||
|
user, err := getTemplateUserWithTheme(c, cfg)
|
||||||
|
if err != nil {
|
||||||
|
return c.HTML(http.StatusInternalServerError, "Error loading user")
|
||||||
|
}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err = templates.AdminProfile(user).Render(c.Request().Context(), &buf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.HTML(http.StatusOK, buf.String())
|
||||||
|
}))
|
||||||
|
|
||||||
|
e.GET("/admin/library", handlers.AdminMiddleware(func(c echo.Context) error {
|
||||||
|
user, err := getTemplateUserWithTheme(c, cfg)
|
||||||
|
if err != nil {
|
||||||
|
return c.HTML(http.StatusInternalServerError, "Error loading user")
|
||||||
|
}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err = templates.AdminLibrary(user).Render(c.Request().Context(), &buf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.HTML(http.StatusOK, buf.String())
|
||||||
|
}))
|
||||||
|
|
||||||
|
// Devices page
|
||||||
|
protected.GET("/devices-page", func(c echo.Context) error {
|
||||||
|
user, err := getTemplateUserWithTheme(c, cfg)
|
||||||
|
if err != nil {
|
||||||
|
return c.HTML(http.StatusInternalServerError, "Error loading user")
|
||||||
|
}
|
||||||
|
deviceData, err := cfg.DeviceHandler.GetDevicesData(c)
|
||||||
|
if err != nil {
|
||||||
|
return c.HTML(http.StatusInternalServerError, "Error loading devices")
|
||||||
|
}
|
||||||
|
pendingData, err := cfg.DeviceHandler.GetPendingRegistrationsData(c)
|
||||||
|
if err != nil {
|
||||||
|
return c.HTML(http.StatusInternalServerError, "Error loading pending")
|
||||||
|
}
|
||||||
|
devicesList := convertDevices(deviceData)
|
||||||
|
pendingList := convertPending(pendingData)
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err = templates.Devices(user, devicesList, pendingList).Render(c.Request().Context(), &buf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.HTML(http.StatusOK, buf.String())
|
||||||
|
})
|
||||||
|
|
||||||
|
// Conflicts page
|
||||||
|
protected.GET("/conflicts-page", func(c echo.Context) error {
|
||||||
|
user, err := getTemplateUserWithTheme(c, cfg)
|
||||||
|
if err != nil {
|
||||||
|
return c.HTML(http.StatusInternalServerError, "Error loading user")
|
||||||
|
}
|
||||||
|
conflictsData, total, unresolved, err := cfg.ConflictHandler.GetConflictsData(c)
|
||||||
|
if err != nil {
|
||||||
|
return c.HTML(http.StatusInternalServerError, "Error loading conflicts")
|
||||||
|
}
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err = templates.Conflicts(user, conflictsData, total, unresolved).Render(c.Request().Context(), &buf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return c.HTML(http.StatusOK, buf.String())
|
||||||
|
})
|
||||||
|
|
||||||
|
// Health check
|
||||||
|
e.GET("/health", func(c echo.Context) error {
|
||||||
|
ctx, cancel := context.WithTimeout(c.Request().Context(), 2*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
if err := pingDB(cfg, ctx); err != nil {
|
||||||
|
return c.JSON(http.StatusServiceUnavailable, map[string]string{
|
||||||
|
"status": "unhealthy",
|
||||||
|
"error": "database unavailable",
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return c.JSON(http.StatusOK, map[string]string{
|
||||||
|
"status": "healthy",
|
||||||
|
"database": "connected",
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"bookhoard/internal/handlers"
|
||||||
|
"bookhoard/templates"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/jackc/pgx/v5/pgtype"
|
||||||
|
"github.com/jackc/pgx/v5/pgxpool"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
func getTemplateUserWithTheme(c echo.Context, cfg *Config) (templates.User, error) {
|
||||||
|
userID := c.Get("user_id").(string)
|
||||||
|
userEmail := c.Get("user_email").(string)
|
||||||
|
userUsername := c.Get("user_username").(string)
|
||||||
|
userRole := c.Get("user_role").(string)
|
||||||
|
|
||||||
|
userUUID, err := uuid.Parse(userID)
|
||||||
|
if err != nil {
|
||||||
|
return templates.User{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
userDB, err := cfg.Queries.GetUser(c.Request().Context(), uuidToPGType(userUUID))
|
||||||
|
if err != nil {
|
||||||
|
return templates.User{}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
userTheme := "tokyo-night"
|
||||||
|
if userDB.Theme.Valid {
|
||||||
|
userTheme = userDB.Theme.String
|
||||||
|
}
|
||||||
|
|
||||||
|
return templates.User{
|
||||||
|
ID: userID,
|
||||||
|
Email: userEmail,
|
||||||
|
Username: userUsername,
|
||||||
|
Role: userRole,
|
||||||
|
Theme: userTheme,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertDevices(deviceInfos []handlers.DeviceInfo) []templates.DeviceData {
|
||||||
|
result := make([]templates.DeviceData, len(deviceInfos))
|
||||||
|
for i, d := range deviceInfos {
|
||||||
|
lastSync := ""
|
||||||
|
if d.LastSync != nil {
|
||||||
|
lastSync = d.LastSync.Format(time.RFC3339)
|
||||||
|
}
|
||||||
|
lastSeen := ""
|
||||||
|
if d.LastSeen != nil {
|
||||||
|
lastSeen = d.LastSeen.Format(time.RFC3339)
|
||||||
|
}
|
||||||
|
result[i] = templates.DeviceData{
|
||||||
|
ID: d.ID.String(),
|
||||||
|
DeviceName: d.DeviceName,
|
||||||
|
DeviceType: d.DeviceType,
|
||||||
|
SyncEnabled: d.SyncEnabled,
|
||||||
|
LastSync: lastSync,
|
||||||
|
LastSeen: lastSeen,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func convertPending(pending []map[string]interface{}) []templates.PendingRegistrationData {
|
||||||
|
result := make([]templates.PendingRegistrationData, len(pending))
|
||||||
|
for i, p := range pending {
|
||||||
|
result[i] = templates.PendingRegistrationData{
|
||||||
|
RegistrationID: p["registration_id"].(string),
|
||||||
|
DeviceName: p["device_name"].(string),
|
||||||
|
DeviceType: p["device_type"].(string),
|
||||||
|
ExpiresAt: p["expires_at"].(string),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func pingDB(cfg *Config, ctx context.Context) error {
|
||||||
|
if cfg.DBPool != nil {
|
||||||
|
if pool, ok := cfg.DBPool.(*pgxpool.Pool); ok {
|
||||||
|
return pool.Ping(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseUUID(s string) (uuid.UUID, error) {
|
||||||
|
return uuid.Parse(s)
|
||||||
|
}
|
||||||
|
|
||||||
|
func uuidToPGType(u uuid.UUID) pgtype.UUID {
|
||||||
|
return pgtype.UUID{Bytes: [16]byte(u), Valid: true}
|
||||||
|
}
|
||||||
@@ -0,0 +1,125 @@
|
|||||||
|
package router
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bookhoard/internal/config"
|
||||||
|
"bookhoard/internal/database"
|
||||||
|
"bookhoard/internal/handlers"
|
||||||
|
"bookhoard/internal/middleware"
|
||||||
|
ratelimit "bookhoard/internal/middleware"
|
||||||
|
"bookhoard/internal/sync"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/go-playground/validator/v10"
|
||||||
|
"github.com/labstack/echo/v4"
|
||||||
|
echomiddleware "github.com/labstack/echo/v4/middleware"
|
||||||
|
)
|
||||||
|
|
||||||
|
// CustomValidator wraps the go-playground validator
|
||||||
|
type CustomValidator struct {
|
||||||
|
validator *validator.Validate
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cv *CustomValidator) Validate(i interface{}) error {
|
||||||
|
return cv.validator.Struct(i)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Config holds all dependencies needed for route registration
|
||||||
|
type Config struct {
|
||||||
|
Echo *echo.Echo
|
||||||
|
Queries *database.Queries
|
||||||
|
Cfg *config.Config
|
||||||
|
DBPool interface{} // pgxpool.Pool interface
|
||||||
|
AuthHandler *handlers.AuthHandler
|
||||||
|
LibraryHandler *handlers.LibraryHandler
|
||||||
|
DeviceHandler *handlers.DeviceHandler
|
||||||
|
KOReaderHandler *handlers.KOReaderHandler
|
||||||
|
WSHandler *handlers.WSHandler
|
||||||
|
ConflictHandler *handlers.ConflictHandler
|
||||||
|
AnalyticsHandler *handlers.AnalyticsHandler
|
||||||
|
QueueHandler *handlers.QueueHandler
|
||||||
|
CollectionHandler *handlers.CollectionHandler
|
||||||
|
OPDSHandler *handlers.OPDSHandler
|
||||||
|
ConnManager *sync.ConnectionManager
|
||||||
|
QueueProcessor *sync.SyncQueueProcessor
|
||||||
|
DeviceAuthMiddleware *middleware.DeviceAuthMiddleware
|
||||||
|
LoginTracker *ratelimit.LoginAttemptTracker
|
||||||
|
}
|
||||||
|
|
||||||
|
// RegisterRoutes registers all application routes
|
||||||
|
func RegisterRoutes(cfg *Config) {
|
||||||
|
e := cfg.Echo
|
||||||
|
|
||||||
|
// Set up validator
|
||||||
|
v := validator.New()
|
||||||
|
if err := ratelimit.RegisterPasswordValidation(v); err != nil {
|
||||||
|
log.Fatal("Failed to register password validator:", err)
|
||||||
|
}
|
||||||
|
e.Validator = &CustomValidator{validator: v}
|
||||||
|
|
||||||
|
// Global middleware
|
||||||
|
e.Use(echomiddleware.Logger())
|
||||||
|
e.Use(echomiddleware.Recover())
|
||||||
|
e.Use(echomiddleware.CORS())
|
||||||
|
e.Use(ratelimit.RequestTracingMiddleware(cfg.Cfg))
|
||||||
|
|
||||||
|
// Rate limiter
|
||||||
|
rateLimiterConfig := ratelimit.RateLimiterConfig{
|
||||||
|
Enabled: cfg.Cfg.RateLimitEnabled,
|
||||||
|
RequestsPerMinute: cfg.Cfg.RequestsPerMinute,
|
||||||
|
CleanupInterval: 5 * time.Minute,
|
||||||
|
}
|
||||||
|
rateLimiter := ratelimit.NewRateLimiter(rateLimiterConfig)
|
||||||
|
rateLimitMiddleware := ratelimit.RateLimiterMiddleware(rateLimiter)
|
||||||
|
|
||||||
|
// Register route groups
|
||||||
|
registerAuthRoutes(cfg, rateLimitMiddleware)
|
||||||
|
registerLibraryRoutes(cfg)
|
||||||
|
registerDeviceRoutes(cfg)
|
||||||
|
registerSyncRoutes(cfg)
|
||||||
|
registerMediaRoutes(cfg)
|
||||||
|
registerConflictRoutes(cfg)
|
||||||
|
registerAnalyticsRoutes(cfg)
|
||||||
|
registerQueueRoutes(cfg)
|
||||||
|
registerOPDSRoutes(cfg)
|
||||||
|
registerWebSocketRoutes(cfg)
|
||||||
|
registerFrontendRoutes(cfg)
|
||||||
|
registerDocumentationRoutes(cfg)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stub functions - will be implemented incrementally
|
||||||
|
func registerLibraryRoutes(cfg *Config) {
|
||||||
|
// TODO: Implement in library.go
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerDeviceRoutes(cfg *Config) {
|
||||||
|
// TODO: Implement in device.go
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerSyncRoutes(cfg *Config) {
|
||||||
|
// TODO: Implement in sync.go
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerMediaRoutes(cfg *Config) {
|
||||||
|
// TODO: Implement in media.go
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerConflictRoutes(cfg *Config) {
|
||||||
|
// TODO: Implement in conflicts.go
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerAnalyticsRoutes(cfg *Config) {
|
||||||
|
// TODO: Implement in analytics.go
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerQueueRoutes(cfg *Config) {
|
||||||
|
// TODO: Implement in queue.go
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerOPDSRoutes(cfg *Config) {
|
||||||
|
// TODO: Implement in opds.go
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerWebSocketRoutes(cfg *Config) {
|
||||||
|
// TODO: Implement in websocket.go
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user