Files
osdb-1-claude-opusplan/backend/cmd/server/router.go
T
juliaandClaude Sonnet 4.6 1b381d9385 feat: protect write endpoints with JWT auth (story #08)
Add bcrypt user file auth, JWT middleware on all write routes, Pinia
authStore with localStorage persistence, login view with redirect
support, and v-if guards on all CRUD controls and admin nav link.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-06-19 11:18:42 +02:00

89 lines
3.0 KiB
Go

package main
import (
"github.com/jackc/pgx/v5/pgxpool"
"github.com/labstack/echo/v4"
"github.com/labstack/echo/v4/middleware"
"osdb/internal/handler"
mw "osdb/internal/middleware"
"osdb/internal/repository"
)
// New creates and configures the Echo instance with all routes registered.
func New(pool *pgxpool.Pool, users map[string]string, jwtSecret string) *echo.Echo {
e := echo.New()
e.HideBanner = true
e.Use(middleware.Logger())
e.Use(middleware.Recover())
jwtAuth := mw.JWTAuth(jwtSecret)
health := handler.NewHealthHandler()
// Root health — used by docker healthcheck + ops tooling
e.GET("/health", health.Health)
// Versioned API group
api := e.Group("/api/v1")
api.GET("/health", health.Health)
// Auth (story #08)
authHandler := handler.NewAuthHandler(users, jwtSecret)
api.POST("/auth/login", authHandler.Login)
// Fruits (story #02)
fruitRepo := repository.NewFruitRepo(pool)
fruits := handler.NewFruitHandler(fruitRepo)
g := api.Group("/fruits")
g.GET("", fruits.List)
g.POST("", fruits.Create, jwtAuth)
g.GET("/:id", fruits.Get)
g.PUT("/:id", fruits.Update, jwtAuth)
g.DELETE("/:id", fruits.Delete, jwtAuth)
g.GET("/:id/images", fruits.ListImages)
g.POST("/:id/images", fruits.UploadImage, jwtAuth, middleware.BodyLimit("5M"))
g.GET("/:id/images/:imageId", fruits.ServeImage)
g.GET("/:id/images/:imageId/thumbnail", fruits.ServeThumbnail)
g.DELETE("/:id/images/:imageId", fruits.DeleteImage, jwtAuth)
// Publications (story #04)
pubRepo := repository.NewPublicationRepo(pool)
pubs := handler.NewPublicationHandler(pubRepo)
// Fruit-scoped publication reads
g.GET("/:id/descriptions", pubs.GetFruitDescriptions)
g.GET("/:id/publication-images", pubs.GetFruitPublicationImages)
p := api.Group("/publications")
p.GET("", pubs.List)
p.POST("", pubs.Create, jwtAuth)
p.GET("/:id", pubs.Get)
p.PUT("/:id", pubs.Update, jwtAuth)
p.DELETE("/:id", pubs.Delete, jwtAuth)
p.POST("/:id/image", pubs.UploadCoverImage, jwtAuth, middleware.BodyLimit("5M"))
p.GET("/:id/image", pubs.ServeCoverImage)
p.DELETE("/:id/image", pubs.DeleteCoverImage, jwtAuth)
p.GET("/:id/fruits", pubs.ListFruits)
p.POST("/:id/fruits", pubs.LinkFruit, jwtAuth)
p.DELETE("/:id/fruits/:fruitId", pubs.UnlinkFruit, jwtAuth)
p.GET("/:id/descriptions", pubs.ListDescriptions)
p.POST("/:id/descriptions", pubs.UploadDescription, jwtAuth, middleware.BodyLimit("5M"))
p.GET("/:id/descriptions/:descId", pubs.ServeDescription)
p.DELETE("/:id/descriptions/:descId", pubs.DeleteDescription, jwtAuth)
p.GET("/:id/fruit-images", pubs.ListFruitImages)
p.POST("/:id/fruit-images", pubs.UploadFruitImage, jwtAuth, middleware.BodyLimit("5M"))
p.GET("/:id/fruit-images/:imgId", pubs.ServeFruitImage)
p.GET("/:id/fruit-images/:imgId/thumbnail", pubs.ServeFruitImageThumbnail)
p.DELETE("/:id/fruit-images/:imgId", pubs.DeleteFruitImage, jwtAuth)
// Admin (story #07) — all admin routes require auth
admin := handler.NewAdminHandler(fruitRepo, pubRepo)
adminGroup := api.Group("/admin", jwtAuth)
adminGroup.POST("/backfill-thumbnails", admin.BackfillThumbnails)
return e
}