package dev import ( "context" "crypto/rand" "database/sql" "encoding/hex" "fmt" "net/http" "strings" "time" "github.com/example/sndit/backend/internal/auth" "github.com/example/sndit/backend/internal/storage" "github.com/gin-gonic/gin" ) type Config struct { DBDriver string StorageEndpoint string StorageBucket string StorageUseSSL bool CORSOrigins string CookieSecure bool } type Handler struct { db *sql.DB storage storage.Storage auth *auth.Service config Config } func NewHandler(db *sql.DB, objectStorage storage.Storage, authService *auth.Service, config Config) *Handler { return &Handler{db: db, storage: objectStorage, auth: authService, config: config} } func (h *Handler) RegisterRoutes(router gin.IRouter, require gin.HandlerFunc) { router.GET("/api/dev/diagnostics", require, h.Diagnostics) router.POST("/api/dev/storage-check", require, h.StorageCheck) } // Diagnostics godoc // @Summary Inspect local development dependencies // @Tags development // @Produce json // @Security studioSession // @Success 200 {object} map[string]interface{} // @Failure 401 {object} map[string]string // @Router /api/dev/diagnostics [get] func (h *Handler) Diagnostics(c *gin.Context) { user, _ := auth.UserFromContext(c) databaseError := "" databaseContext, cancel := context.WithTimeout(c.Request.Context(), 2*time.Second) if err := h.db.PingContext(databaseContext); err != nil { databaseError = err.Error() } cancel() storageError := "" storageContext, storageCancel := context.WithTimeout(c.Request.Context(), 3*time.Second) if err := h.storage.EnsureBucket(storageContext); err != nil { storageError = err.Error() } storageCancel() writeJSON(c, http.StatusOK, map[string]any{ "environment": "development", "now": time.Now().UTC().Format(time.RFC3339), "user": map[string]string{ "id": user.ID.String(), "email": user.Email, "name": user.Name, }, "database": map[string]any{ "driver": h.config.DBDriver, "connected": databaseError == "", "error": databaseError, }, "storage": map[string]any{ "provider": "MinIO / S3-compatible", "endpoint": h.config.StorageEndpoint, "bucket": h.config.StorageBucket, "secure": h.config.StorageUseSSL, "reachable": storageError == "", "error": storageError, }, "http": map[string]any{ "corsOrigins": splitOrigins(h.config.CORSOrigins), "cookieSecure": h.config.CookieSecure, }, }) } // StorageCheck godoc // @Summary Check MinIO storage read/write access // @Tags development // @Produce json // @Security studioSession // @Success 200 {object} map[string]interface{} // @Failure 401 {object} map[string]string // @Failure 500 {object} map[string]interface{} // @Failure 503 {object} map[string]interface{} // @Router /api/dev/storage-check [post] func (h *Handler) StorageCheck(c *gin.Context) { if err := h.storage.EnsureBucket(c.Request.Context()); err != nil { writeJSON(c, http.StatusServiceUnavailable, map[string]any{"ok": false, "step": "bucket", "error": err.Error()}) return } var randomBytes [12]byte if _, err := rand.Read(randomBytes[:]); err != nil { writeJSON(c, http.StatusInternalServerError, map[string]any{"ok": false, "step": "random", "error": err.Error()}) return } key := fmt.Sprintf("dev/diagnostics/%s.txt", hex.EncodeToString(randomBytes[:])) contents := "studio storage check " + time.Now().UTC().Format(time.RFC3339Nano) if err := h.storage.Put(c.Request.Context(), key, strings.NewReader(contents), int64(len(contents)), "text/plain"); err != nil { writeJSON(c, http.StatusServiceUnavailable, map[string]any{"ok": false, "step": "put", "error": err.Error()}) return } info, statErr := h.storage.Stat(c.Request.Context(), key) deleteErr := h.storage.Delete(c.Request.Context(), key) if statErr != nil { writeJSON(c, http.StatusServiceUnavailable, map[string]any{"ok": false, "step": "stat", "error": statErr.Error()}) return } if deleteErr != nil { writeJSON(c, http.StatusServiceUnavailable, map[string]any{"ok": false, "step": "delete", "error": deleteErr.Error()}) return } writeJSON(c, http.StatusOK, map[string]any{"ok": true, "bytes": info.Size, "contentType": info.ContentType}) } func splitOrigins(value string) []string { result := make([]string, 0) for _, origin := range strings.Split(value, ",") { if trimmed := strings.TrimSpace(origin); trimmed != "" { result = append(result, trimmed) } } return result } func writeJSON(c *gin.Context, status int, value any) { c.JSON(status, value) }