06cbad708d
- Add Database struct in internal/db/database.go with Pool, Ctx, and RunMigrations() - Update server.go to use Database struct with NewServerInstance() - Add backend.go with InitBackend(), BackendRepo(), BackendCtx(), BackendPool() - Update music.go and sync.go to use BackendRepo() and BackendCtx() instead of db.Dbpool/db.Ctx - Update token_handler.go to accept pool parameter - Update routes.go to use s.db.Pool for middleware - Update cmd/main.go to use NewServerInstance() and HTTPServer() - Update test_helpers.go to initialize backend with test database - Update test files to use backend.BackendPool() and backend.BackendCtx() Benefits: - Easier to mock database for unit tests - Follows Go best practices (dependency injection) - Better architecture with explicit dependencies - RunMigrations() replaces old Migrate_db() function Note: Global db.Dbpool and db.Ctx still exist in dbHelper.go for backward compatibility with test_helpers.go, but production code no longer uses them. Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
176 lines
5.5 KiB
Go
176 lines
5.5 KiB
Go
package server
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/base64"
|
|
"net/http"
|
|
"strings"
|
|
"time"
|
|
|
|
"music-server/internal/db/repository"
|
|
"music-server/internal/logging"
|
|
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
"github.com/jackc/pgx/v5/pgtype"
|
|
"github.com/labstack/echo/v5"
|
|
"go.uber.org/zap"
|
|
)
|
|
|
|
// TokenRequest represents a request to generate a new token
|
|
type TokenRequest struct {
|
|
ClientType string `json:"client_type"` // Optional: "web", "mobile", "api"
|
|
}
|
|
|
|
// TokenResponse represents the response with a new token
|
|
type TokenResponse struct {
|
|
Token string `json:"token"`
|
|
ExpiresAt time.Time `json:"expires_at"`
|
|
ClientType string `json:"client_type"`
|
|
}
|
|
|
|
// TokenHandler contains the database pool for token operations
|
|
type TokenHandler struct {
|
|
pool *pgxpool.Pool
|
|
}
|
|
|
|
// NewTokenHandler creates a new token handler with database pool
|
|
func NewTokenHandler(pool *pgxpool.Pool) *TokenHandler {
|
|
return &TokenHandler{
|
|
pool: pool,
|
|
}
|
|
}
|
|
|
|
// generateToken creates a new cryptographically secure token
|
|
func (h *TokenHandler) generateToken() (string, error) {
|
|
b := make([]byte, 32)
|
|
if _, err := rand.Read(b); err != nil {
|
|
return "", err
|
|
}
|
|
return base64.URLEncoding.EncodeToString(b), nil
|
|
}
|
|
|
|
// CreateTokenHandler creates a new session token
|
|
// POST /api/v1/token
|
|
//
|
|
// @Summary Create session token
|
|
// @Description Returns a new session token for API access
|
|
// @Tags auth
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param request body TokenRequest true "Client type"
|
|
// @Success 200 {object} TokenResponse
|
|
// @Failure 400 {object} map[string]string
|
|
// @Failure 500 {object} map[string]string
|
|
// @Router /api/v1/token [post]
|
|
func (h *TokenHandler) CreateTokenHandler(c *echo.Context) error {
|
|
var req TokenRequest
|
|
if err := c.Bind(&req); err != nil {
|
|
return c.JSON(http.StatusBadRequest, map[string]string{"error": "Invalid request body"})
|
|
}
|
|
|
|
if req.ClientType == "" {
|
|
req.ClientType = "web"
|
|
}
|
|
|
|
// Generate token
|
|
token, err := h.generateToken()
|
|
if err != nil {
|
|
logging.GetLogger().Error("Failed to generate token", zap.String("error", err.Error()))
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to generate token"})
|
|
}
|
|
|
|
// Set expiration (24 hours from now)
|
|
expiresAt := time.Now().Add(24 * time.Hour)
|
|
clientType := req.ClientType
|
|
|
|
// Store in database using sqlc-generated repository
|
|
queries := repository.New(h.pool)
|
|
session, err := queries.CreateSession(c.Request().Context(), repository.CreateSessionParams{
|
|
Token: token,
|
|
IpAddress: c.RealIP(),
|
|
UserAgent: c.Request().UserAgent(),
|
|
ClientType: &clientType,
|
|
ExpiresAt: pgtype.Timestamptz{Time: expiresAt, Valid: true},
|
|
})
|
|
if err != nil {
|
|
logging.GetLogger().Error("Failed to create session", zap.String("error", err.Error()))
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to create session"})
|
|
}
|
|
|
|
response := TokenResponse{
|
|
Token: session.Token,
|
|
ExpiresAt: session.ExpiresAt.Time,
|
|
ClientType: *session.ClientType,
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, response)
|
|
}
|
|
|
|
// DeleteTokenHandler invalidates a session token
|
|
// DELETE /api/v1/token
|
|
//
|
|
// @Summary Invalidate session token
|
|
// @Description Deletes the current session token
|
|
// @Tags auth
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param Authorization header string true "Bearer token"
|
|
// @Success 200 {object} map[string]string
|
|
// @Failure 401 {object} map[string]string
|
|
// @Failure 500 {object} map[string]string
|
|
// @Router /api/v1/token [delete]
|
|
func (h *TokenHandler) DeleteTokenHandler(c *echo.Context) error {
|
|
authHeader := c.Request().Header.Get("Authorization")
|
|
if authHeader == "" {
|
|
return c.JSON(http.StatusUnauthorized, map[string]string{"error": "Authorization header required"})
|
|
}
|
|
|
|
parts := strings.Split(authHeader, " ")
|
|
if len(parts) != 2 || parts[0] != "Bearer" {
|
|
return c.JSON(http.StatusUnauthorized, map[string]string{"error": "Invalid authorization format"})
|
|
}
|
|
|
|
token := parts[1]
|
|
|
|
// Delete session using sqlc-generated repository
|
|
queries := repository.New(h.pool)
|
|
err := queries.DeleteSession(c.Request().Context(), token)
|
|
if err != nil {
|
|
logging.GetLogger().Error("Failed to delete session", zap.String("error", err.Error()))
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to invalidate token"})
|
|
}
|
|
|
|
return c.JSON(http.StatusOK, map[string]string{"status": "token invalidated"})
|
|
}
|
|
|
|
// CleanupExpiredSessionsHandler removes all expired sessions
|
|
// POST /api/v1/token/cleanup
|
|
//
|
|
// @Summary Cleanup expired sessions
|
|
// @Description Removes all expired session tokens from the database
|
|
// @Tags auth
|
|
// @Accept json
|
|
// @Produce json
|
|
// @Param Authorization header string true "Bearer token"
|
|
// @Success 200 {object} map[string]interface{}
|
|
// @Failure 401 {object} map[string]string
|
|
// @Failure 500 {object} map[string]string
|
|
// @Router /api/v1/token/cleanup [post]
|
|
func (h *TokenHandler) CleanupExpiredSessionsHandler(c *echo.Context) error {
|
|
// Verify token is valid first (using existing middleware)
|
|
// The middleware will have already validated the token
|
|
|
|
queries := repository.New(h.pool)
|
|
err := queries.DeleteExpiredSessions(c.Request().Context())
|
|
if err != nil {
|
|
logging.GetLogger().Error("Failed to cleanup sessions", zap.String("error", err.Error()))
|
|
return c.JSON(http.StatusInternalServerError, map[string]string{"error": "Failed to cleanup sessions"})
|
|
}
|
|
|
|
// Get count of deleted rows (DeleteExpiredSessions doesn't return count in the generated code)
|
|
// So we just return success
|
|
return c.JSON(http.StatusOK, map[string]interface{}{
|
|
"status": "cleanup complete",
|
|
})
|
|
}
|