2025-11-08 16:17:55 +01:00

252 lines
6.4 KiB
Go

//go:build !prod
// +build !prod
package main
import (
"context"
"crud/datastar"
"crud/sqlite"
"crypto/rand"
"embed"
"encoding/hex"
"fmt"
"html/template"
"io/fs"
"log"
"log/slog"
"net/http"
"os"
"os/signal"
"path/filepath"
"strings"
"sync"
"time"
)
const (
exitCodeErr = 1
exitCodeInterrupt = 2
)
//go:embed frontend
var frontend embed.FS
//go:embed templates
var templates embed.FS
var templ = template.Must(template.ParseFS(templates, "templates/*.html"))
// main is the entry point of the application.
// Its task is to check wether all execution conditions are fullfilled.
// Collecting information from the environment: flags, environment vars, configs.
// Calling the run() function.
func main() {
fmt.Println("Developement mode")
ctx := context.Background()
// logging
logFileName := "./crud.log"
logFile, err := os.OpenFile(logFileName, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0666)
if err != nil {
log.Printf("error opening file: %v", err)
os.Exit(exitCodeErr)
}
defer logFile.Close()
log.SetOutput(logFile)
// database
dbName := "./user.db"
db := sqlite.New(dbName)
if err != nil {
log.Printf("Failed to open %s database: %v", dbName, err)
os.Exit(exitCodeErr)
}
// run the app
if err := run(db, ctx); err != nil {
fmt.Fprintf(os.Stderr, "%s\n", err)
os.Exit(exitCodeErr)
}
}
// Setting up all dependencies
// Creating the server (a central http handler)
func run(db *sqlite.Database, ctx context.Context) error {
ctx, cancel := signal.NotifyContext(ctx, os.Interrupt)
defer cancel()
err := db.Open(ctx)
if err != nil {
log.Printf("Failed to open %s database: %v", db.Name(), err)
os.Exit(exitCodeErr)
}
defer db.Close()
server := NewServer(db)
httpServer := &http.Server{
Addr: ":8080",
Handler: server,
}
go func() {
log.Printf("listening on %s\n", httpServer.Addr)
if err := httpServer.ListenAndServe(); err != nil && err != http.ErrServerClosed {
fmt.Fprintf(os.Stderr, "error listening and serving: %s\n", err)
}
}()
var wg sync.WaitGroup
wg.Add(1)
go func() {
defer wg.Done()
<-ctx.Done()
// make a new context for the Shutdown (thanks Alessandro Rosetti)
// shutdownCtx := context.Background()
shutdownCtx, cancel := context.WithTimeout(ctx, 10*time.Second)
defer cancel()
if err := httpServer.Shutdown(shutdownCtx); err != nil {
fmt.Fprintf(os.Stderr, "error shutting down http server: %s\n", err)
}
log.Printf("shut down http server on %s\n", httpServer.Addr)
}()
wg.Wait()
return nil
}
// The NewServer constructor is responsible for all the top-level HTTP stuff that applies to all endpoints, like CORS, auth middleware, and logging:
func NewServer(db *sqlite.Database) http.Handler {
mux := http.NewServeMux()
static, err := fs.Sub(frontend, "frontend")
if err != nil {
log.Fatal(err)
}
// Print the embedded filesystem tree
fmt.Println("Embedded frontend filesystem tree:")
err = printFSTree(static, ".", 0)
if err != nil {
log.Printf("Error printing filesystem tree: %v\n", err)
}
fmt.Println("--- End of tree ---")
addRoutes(
mux,
db,
static,
// templates,
)
mux.HandleFunc("GET /stream", func(w http.ResponseWriter, r *http.Request) {
ticker := time.NewTicker(1000 * time.Millisecond)
// original: defer ticker.Stop()
defer func() {
fmt.Println("defer executed")
ticker.Stop()
}()
sse := datastar.NewSSE(w, r)
for {
select {
case <-r.Context().Done():
slog.Debug("Client connection closed")
return
case <-ticker.C:
bytes := make([]byte, 3)
if _, err := rand.Read(bytes); err != nil {
slog.Error("Error generating random bytes: ", slog.String("error", err.Error()))
return
}
hexString := hex.EncodeToString(bytes)
frag := fmt.Sprintf(`<span id="feed" style="color:#%s;border:1px solid #%s;border-radius:0.25rem;padding:1rem;">%s</span>`, hexString, hexString, hexString)
sse.MergeFragments(frag)
}
}
})
var handler http.Handler = mux
// handler = authMiddleware(handler)
handler = headerMiddleware(handler)
return handler
}
// printFSTree prints a tree-like structure of the given filesystem.
func printFSTree(efs fs.FS, root string, indentLevel int) error {
return fs.WalkDir(efs, root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
// Skip the root directory itself for cleaner output if it's "."
if path == "." && root == "." {
return nil
}
indent := strings.Repeat("│ ", indentLevel)
connector := "├── "
// For the last item in a directory, use a different connector.
// This requires knowing if it's the last item, which fs.WalkDir doesn't directly provide.
// For simplicity, we'll use the same connector for all items.
// A more sophisticated approach would involve reading directory entries first.
fmt.Printf("%s%s%s\n", indent, connector, filepath.Base(path))
if d.IsDir() && path != root { // Avoid infinite recursion for the root itself if not handled carefully
// The WalkDir function handles recursion, so we don't need to call printFSTree recursively here.
// We adjust indentLevel based on path depth for visual representation.
// This simple indentation based on WalkDir's path might not be perfect for deep structures
// but gives a good overview.
}
return nil
})
}
// authMiddleware is a simple authentication middleware
// func authMiddleware(next http.Handler) http.Handler {
// return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// user, pass, ok := r.BasicAuth()
// if !ok || !validateUser(user, pass) {
// w.Header().Set("WWW-Authenticate", `Basic realm="Please enter your credentials"`)
// http.Error(w, "Unauthorized", http.StatusUnauthorized)
// return
// }
// next.ServeHTTP(w, r)
// })
// }
// authMiddleware is a simple authentication middleware
func headerMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
fmt.Println("Request URL:", r.URL.String())
fmt.Println("Request Headers:")
for key, values := range r.Header {
for _, value := range values {
if key == "Referer" || strings.HasPrefix(key, "Hx") {
fmt.Printf("%s: %s\n", key, value)
}
}
}
fmt.Println()
next.ServeHTTP(w, r)
})
}
// validateUser validates the user credentials
func validateUser(username, password string) bool {
// In a real application, these credentials should be stored securely.
return strings.EqualFold(username, "admin") && password == "password"
}