From 4a6209a8dbb37b5821b03d8cfb0149d1980595f8 Mon Sep 17 00:00:00 2001 From: Floriansylvain Date: Wed, 24 Dec 2025 01:05:58 +0100 Subject: [PATCH] feat: api serve frontend, build workflow, router readyness --- .gitignore | 1 + api/handlers/auth/authHandler.go | 8 ++- api/handlers/frontend/frontHandler.go | 79 ++++++++++++++++++++++++ api/middleware/auth.go | 11 +--- api/router.go | 24 ++++++- internal/infrastructure/assets/assets.go | 6 ++ scripts/build.sh | 41 ++++++++++++ super_build.sh => scripts/build_super.sh | 3 +- templ_static_files_validator.sh | 51 --------------- web/src/components/NavComponent.vue | 5 ++ web/src/router/index.ts | 3 +- web/src/views/ArticleView.vue | 7 +++ web/vite.config.ts | 9 ++- 13 files changed, 174 insertions(+), 74 deletions(-) create mode 100644 api/handlers/frontend/frontHandler.go create mode 100644 internal/infrastructure/assets/assets.go create mode 100644 scripts/build.sh rename super_build.sh => scripts/build_super.sh (95%) delete mode 100644 templ_static_files_validator.sh create mode 100644 web/src/views/ArticleView.vue diff --git a/.gitignore b/.gitignore index 8ea53dc..6fe6716 100644 --- a/.gitignore +++ b/.gitignore @@ -7,6 +7,7 @@ # Build bin +dist/ # Environment .env diff --git a/api/handlers/auth/authHandler.go b/api/handlers/auth/authHandler.go index 56c7bf0..682d3b8 100644 --- a/api/handlers/auth/authHandler.go +++ b/api/handlers/auth/authHandler.go @@ -32,7 +32,9 @@ type LoginCredentials struct { Password string `json:"password" validate:"required,min=8"` } -var shouldCookieBeSecure = os.Getenv("ENVIRONMENT") == "production" +func shouldCookieBeSecure() bool { + return os.Getenv("ENVIRONMENT") == "production" +} func removeJwtCookie(w http.ResponseWriter) { http.SetCookie(w, &http.Cookie{ @@ -40,7 +42,7 @@ func removeJwtCookie(w http.ResponseWriter) { Value: "", Expires: time.Now(), MaxAge: -1, - Secure: shouldCookieBeSecure, + Secure: shouldCookieBeSecure(), HttpOnly: true, Path: "/", }) @@ -64,7 +66,7 @@ func (h *Handler) SetJwtCookie(w *http.ResponseWriter, userId uint32) error { Name: "jwt", Value: tokenString, Expires: time.Now().Add(24 * time.Hour), - Secure: shouldCookieBeSecure, + Secure: shouldCookieBeSecure(), HttpOnly: true, Path: "/", }) diff --git a/api/handlers/frontend/frontHandler.go b/api/handlers/frontend/frontHandler.go new file mode 100644 index 0000000..3cdd7eb --- /dev/null +++ b/api/handlers/frontend/frontHandler.go @@ -0,0 +1,79 @@ +package frontend + +import ( + "RenewCMS/internal/infrastructure/assets" + "io" + "io/fs" + "net/http" + "path/filepath" + "strings" + "time" + + "github.com/go-chi/chi/v5" +) + +type spaHandler struct { + staticFS fs.FS + modTime time.Time +} + +func NewFrontendRouter() http.Handler { + r := chi.NewRouter() + + distFS, _ := fs.Sub(assets.DistEmbed, "dist") + handler := &spaHandler{ + staticFS: distFS, + modTime: time.Now(), + } + + r.Get("/*", handler.ServeHTTP) + return r +} + +func (h *spaHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + path := h.cleanPath(r.URL.Path) + if h.fileExists(path) { + h.serveAsset(w, r, path) + return + } + h.serveIndex(w, r) +} + +func (h *spaHandler) cleanPath(path string) string { + return strings.TrimPrefix(filepath.Clean(path), "/") +} + +func (h *spaHandler) fileExists(path string) bool { + if path == "" || path == "." { + return false + } + f, err := h.staticFS.Open(path) + if err != nil { + return false + } + defer f.Close() + return true +} + +func (h *spaHandler) serveAsset(w http.ResponseWriter, r *http.Request, path string) { + w.Header().Set("Cache-Control", "public, max-age=31536000, immutable") + http.FileServer(http.FS(h.staticFS)).ServeHTTP(w, r) +} + +func (h *spaHandler) serveIndex(w http.ResponseWriter, r *http.Request) { + f, err := h.staticFS.Open("index.html") + if err != nil { + http.Error(w, "Index not found", http.StatusNotFound) + return + } + defer f.Close() + + content, ok := f.(io.ReadSeeker) + if !ok { + http.Error(w, "File not seekable", http.StatusInternalServerError) + return + } + + w.Header().Set("Cache-Control", "no-store, no-cache, must-revalidate") + http.ServeContent(w, r, "index.html", h.modTime, content) +} diff --git a/api/middleware/auth.go b/api/middleware/auth.go index 7e1a698..facf2d7 100644 --- a/api/middleware/auth.go +++ b/api/middleware/auth.go @@ -2,18 +2,9 @@ package middleware import "net/http" -const KeyContentType = "Content-Type" - func JsonContentTypeMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set(KeyContentType, "application/json") - next.ServeHTTP(w, r) - }) -} - -func HtmlContentTypeMiddleware(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set(KeyContentType, "text/html") + w.Header().Set("Content-Type", "application/json") next.ServeHTTP(w, r) }) } diff --git a/api/router.go b/api/router.go index 539b069..17043b2 100644 --- a/api/router.go +++ b/api/router.go @@ -3,6 +3,7 @@ package api import ( "RenewCMS/api/handlers/article" "RenewCMS/api/handlers/auth" + "RenewCMS/api/handlers/frontend" "RenewCMS/api/middleware" "encoding/json" "net/http" @@ -15,6 +16,12 @@ import ( "github.com/go-chi/jwtauth/v5" ) +func enableLoggingIfDev(r *chi.Mux, routerName string) { + if os.Getenv("ENVIRONMENT") == "development" { + r.Use(httplog.LoggerWithName(routerName)) + } +} + func GetHelloWorld(w http.ResponseWriter, _ *http.Request) { msg, _ := json.Marshal(map[string]string{"message": "Hello World"}) _, _ = w.Write(msg) @@ -46,7 +53,8 @@ func NewAuthRouter(container *UseCases) http.Handler { func InitBackendRoutes(container *UseCases) *chi.Mux { r := chi.NewRouter() - r.Use(httplog.LoggerWithName("backend")) + enableLoggingIfDev(r, "backend") + r.Use(middleware.JsonContentTypeMiddleware) r.Get("/", GetHelloWorld) r.Group(func(r chi.Router) { @@ -59,19 +67,31 @@ func InitBackendRoutes(container *UseCases) *chi.Mux { return r } +func InitFrontendRoutes() *chi.Mux { + r := chi.NewRouter() + + enableLoggingIfDev(r, "frontend") + + r.Mount("/", frontend.NewFrontendRouter()) + + return r +} + func InitRoutes(container *UseCases) *chi.Mux { backend := InitBackendRoutes(container) + frontend := InitFrontendRoutes() apiRouter := chi.NewRouter() apiRouter.Use(cors.Handler(cors.Options{ AllowedOrigins: strings.Split(os.Getenv("CORS_ALLOWED_ORIGINS"), ";"), AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"}, - AllowedHeaders: []string{"Accept", "Authorization", middleware.KeyContentType, "X-CSRF-Token"}, + AllowedHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"}, ExposedHeaders: []string{"Link"}, AllowCredentials: false, MaxAge: 300, })) apiRouter.Mount("/v1", backend) + apiRouter.Mount("/", frontend) return apiRouter } diff --git a/internal/infrastructure/assets/assets.go b/internal/infrastructure/assets/assets.go new file mode 100644 index 0000000..5723ce6 --- /dev/null +++ b/internal/infrastructure/assets/assets.go @@ -0,0 +1,6 @@ +package assets + +import "embed" + +//go:embed all:dist +var DistEmbed embed.FS diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100644 index 0000000..d2ab30a --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,41 @@ +#!/bin/sh + +OUTPUT_DIR=./bin +GO_FILE_PATH="./cmd/server/main.go" +PROGRAM_NAME=RenewCMS +WEB_DIR="./web" + +echo "Starting frontend build in $WEB_DIR..." +if [ -d "$WEB_DIR" ]; then + (cd "$WEB_DIR" && bun run build) + if [ $? -ne 0 ]; then + echo "Frontend build failed. Aborting process." + exit 1 + fi +else + echo "Error: Directory $WEB_DIR not found." + exit 1 +fi + +echo "Frontend build completed successfully." +echo "----------------------------------------" + +GOOS=$(go env GOOS) +GOARCH=$(go env GOARCH) + +output_name="$OUTPUT_DIR/${PROGRAM_NAME}_${GOOS}_${GOARCH}" +if [ "$GOOS" = "windows" ]; then + output_name+='.exe' +fi + +echo "Detected platform: $GOOS/$GOARCH" +echo "Building for $GOOS/$GOARCH..." + +env GOOS=$GOOS GOARCH=$GOARCH go build -o "$output_name" "$GO_FILE_PATH" + +if [ $? -eq 0 ]; then + echo "Compilation successful: $output_name" +else + echo "Compilation for $GOOS/$GOARCH failed." + exit 1 +fi diff --git a/super_build.sh b/scripts/build_super.sh similarity index 95% rename from super_build.sh rename to scripts/build_super.sh index ce721a8..c49de73 100644 --- a/super_build.sh +++ b/scripts/build_super.sh @@ -1,9 +1,8 @@ #!/bin/sh OUTPUT_DIR=./bin -GO_FILE_PATH="./main/main.go" +GO_FILE_PATH="./cmd/server/main.go" PROGRAM_NAME=RenewCMS - platforms=("windows/amd64" "windows/arm64" "linux/amd64" "linux/arm64" "darwin/amd64" "darwin/arm64") for platform in "${platforms[@]}" diff --git a/templ_static_files_validator.sh b/templ_static_files_validator.sh deleted file mode 100644 index 786c3c3..0000000 --- a/templ_static_files_validator.sh +++ /dev/null @@ -1,51 +0,0 @@ -#!/bin/bash - -find_html_files() { - local directory="$1" - find "$directory" -type f -name "*.html" -} - -extract_referenced_files() { - local html_file="$1" - grep -oP '(?<=src="/static/|href="/static/)[^"]+' "$html_file" -} - -is_link() { - local path="$1" - [[ "$path" =~ ^https?://.* ]] -} - -validate_referenced_files() { - local source_dir="$1" - local target_dir="$2" - - local html_files - html_files=$(find_html_files "$source_dir") - - for html_file in $html_files; do - local referenced_files - referenced_files=$(extract_referenced_files "$html_file") - - for referenced_file in $referenced_files; do - referenced_file="${referenced_file#/}" - - if is_link "$referenced_file"; then - continue - fi - - local full_referenced_path="$target_dir$referenced_file" - - if [ ! -e "$full_referenced_path" ]; then - echo "Error: $full_referenced_path not found (referenced in $html_file)" - exit 1 - fi - done - done -} - -source_dir="./adapters/secondary/gateways/web/templates/" -target_dir="./api/static/" - -validate_referenced_files "$source_dir" "$target_dir" - -echo "All files referenced in HTML files are present in $target_dir" diff --git a/web/src/components/NavComponent.vue b/web/src/components/NavComponent.vue index cf368d4..4d2ebda 100644 --- a/web/src/components/NavComponent.vue +++ b/web/src/components/NavComponent.vue @@ -1,6 +1,7 @@ @@ -9,6 +10,10 @@ const visible = ref(false)