feat: api serve frontend, build workflow, router readyness

This commit is contained in:
Floriansylvain
2025-12-24 01:05:58 +01:00
parent aee595dee9
commit 4a6209a8db
13 changed files with 174 additions and 74 deletions
+1
View File
@@ -7,6 +7,7 @@
# Build
bin
dist/
# Environment
.env
+5 -3
View File
@@ -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: "/",
})
+79
View File
@@ -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)
}
+1 -10
View File
@@ -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)
})
}
+22 -2
View File
@@ -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
}
+6
View File
@@ -0,0 +1,6 @@
package assets
import "embed"
//go:embed all:dist
var DistEmbed embed.FS
+41
View File
@@ -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
+1 -2
View File
@@ -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[@]}"
-51
View File
@@ -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"
+5
View File
@@ -1,6 +1,7 @@
<script setup lang="ts">
import { ref } from 'vue'
import { Button, Drawer } from 'primevue'
import { RouterLink } from 'vue-router'
const visible = ref(false)
</script>
@@ -9,6 +10,10 @@ const visible = ref(false)
<header>
<Drawer v-model:visible="visible">
<template #container="{ closeCallback }">
<ul @click="closeCallback">
<li><RouterLink to="/">Home</RouterLink></li>
<li><RouterLink to="/article">Article</RouterLink></li>
</ul>
<div class="button-position">
<Button
type="button"
+2 -1
View File
@@ -3,7 +3,8 @@ import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
{ path: '/', component: () => import('@/views/HomeView.vue')}
{ path: '/', component: () => import('@/views/HomeView.vue') },
{ path: '/article', component: () => import('@/views/ArticleView.vue') },
],
})
+7
View File
@@ -0,0 +1,7 @@
<script setup lang="ts"></script>
<template>
<p>Article</p>
</template>
<style scoped></style>
+4 -5
View File
@@ -7,11 +7,10 @@ import tailwindcss from '@tailwindcss/vite'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
vueDevTools(),
tailwindcss()
],
build: {
outDir: '../internal/infrastructure/assets/dist/',
},
plugins: [vue(), vueDevTools(), tailwindcss()],
resolve: {
alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)),