1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
|
package api
import (
"io/fs"
"log"
"net/http"
"strings"
"yaum/internal/static"
)
func NewRouter(h *Handler, stream *StreamHandler, auth *AuthHandler) http.Handler {
mux := http.NewServeMux()
mux.HandleFunc("POST /api/auth/login", auth.Login)
mux.HandleFunc("GET /api/auth/verify", auth.Verify)
mux.HandleFunc("GET /api/services", h.ListServices)
mux.HandleFunc("POST /api/services", h.CreateService)
mux.HandleFunc("PUT /api/services/{id}", h.UpdateService)
mux.HandleFunc("PATCH /api/services/{id}/toggle", h.ToggleService)
mux.HandleFunc("POST /api/services/{id}/test", h.TestService)
mux.HandleFunc("DELETE /api/services/{id}", h.DeleteService)
mux.HandleFunc("GET /api/services/{id}/history", h.GetHistory)
mux.HandleFunc("GET /api/services/{id}/stats", h.GetStats)
mux.HandleFunc("GET /api/services/{id}/ssl", h.GetSSLInfo)
mux.HandleFunc("GET /api/services/{id}/badge.svg", h.BadgeSVG)
mux.HandleFunc("GET /api/active-maintenance", h.GetActiveMaintenance)
mux.HandleFunc("GET /api/maintenances", h.ListMaintenances)
mux.HandleFunc("POST /api/maintenances", h.CreateMaintenance)
mux.HandleFunc("PUT /api/maintenances/{id}", h.UpdateMaintenance)
mux.HandleFunc("DELETE /api/maintenances/{id}", h.DeleteMaintenance)
mux.HandleFunc("GET /api/admin/server-stats", h.ServerStats)
mux.Handle("GET /api/stream", stream)
// Static file serving — register only if build FS is available
if buildFS := static.BuildFS(); buildFS != nil {
staticHandler := spaFileServer(buildFS)
mux.HandleFunc("/", staticHandler.ServeHTTP)
} else {
log.Println("[router] nenhum build frontend encontrado — servindo apenas API")
// Still register root to give a friendly message
mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
if strings.HasPrefix(r.URL.Path, "/api") {
http.NotFound(w, r)
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte(`YAUM API — frontend build not available.
Start the frontend dev server (npm run dev) or set YAUM_STATIC_DIR.`))
})
}
return corsMiddleware(auth.Middleware(mux))
}
// spaFileServer returns an http.Handler that serves static files and falls
// back to index.html (or 200.html) for SPA routes like /admin, /status/123.
// Only GET/HEAD requests are handled; other methods return 405.
func spaFileServer(fsys fs.FS) http.Handler {
fileServer := http.FileServer(http.FS(fsys))
exists := func(name string) bool {
f, err := fsys.Open(name)
if err != nil {
return false
}
f.Close()
return true
}
// Determine which SPA fallback file exists
fallbackFile := "200.html"
fallbackBytes, fallbackErr := fs.ReadFile(fsys, fallbackFile)
if fallbackErr != nil {
fallbackFile = "index.html"
fallbackBytes, fallbackErr = fs.ReadFile(fsys, fallbackFile)
}
if fallbackErr != nil {
log.Printf("[router] nenhum fallback SPA (200.html/index.html) encontrado")
}
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Only serve GET/HEAD for static files
if r.Method != http.MethodGet && r.Method != http.MethodHead {
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
return
}
// API paths should already be handled by the mux, but just in case
if strings.HasPrefix(r.URL.Path, "/api") {
http.NotFound(w, r)
return
}
p := strings.TrimPrefix(r.URL.Path, "/")
// Root path or non-existent file → serve SPA fallback
if p == "" || !exists(p) {
if fallbackErr == nil {
w.Header().Set("Content-Type", "text/html; charset=utf-8")
w.WriteHeader(http.StatusOK)
w.Write(fallbackBytes)
return
}
http.NotFound(w, r)
return
}
fileServer.ServeHTTP(w, r)
})
}
func corsMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
if origin != "" && isAllowedOrigin(origin) {
w.Header().Set("Access-Control-Allow-Origin", origin)
w.Header().Set("Vary", "Origin")
} else {
w.Header().Set("Access-Control-Allow-Origin", "*")
}
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, PATCH, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
w.Header().Set("Access-Control-Max-Age", "86400")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
func isAllowedOrigin(origin string) bool {
allowed := []string{
"http://localhost:5173",
"http://localhost:4173",
"http://127.0.0.1:5173",
"http://127.0.0.1:4173",
}
for _, a := range allowed {
if strings.EqualFold(origin, a) {
return true
}
}
if strings.HasPrefix(origin, "http://localhost:") {
return true
}
return false
}
|