Initial add backend stuff

This commit is contained in:
2026-07-08 15:45:16 -04:00
commit a7964f9410
89 changed files with 25924 additions and 0 deletions

57
httputil/cors.go Normal file
View File

@@ -0,0 +1,57 @@
package httputil
import (
"net/http"
"slices"
"strings"
"kjol/appenv"
)
// CorsConfig configures CorsMiddleware. AllowedDomains is consulted only outside
// development (in development every origin is allowed). BundleVersion, when set,
// is called to stamp the X-Bundle-Version header on /api/ responses; leave it nil
// to skip that header.
type CorsConfig struct {
AllowedDomains []string
BundleVersion func() string
}
// CorsMiddleware returns middleware that applies CORS headers to all requests.
// It is a constructor (not the middleware itself) so the app can inject its
// allowed domains and bundle-version source without this package importing app
// config or handlers.
func CorsMiddleware(cfg CorsConfig) func(http.Handler) http.Handler {
return func(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
origin := r.Header.Get("Origin")
if appenv.Environment != appenv.EnvTypeDevelopment {
if len(cfg.AllowedDomains) > 0 && slices.Contains(cfg.AllowedDomains, origin) {
w.Header().Set("Access-Control-Allow-Origin", origin)
}
} else {
w.Header().Set("Access-Control-Allow-Origin", "*")
}
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
w.Header().Set("Access-Control-Expose-Headers", "X-Bundle-Version")
w.Header().Set("Access-Control-Allow-Credentials", "true")
if cfg.BundleVersion != nil {
if version := cfg.BundleVersion(); version != "" && strings.HasPrefix(r.URL.Path, "/api/") {
w.Header().Set("X-Bundle-Version", version)
}
}
// Handle preflight OPTIONS request
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusNoContent)
return
}
next.ServeHTTP(w, r)
})
}
}

45
httputil/cors_test.go Normal file
View File

@@ -0,0 +1,45 @@
package httputil_test
import (
"net/http"
"net/http/httptest"
"testing"
"kjol/httputil"
)
func TestCorsMiddleware_SetsBundleVersionOnAPI(t *testing.T) {
const testVersion = "test-bundle-version"
mux := http.NewServeMux()
mux.HandleFunc("GET /api/ping", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
mux.HandleFunc("GET /health", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusOK)
})
handler := httputil.CorsMiddleware(httputil.CorsConfig{
BundleVersion: func() string { return testVersion },
})(mux)
t.Run("api route", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/api/ping", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if got := rec.Header().Get("X-Bundle-Version"); got != testVersion {
t.Fatalf("expected X-Bundle-Version %q, got %q", testVersion, got)
}
})
t.Run("non-api route", func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/health", nil)
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, req)
if got := rec.Header().Get("X-Bundle-Version"); got != "" {
t.Fatalf("expected no bundle header on non-API route, got %q", got)
}
})
}

5
httputil/doc.go Normal file
View File

@@ -0,0 +1,5 @@
// Package httputil holds small, dependency-free HTTP helpers shared across apps:
// CORS middleware, JSON response helpers, and user-agent parsing. Anything that
// needs the application's auth/session/permission model lives app-side (in
// internal/httpauth), not here, so this package never imports app code.
package httputil

20
httputil/respond.go Normal file
View File

@@ -0,0 +1,20 @@
package httputil
import (
"encoding/json"
"net/http"
)
func RespondJSON(w http.ResponseWriter, statusCode int, data any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(statusCode)
json.NewEncoder(w).Encode(data)
}
func RespondError(w http.ResponseWriter, statusCode int, message string) {
RespondJSON(w, statusCode, map[string]string{"error": message})
}
func RespondSuccess(w http.ResponseWriter) {
RespondJSON(w, http.StatusOK, map[string]bool{"success": true})
}

138
httputil/useragent.go Normal file
View File

@@ -0,0 +1,138 @@
package httputil
import (
"regexp"
"strings"
)
func ParseUserAgent(ua string) string {
if ua == "" {
return "Unknown"
}
browser := parseBrowser(ua)
os := parseOS(ua)
if browser == "" && os == "" {
return "Unknown"
}
if browser == "" {
return os
}
if os == "" {
return browser
}
return browser + " on " + os
}
func parseBrowser(ua string) string {
if match := regexp.MustCompile(`Edg(?:e|A|iOS)?/(\d+)`).FindStringSubmatch(ua); match != nil {
return "Edge " + match[1]
}
if match := regexp.MustCompile(`(?:OPR|Opera)[/ ](\d+)`).FindStringSubmatch(ua); match != nil {
return "Opera " + match[1]
}
if match := regexp.MustCompile(`SamsungBrowser/(\d+)`).FindStringSubmatch(ua); match != nil {
return "Samsung Browser " + match[1]
}
if strings.Contains(ua, "Chrome") && !strings.Contains(ua, "Chromium") {
if match := regexp.MustCompile(`Chrome/(\d+)`).FindStringSubmatch(ua); match != nil {
return "Chrome " + match[1]
}
}
if match := regexp.MustCompile(`Chromium/(\d+)`).FindStringSubmatch(ua); match != nil {
return "Chromium " + match[1]
}
if match := regexp.MustCompile(`Firefox/(\d+)`).FindStringSubmatch(ua); match != nil {
return "Firefox " + match[1]
}
if strings.Contains(ua, "Safari") && !strings.Contains(ua, "Chrome") {
if match := regexp.MustCompile(`Version/(\d+)`).FindStringSubmatch(ua); match != nil {
return "Safari " + match[1]
}
return "Safari"
}
if match := regexp.MustCompile(`MSIE (\d+)`).FindStringSubmatch(ua); match != nil {
return "Internet Explorer " + match[1]
}
if strings.Contains(ua, "Trident/") {
if match := regexp.MustCompile(`rv:(\d+)`).FindStringSubmatch(ua); match != nil {
return "Internet Explorer " + match[1]
}
return "Internet Explorer"
}
return ""
}
func parseOS(ua string) string {
if strings.Contains(ua, "iPhone") {
if match := regexp.MustCompile(`iPhone OS (\d+)[_\d]*`).FindStringSubmatch(ua); match != nil {
return "iOS " + match[1]
}
return "iOS"
}
if strings.Contains(ua, "iPad") {
if match := regexp.MustCompile(`CPU OS (\d+)[_\d]*`).FindStringSubmatch(ua); match != nil {
return "iPadOS " + match[1]
}
return "iPadOS"
}
if match := regexp.MustCompile(`Android (\d+)`).FindStringSubmatch(ua); match != nil {
return "Android " + match[1]
}
if strings.Contains(ua, "Windows") {
if strings.Contains(ua, "Windows NT 10.0") {
return "Windows 10/11"
}
if strings.Contains(ua, "Windows NT 6.3") {
return "Windows 8.1"
}
if strings.Contains(ua, "Windows NT 6.2") {
return "Windows 8"
}
if strings.Contains(ua, "Windows NT 6.1") {
return "Windows 7"
}
if strings.Contains(ua, "Windows NT 6.0") {
return "Windows Vista"
}
if strings.Contains(ua, "Windows NT 5.1") {
return "Windows XP"
}
return "Windows"
}
if strings.Contains(ua, "Mac OS X") || strings.Contains(ua, "Macintosh") {
if match := regexp.MustCompile(`Mac OS X (\d+)[_.](\d+)`).FindStringSubmatch(ua); match != nil {
return "macOS " + match[1] + "." + match[2]
}
return "macOS"
}
if strings.Contains(ua, "Ubuntu") {
return "Ubuntu"
}
if strings.Contains(ua, "Fedora") {
return "Fedora"
}
if strings.Contains(ua, "Linux") {
return "Linux"
}
if strings.Contains(ua, "CrOS") {
return "Chrome OS"
}
return ""
}