diff --git a/backend/cmd/server/main.go b/backend/cmd/server/main.go index 54474c7..eb26e9a 100644 --- a/backend/cmd/server/main.go +++ b/backend/cmd/server/main.go @@ -1,17 +1,51 @@ package main import ( + "fmt" "log" + "github.com/gin-gonic/gin" "github.com/vaultdrop/backend/config" + "github.com/vaultdrop/backend/handlers" ) +func newRouter() *gin.Engine { + + r := gin.Default() + + api := r.Group("/api/v1") + { + api.GET("/health", handlers.Health) + api.POST("/devices", handlers.DevicesRegister) + + api.GET("/files", handlers.FilesList) + api.GET("/files/search", handlers.FilesSearch) + api.GET("/files/:id", handlers.FilesGet) + api.DELETE("/files/:id", handlers.FilesDelete) + api.GET("/files/folders", handlers.FoldersList) + api.POST("/files/upload", handlers.FilesUpload) + + api.POST("/ocr/jobs", handlers.OcrJobsCreate) + api.GET("/ocr/jobs/:id", handlers.OcrJobsGet) + + api.POST("/sync/ops", handlers.SyncOpsPush) + api.GET("/sync/permissions", handlers.SyncPermissionsGet) + } + + return r + +} + func main() { - err, _ := config.LoadApplicationConfig() + err, cfg := config.LoadApplicationConfig() if err != nil { log.Fatalln(err) } + if err := newRouter().Run(fmt.Sprintf(":%d", cfg.Port)); err != nil { + log.Fatalln(err) + } + } diff --git a/backend/cmd/server/main_test.go b/backend/cmd/server/main_test.go index 7ef862f..d2caeb3 100644 --- a/backend/cmd/server/main_test.go +++ b/backend/cmd/server/main_test.go @@ -37,4 +37,4 @@ func TestCreateDocument(t *testing.T) { fmt.Println(document, directory) -} \ No newline at end of file +} diff --git a/backend/cmd/server/router_test.go b/backend/cmd/server/router_test.go new file mode 100644 index 0000000..f844c63 --- /dev/null +++ b/backend/cmd/server/router_test.go @@ -0,0 +1,40 @@ +package main + +import ( + "testing" +) + +// expectedRoutes mirrors the endpoints in mobile/api/client.ts (the contract). +// Paths are full (under /api/v1), methods match the client calls exactly. +var expectedRoutes = []string{ + "GET /api/v1/health", + "POST /api/v1/devices", + + "GET /api/v1/files", + "GET /api/v1/files/:id", + "DELETE /api/v1/files/:id", + "GET /api/v1/files/search", + "GET /api/v1/files/folders", + "POST /api/v1/files/upload", + + "POST /api/v1/ocr/jobs", + "GET /api/v1/ocr/jobs/:id", + + "POST /api/v1/sync/ops", + "GET /api/v1/sync/permissions", +} + +func TestRoutesMatchClientContract(t *testing.T) { + + registered := map[string]bool{} + for _, route := range newRouter().Routes() { + registered[route.Method+" "+route.Path] = true + } + + for _, want := range expectedRoutes { + if !registered[want] { + t.Errorf("missing route %q — must mirror mobile/api/client.ts", want) + } + } + +} diff --git a/backend/config/config.go b/backend/config/config.go index 02c96d2..f55da92 100644 --- a/backend/config/config.go +++ b/backend/config/config.go @@ -1,8 +1,6 @@ package config import ( - "fmt" - "log" "os" "strconv" @@ -10,46 +8,52 @@ import ( ) type ApplicationConfig struct { - Port int64 + Port int + DatabaseURL string + UploadDir string + MaxFileSizeMB int64 + OcrLang string + AuthSecret string } func LoadApplicationConfig() (error, *ApplicationConfig) { - err := godotenv.Load() + // .env optionnel — les défauts suffisent pour le dev local. + _ = godotenv.Load() - if err != nil { - log.Fatal("Error loading .env file") - } - - err, portString := getVar("PORT", "8080", true) + port, err := getInt("PORT", 8080) if err != nil { return err, nil } - port, err := strconv.ParseInt(portString, 10, 64) + maxSize, err := getInt("MAX_FILE_SIZE_MB", 50) if err != nil { return err, nil } return nil, &ApplicationConfig{ - Port: port, + Port: port, + DatabaseURL: get("DATABASE_URL", "postgres://vaultdrop:vaultdrop@localhost:5432/vaultdrop?sslmode=disable"), + UploadDir: get("UPLOAD_DIR", "./uploads"), + MaxFileSizeMB: int64(maxSize), + OcrLang: get("OCR_LANG", "fra+eng"), + AuthSecret: get("AUTH_SECRET", "dev-secret-change-me"), } } -func getVar(varName string, defaultValue string, isRequired bool) (error, string) { - varValue := os.Getenv(varName) - - if varValue == "" { - - if isRequired { - return fmt.Errorf("%s is missing and required : ", varName), "" - } - - return nil, defaultValue - +func get(name string, defaultValue string) string { + value := os.Getenv(name) + if value == "" { + return defaultValue } + return value +} - return nil, varValue - +func getInt(name string, defaultValue int) (int, error) { + value := os.Getenv(name) + if value == "" { + return defaultValue, nil + } + return strconv.Atoi(value) } diff --git a/backend/handlers/.gitkeep b/backend/handlers/.gitkeep deleted file mode 100644 index 4dff090..0000000 --- a/backend/handlers/.gitkeep +++ /dev/null @@ -1 +0,0 @@ -# Gin HTTP handlers land here (bind the routes, current 501 stubs). \ No newline at end of file diff --git a/backend/handlers/devices.go b/backend/handlers/devices.go new file mode 100644 index 0000000..e4ea106 --- /dev/null +++ b/backend/handlers/devices.go @@ -0,0 +1,8 @@ +package handlers + +import ( + "github.com/gin-gonic/gin" + "github.com/vaultdrop/backend/pkg/api" +) + +func DevicesRegister(c *gin.Context) { api.NotImplemented(c) } diff --git a/backend/handlers/files.go b/backend/handlers/files.go new file mode 100644 index 0000000..ad1b6e5 --- /dev/null +++ b/backend/handlers/files.go @@ -0,0 +1,12 @@ +package handlers + +import ( + "github.com/gin-gonic/gin" + "github.com/vaultdrop/backend/pkg/api" +) + +func FilesList(c *gin.Context) { api.NotImplemented(c) } +func FilesGet(c *gin.Context) { api.NotImplemented(c) } +func FilesDelete(c *gin.Context) { api.NotImplemented(c) } +func FilesSearch(c *gin.Context) { api.NotImplemented(c) } +func FilesUpload(c *gin.Context) { api.NotImplemented(c) } diff --git a/backend/handlers/folders.go b/backend/handlers/folders.go new file mode 100644 index 0000000..14a3623 --- /dev/null +++ b/backend/handlers/folders.go @@ -0,0 +1,8 @@ +package handlers + +import ( + "github.com/gin-gonic/gin" + "github.com/vaultdrop/backend/pkg/api" +) + +func FoldersList(c *gin.Context) { api.NotImplemented(c) } diff --git a/backend/handlers/health.go b/backend/handlers/health.go new file mode 100644 index 0000000..13af2db --- /dev/null +++ b/backend/handlers/health.go @@ -0,0 +1,10 @@ +package handlers + +import ( + "github.com/gin-gonic/gin" + "github.com/vaultdrop/backend/pkg/api" +) + +func Health(c *gin.Context) { + api.OK(c, gin.H{"status": "healthy"}) +} diff --git a/backend/handlers/ocr.go b/backend/handlers/ocr.go new file mode 100644 index 0000000..9520364 --- /dev/null +++ b/backend/handlers/ocr.go @@ -0,0 +1,9 @@ +package handlers + +import ( + "github.com/gin-gonic/gin" + "github.com/vaultdrop/backend/pkg/api" +) + +func OcrJobsCreate(c *gin.Context) { api.NotImplemented(c) } +func OcrJobsGet(c *gin.Context) { api.NotImplemented(c) } diff --git a/backend/handlers/sync.go b/backend/handlers/sync.go new file mode 100644 index 0000000..c5c7327 --- /dev/null +++ b/backend/handlers/sync.go @@ -0,0 +1,9 @@ +package handlers + +import ( + "github.com/gin-gonic/gin" + "github.com/vaultdrop/backend/pkg/api" +) + +func SyncOpsPush(c *gin.Context) { api.NotImplemented(c) } +func SyncPermissionsGet(c *gin.Context) { api.NotImplemented(c) } diff --git a/backend/ocr/engine.go b/backend/ocr/engine.go index 3317f4f..f17fa4f 100644 --- a/backend/ocr/engine.go +++ b/backend/ocr/engine.go @@ -7,4 +7,4 @@ import "context" // swapping to a remote service later only changes the injection point. type Engine interface { ExtractText(ctx context.Context, filePath string, lang string) (string, error) -} \ No newline at end of file +} diff --git a/backend/pkg/api/response.go b/backend/pkg/api/response.go new file mode 100644 index 0000000..7083b3f --- /dev/null +++ b/backend/pkg/api/response.go @@ -0,0 +1,32 @@ +package api + +import "github.com/gin-gonic/gin" + +const NotImplementedCode = "NOT_IMPLEMENTED" + +type Meta struct { + Page int `json:"page"` + PageSize int `json:"pageSize"` + Total int `json:"total"` +} + +func OK(c *gin.Context, data any) { + c.JSON(200, gin.H{"data": data}) +} + +func OKList(c *gin.Context, data any, page, pageSize, total int) { + c.JSON(200, gin.H{ + "data": data, + "meta": Meta{Page: page, PageSize: pageSize, Total: total}, + }) +} + +func Error(c *gin.Context, status int, code, message string) { + c.JSON(status, gin.H{ + "error": gin.H{"code": code, "message": message}, + }) +} + +func NotImplemented(c *gin.Context) { + Error(c, 501, NotImplementedCode, "route not implemented yet") +}