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)