feat: 10 quick wins — account deletion, health check, debug cleanup, UX polish, graceful shutdown

- backend/handlers/user/account.go: Wire DELETE /api/user/account to call
  anonymize_user() for registered users and delete_guest_user() for guests,
  with CardDAV contact cleanup
- backend/handlers/user/profile_test.go: Add TestAccount_DeleteGuest and
  enhance TestAccount_Delete to verify anonymization results
- backend/main.go: Add GET /api/health endpoint with DB ping and S3 status
  check; add HSTS and Referrer-Policy security headers; replace
  http.ListenAndServe with http.Server + graceful SIGTERM/SIGINT shutdown
- frontend/routes/+layout.svelte: Replace alert() with toast notifications
  for email verification flow
- frontend/routes/login/+page.svelte: Replace alert() with toast.info for
  social login prototype buttons
- frontend/booking/BookingFlow.svelte: Remove 2 console.log debug calls;
  add cancellation policy note in Step 3; add timezone policy comment
- frontend/ImageUpload.svelte: Comment out debug console.log
- init-scripts/init-script.sql: Add delete_guest_user() SQL function
- docs: Update README.md and Obsidian notes to reflect completed items
This commit is contained in:
2026-05-01 11:33:27 +01:00
parent 7b396b7a9d
commit bff86a6660
11 changed files with 673 additions and 435 deletions
+68 -1
View File
@@ -1,13 +1,17 @@
package main
import (
"context"
"crussell/auth"
"crussell/internal/dav"
"crussell/internal/s3"
"encoding/json"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/go-chi/chi/v5"
@@ -56,6 +60,42 @@ func initS3() {
}
}
func healthCheckHandler(w http.ResponseWriter, r *http.Request) {
status := "ok"
services := map[string]string{
"backend": "ok",
"database": "ok",
"s3_storage": "ok",
"square_payments": "not_implemented",
"frontend": "unknown",
}
if db.DB != nil {
if err := db.DB.Ping(r.Context()); err != nil {
services["database"] = "error"
status = "degraded"
}
} else {
services["database"] = "error"
status = "degraded"
}
if s3.Client == nil {
services["s3_storage"] = "not_configured"
}
w.Header().Set("Content-Type", "application/json")
if status == "degraded" {
w.WriteHeader(http.StatusServiceUnavailable)
} else {
w.WriteHeader(http.StatusOK)
}
json.NewEncoder(w).Encode(map[string]interface{}{
"status": status,
"services": services,
})
}
func main() {
initDB()
initDav()
@@ -74,6 +114,10 @@ func main() {
w.Header().Set("X-Content-Type-Options", "nosniff")
w.Header().Set("X-Frame-Options", "DENY")
w.Header().Set("X-XSS-Protection", "1; mode=block")
// TODO: Enable HSTS in production
w.Header().Set("Strict-Transport-Security", "max-age=31536000; includeSubDomains")
// TODO: Enable Referrer-Policy in production
w.Header().Set("Referrer-Policy", "strict-origin-when-cross-origin")
next.ServeHTTP(w, r)
})
})
@@ -98,6 +142,9 @@ func main() {
r.Post("/verify/generate", authHandlers.GenerateVerificationCodeHandler)
r.Post("/verify/check", authHandlers.VerifyCodeHandler)
// Health check
r.Get("/health", healthCheckHandler)
// Public contact info
r.Get("/contact", user.GetContactInfoHandler)
@@ -225,6 +272,26 @@ func main() {
})
})
srv := &http.Server{
Addr: ":8080",
Handler: r,
}
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGTERM, syscall.SIGINT)
go func() {
<-quit
log.Println("Shutting down server...")
ctx, cancel := context.WithTimeout(context.Background(), 15*time.Second)
defer cancel()
if err := srv.Shutdown(ctx); err != nil {
log.Printf("Server forced to shutdown: %v", err)
}
}()
fmt.Println("Server is listening on :8080")
http.ListenAndServe(":8080", r)
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("Server failed to start: %v", err)
}
log.Println("Server exited")
}