diff --git a/.github/workflows/synkronus-docker.yml b/.github/workflows/synkronus-docker.yml index 505eaa5bd..470c5a8e6 100644 --- a/.github/workflows/synkronus-docker.yml +++ b/.github/workflows/synkronus-docker.yml @@ -67,7 +67,7 @@ jobs: # For PRs: pr-number type=ref,event=pr # SHA for traceability (only for non-release events) - type=sha,prefix={{branch}}-,enable=${{ github.event_name != 'release' }} + type=sha,prefix=sha-,enable=${{ github.event_name != 'release' && github.event_name != 'pull_request' }} labels: | org.opencontainers.image.title=Synkronus org.opencontainers.image.description=Synchronization API for offline-first applications diff --git a/synkronus/Dockerfile b/synkronus/Dockerfile index 4ea10a9e7..81eed3834 100644 --- a/synkronus/Dockerfile +++ b/synkronus/Dockerfile @@ -39,6 +39,12 @@ WORKDIR /app # Copy binary from builder COPY --from=builder /app/synkronus . +# Copy openapi directory for Swagger UI +COPY --from=builder /app/openapi /app/openapi + +# Copy static directory for static files +COPY --from=builder /app/static /app/static + # Create directories for data storage with proper permissions RUN mkdir -p /app/data/app-bundles && \ chown -R synkronus:synkronus /app diff --git a/synkronus/internal/api/api.go b/synkronus/internal/api/api.go index 72be4ccdf..b3a4552d0 100644 --- a/synkronus/internal/api/api.go +++ b/synkronus/internal/api/api.go @@ -21,13 +21,11 @@ import ( // static files from a http.FileSystem. func FileServer(r chi.Router, path string, root http.FileSystem) { if path != "/" && path[len(path)-1] != '/' { - r.Get(path, http.RedirectHandler(path+"/", 301).ServeHTTP) + r.Get(path, http.RedirectHandler(path+"/", http.StatusMovedPermanently).ServeHTTP) path += "/" } r.Get(path+"*", func(w http.ResponseWriter, r *http.Request) { - rctx := chi.RouteContext(r.Context()) - pathPrefix := rctx.RoutePattern() - fs := http.StripPrefix(pathPrefix, http.FileServer(root)) + fs := http.StripPrefix(path, http.FileServer(root)) fs.ServeHTTP(w, r) }) } @@ -57,6 +55,8 @@ func NewRouter(log *logger.Logger, h *handlers.Handler) http.Handler { // Public endpoints r.Get("/health", h.HealthCheck) + r.Get("/openapi/swagger", http.RedirectHandler("/openapi/swagger-ui.html", http.StatusMovedPermanently).ServeHTTP) + // Serve favicon.ico r.Get("/favicon.ico", func(w http.ResponseWriter, r *http.Request) { // Get the executable directory @@ -81,6 +81,11 @@ func NewRouter(log *logger.Logger, h *handlers.Handler) http.Handler { // Serve static files from the static directory staticDir := filepath.Join(rootDir, "static") FileServer(r, "/static", http.Dir(staticDir)) + + // Serve OpenAPI documentation (Swagger UI) + appDir := filepath.Dir(execDir) + openapiDir := filepath.Join(appDir, "openapi") + FileServer(r, "/openapi", http.Dir(openapiDir)) } // Authentication routes @@ -136,7 +141,7 @@ func NewRouter(log *logger.Logger, h *handlers.Handler) http.Handler { // User management routes r.Route("/users", func(r chi.Router) { // Admin-only routes - r.With(auth.RequireRole(models.RoleAdmin)).Post("/", h.CreateUserHandler) + r.With(auth.RequireRole(models.RoleAdmin)).Post("/create", h.CreateUserHandler) r.With(auth.RequireRole(models.RoleAdmin)).Delete("/delete/{username}", h.DeleteUserHandler) r.With(auth.RequireRole(models.RoleAdmin)).Post("/reset-password", h.ResetPasswordHandler) r.With(auth.RequireRole(models.RoleAdmin)).Get("/", h.ListUsersHandler) diff --git a/synkronus/openapi/swagger-ui.html b/synkronus/openapi/swagger-ui.html new file mode 100644 index 000000000..44cbe2cc6 --- /dev/null +++ b/synkronus/openapi/swagger-ui.html @@ -0,0 +1,20 @@ + + + + Synkronus API Documentation + + + +
+ + + + +