Files
EvolioHealth/server/cmd/evoliohealth/main.go
T
tony b96edf0406
Android development APK / apk (push) Has been cancelled
Checks / go (push) Failing after 6s
Container images / publish (., deploy/pocketbase/Dockerfile, evoliohealth-pocketbase) (push) Has been cancelled
Container images / publish (., server/Dockerfile, evoliohealth-server) (push) Has been cancelled
Checks / compose (push) Has been cancelled
Checks / flutter (push) Has been cancelled
test: first build
2026-07-21 21:30:06 +02:00

122 lines
3.3 KiB
Go

package main
import (
"context"
"embed"
"errors"
"fmt"
"io/fs"
"log/slog"
"net"
"net/http"
"os"
"os/signal"
"strings"
"syscall"
"time"
)
// webAssets contains the Flutter Web release copied by the container build.
//go:embed webdist
var webAssets embed.FS
type application struct {
backendAddress string
logger *slog.Logger
}
func main() {
logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
app := application{
backendAddress: envOrDefault("POCKETBASE_URL", "http://pocketbase:8090"),
logger: logger,
}
assets, err := fs.Sub(webAssets, "webdist")
if err != nil {
logger.Error("web assets are unavailable")
os.Exit(1)
}
mux := http.NewServeMux()
mux.HandleFunc("GET /health/live", app.live)
mux.HandleFunc("GET /health/ready", app.ready)
mux.Handle("/", http.FileServer(http.FS(assets)))
server := &http.Server{
Addr: ":8080",
Handler: securityHeaders(mux),
ReadHeaderTimeout: 5 * time.Second,
ReadTimeout: 15 * time.Second,
WriteTimeout: 30 * time.Second,
IdleTimeout: 60 * time.Second,
MaxHeaderBytes: 1 << 20,
}
stop, cancel := signal.NotifyContext(context.Background(), syscall.SIGINT, syscall.SIGTERM)
defer cancel()
go func() {
logger.Info("server started", "address", server.Addr)
if err := server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
logger.Error("server stopped unexpectedly", "error", err)
cancel()
}
}()
<-stop.Done()
shutdownContext, shutdownCancel := context.WithTimeout(context.Background(), 15*time.Second)
defer shutdownCancel()
if err := server.Shutdown(shutdownContext); err != nil {
logger.Error("graceful shutdown failed", "error", err)
os.Exit(1)
}
logger.Info("server stopped")
}
func (app application) live(response http.ResponseWriter, _ *http.Request) {
response.Header().Set("Content-Type", "application/json")
response.WriteHeader(http.StatusOK)
_, _ = response.Write([]byte(`{"status":"ok"}`))
}
func (app application) ready(response http.ResponseWriter, request *http.Request) {
address := strings.TrimPrefix(strings.TrimPrefix(app.backendAddress, "http://"), "https://")
if !strings.Contains(address, ":") {
address += ":8090"
}
connection, err := (&net.Dialer{Timeout: 2 * time.Second}).DialContext(request.Context(), "tcp", address)
if err != nil {
http.Error(response, `{"status":"unavailable"}`, http.StatusServiceUnavailable)
return
}
_ = connection.Close()
response.Header().Set("Content-Type", "application/json")
response.WriteHeader(http.StatusOK)
_, _ = response.Write([]byte(`{"status":"ready"}`))
}
func securityHeaders(next http.Handler) http.Handler {
return http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
response.Header().Set("Content-Security-Policy", "default-src 'self'; img-src 'self' data:; style-src 'self' 'unsafe-inline'; script-src 'self' 'unsafe-inline'")
response.Header().Set("Referrer-Policy", "no-referrer")
response.Header().Set("X-Content-Type-Options", "nosniff")
response.Header().Set("X-Frame-Options", "DENY")
next.ServeHTTP(response, request)
})
}
func envOrDefault(name, fallback string) string {
if value := os.Getenv(name); value != "" {
return value
}
return fallback
}
func (app application) String() string {
return fmt.Sprintf("application(%s)", app.backendAddress)
}