89e884fae9
- Add migration 000004 for sessions table and performance indexes - Create session.sql queries for CRUD operations - Generate session repository code with sqlc - Create token auth middleware for Echo framework - Create token handler with create/delete/cleanup endpoints - Add /api/v1 router with token authentication infrastructure - Update dbHelper.go to use Up() instead of Migrate(2) - Update server.go to initialize token handler - Existing endpoints remain functional (to be deprecated) New endpoints: - POST /api/v1/token - Create new session token - DELETE /api/v1/token - Invalidate token - POST /api/v1/token/cleanup - Remove expired sessions Generated by Mistral Vibe. Co-Authored-By: Mistral Vibe <vibe@mistral.ai>
173 lines
5.4 KiB
Go
173 lines
5.4 KiB
Go
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",
|
|
})
|
|
}
|