package server import ( "crypto/rand" "encoding/base64" "net/http" "strings" "time" "music-server/internal/db" "music-server/internal/db/repository" "music-server/internal/logging" "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 *repository.Queries } // NewTokenHandler creates a new token handler with database pool func NewTokenHandler() *TokenHandler { return &TokenHandler{ pool: repository.New(db.Dbpool), } } // 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 session, err := h.pool.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 err := h.pool.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 err := h.pool.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", }) }