Files
2026-08-22 20:08:37 +02:00

149 lines
4.5 KiB
Go

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
APIURL 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),
"url": h.config.APIURL,
"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)
}