mirror of
https://github.com/Floriansylvain/RenewCMS.git
synced 2026-08-19 11:43:22 +02:00
feat: api serve frontend, build workflow, router readyness
This commit is contained in:
@@ -7,6 +7,7 @@
|
|||||||
|
|
||||||
# Build
|
# Build
|
||||||
bin
|
bin
|
||||||
|
dist/
|
||||||
|
|
||||||
# Environment
|
# Environment
|
||||||
.env
|
.env
|
||||||
|
|||||||
@@ -32,7 +32,9 @@ type LoginCredentials struct {
|
|||||||
Password string `json:"password" validate:"required,min=8"`
|
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) {
|
func removeJwtCookie(w http.ResponseWriter) {
|
||||||
http.SetCookie(w, &http.Cookie{
|
http.SetCookie(w, &http.Cookie{
|
||||||
@@ -40,7 +42,7 @@ func removeJwtCookie(w http.ResponseWriter) {
|
|||||||
Value: "",
|
Value: "",
|
||||||
Expires: time.Now(),
|
Expires: time.Now(),
|
||||||
MaxAge: -1,
|
MaxAge: -1,
|
||||||
Secure: shouldCookieBeSecure,
|
Secure: shouldCookieBeSecure(),
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
Path: "/",
|
Path: "/",
|
||||||
})
|
})
|
||||||
@@ -64,7 +66,7 @@ func (h *Handler) SetJwtCookie(w *http.ResponseWriter, userId uint32) error {
|
|||||||
Name: "jwt",
|
Name: "jwt",
|
||||||
Value: tokenString,
|
Value: tokenString,
|
||||||
Expires: time.Now().Add(24 * time.Hour),
|
Expires: time.Now().Add(24 * time.Hour),
|
||||||
Secure: shouldCookieBeSecure,
|
Secure: shouldCookieBeSecure(),
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
Path: "/",
|
Path: "/",
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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
@@ -2,18 +2,9 @@ package middleware
|
|||||||
|
|
||||||
import "net/http"
|
import "net/http"
|
||||||
|
|
||||||
const KeyContentType = "Content-Type"
|
|
||||||
|
|
||||||
func JsonContentTypeMiddleware(next http.Handler) http.Handler {
|
func JsonContentTypeMiddleware(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
w.Header().Set(KeyContentType, "application/json")
|
w.Header().Set("Content-Type", "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")
|
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
+22
-2
@@ -3,6 +3,7 @@ package api
|
|||||||
import (
|
import (
|
||||||
"RenewCMS/api/handlers/article"
|
"RenewCMS/api/handlers/article"
|
||||||
"RenewCMS/api/handlers/auth"
|
"RenewCMS/api/handlers/auth"
|
||||||
|
"RenewCMS/api/handlers/frontend"
|
||||||
"RenewCMS/api/middleware"
|
"RenewCMS/api/middleware"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -15,6 +16,12 @@ import (
|
|||||||
"github.com/go-chi/jwtauth/v5"
|
"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) {
|
func GetHelloWorld(w http.ResponseWriter, _ *http.Request) {
|
||||||
msg, _ := json.Marshal(map[string]string{"message": "Hello World"})
|
msg, _ := json.Marshal(map[string]string{"message": "Hello World"})
|
||||||
_, _ = w.Write(msg)
|
_, _ = w.Write(msg)
|
||||||
@@ -46,7 +53,8 @@ func NewAuthRouter(container *UseCases) http.Handler {
|
|||||||
func InitBackendRoutes(container *UseCases) *chi.Mux {
|
func InitBackendRoutes(container *UseCases) *chi.Mux {
|
||||||
r := chi.NewRouter()
|
r := chi.NewRouter()
|
||||||
|
|
||||||
r.Use(httplog.LoggerWithName("backend"))
|
enableLoggingIfDev(r, "backend")
|
||||||
|
|
||||||
r.Use(middleware.JsonContentTypeMiddleware)
|
r.Use(middleware.JsonContentTypeMiddleware)
|
||||||
r.Get("/", GetHelloWorld)
|
r.Get("/", GetHelloWorld)
|
||||||
r.Group(func(r chi.Router) {
|
r.Group(func(r chi.Router) {
|
||||||
@@ -59,19 +67,31 @@ func InitBackendRoutes(container *UseCases) *chi.Mux {
|
|||||||
return r
|
return r
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func InitFrontendRoutes() *chi.Mux {
|
||||||
|
r := chi.NewRouter()
|
||||||
|
|
||||||
|
enableLoggingIfDev(r, "frontend")
|
||||||
|
|
||||||
|
r.Mount("/", frontend.NewFrontendRouter())
|
||||||
|
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
func InitRoutes(container *UseCases) *chi.Mux {
|
func InitRoutes(container *UseCases) *chi.Mux {
|
||||||
backend := InitBackendRoutes(container)
|
backend := InitBackendRoutes(container)
|
||||||
|
frontend := InitFrontendRoutes()
|
||||||
|
|
||||||
apiRouter := chi.NewRouter()
|
apiRouter := chi.NewRouter()
|
||||||
apiRouter.Use(cors.Handler(cors.Options{
|
apiRouter.Use(cors.Handler(cors.Options{
|
||||||
AllowedOrigins: strings.Split(os.Getenv("CORS_ALLOWED_ORIGINS"), ";"),
|
AllowedOrigins: strings.Split(os.Getenv("CORS_ALLOWED_ORIGINS"), ";"),
|
||||||
AllowedMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
|
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"},
|
ExposedHeaders: []string{"Link"},
|
||||||
AllowCredentials: false,
|
AllowCredentials: false,
|
||||||
MaxAge: 300,
|
MaxAge: 300,
|
||||||
}))
|
}))
|
||||||
apiRouter.Mount("/v1", backend)
|
apiRouter.Mount("/v1", backend)
|
||||||
|
apiRouter.Mount("/", frontend)
|
||||||
|
|
||||||
return apiRouter
|
return apiRouter
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,6 @@
|
|||||||
|
package assets
|
||||||
|
|
||||||
|
import "embed"
|
||||||
|
|
||||||
|
//go:embed all:dist
|
||||||
|
var DistEmbed embed.FS
|
||||||
@@ -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,9 +1,8 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
OUTPUT_DIR=./bin
|
OUTPUT_DIR=./bin
|
||||||
GO_FILE_PATH="./main/main.go"
|
GO_FILE_PATH="./cmd/server/main.go"
|
||||||
PROGRAM_NAME=RenewCMS
|
PROGRAM_NAME=RenewCMS
|
||||||
|
|
||||||
platforms=("windows/amd64" "windows/arm64" "linux/amd64" "linux/arm64" "darwin/amd64" "darwin/arm64")
|
platforms=("windows/amd64" "windows/arm64" "linux/amd64" "linux/arm64" "darwin/amd64" "darwin/arm64")
|
||||||
|
|
||||||
for platform in "${platforms[@]}"
|
for platform in "${platforms[@]}"
|
||||||
@@ -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"
|
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { Button, Drawer } from 'primevue'
|
import { Button, Drawer } from 'primevue'
|
||||||
|
import { RouterLink } from 'vue-router'
|
||||||
|
|
||||||
const visible = ref(false)
|
const visible = ref(false)
|
||||||
</script>
|
</script>
|
||||||
@@ -9,6 +10,10 @@ const visible = ref(false)
|
|||||||
<header>
|
<header>
|
||||||
<Drawer v-model:visible="visible">
|
<Drawer v-model:visible="visible">
|
||||||
<template #container="{ closeCallback }">
|
<template #container="{ closeCallback }">
|
||||||
|
<ul @click="closeCallback">
|
||||||
|
<li><RouterLink to="/">Home</RouterLink></li>
|
||||||
|
<li><RouterLink to="/article">Article</RouterLink></li>
|
||||||
|
</ul>
|
||||||
<div class="button-position">
|
<div class="button-position">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -3,7 +3,8 @@ import { createRouter, createWebHistory } from 'vue-router'
|
|||||||
const router = createRouter({
|
const router = createRouter({
|
||||||
history: createWebHistory(import.meta.env.BASE_URL),
|
history: createWebHistory(import.meta.env.BASE_URL),
|
||||||
routes: [
|
routes: [
|
||||||
{ path: '/', component: () => import('@/views/HomeView.vue')}
|
{ path: '/', component: () => import('@/views/HomeView.vue') },
|
||||||
|
{ path: '/article', component: () => import('@/views/ArticleView.vue') },
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
<script setup lang="ts"></script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<p>Article</p>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped></style>
|
||||||
+4
-5
@@ -7,11 +7,10 @@ import tailwindcss from '@tailwindcss/vite'
|
|||||||
|
|
||||||
// https://vite.dev/config/
|
// https://vite.dev/config/
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
build: {
|
||||||
vue(),
|
outDir: '../internal/infrastructure/assets/dist/',
|
||||||
vueDevTools(),
|
},
|
||||||
tailwindcss()
|
plugins: [vue(), vueDevTools(), tailwindcss()],
|
||||||
],
|
|
||||||
resolve: {
|
resolve: {
|
||||||
alias: {
|
alias: {
|
||||||
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
'@': fileURLToPath(new URL('./src', import.meta.url)),
|
||||||
|
|||||||
Reference in New Issue
Block a user