Feature/statistics api #26
@@ -121,18 +121,22 @@ func Migrate_db(host string, port string, user string, password string, dbname s
|
||||
// logging.GetLogger().Error("Force migration error", zap.String("error", err.Error()))
|
||||
//}
|
||||
|
||||
err = m.Migrate(2)
|
||||
//err = m.Up() // or m.Steps(2) if you want to explicitly set the number of migrations to run
|
||||
// Use Up() to apply all pending migrations instead of Migrate(2)
|
||||
err = m.Up()
|
||||
if err != nil {
|
||||
if err == migrate.ErrNoChange {
|
||||
logging.GetLogger().Info("Database already up to date")
|
||||
} else {
|
||||
logging.GetLogger().Error("Migration error", zap.String("error", err.Error()))
|
||||
}
|
||||
|
||||
} else {
|
||||
versionAfter, _, err := m.Version()
|
||||
if err != nil {
|
||||
logging.GetLogger().Error("Failed to get migration version after", zap.String("error", err.Error()))
|
||||
} else {
|
||||
logging.GetLogger().Info("Migrated to version", zap.Uint("version", versionAfter))
|
||||
}
|
||||
}
|
||||
|
||||
logging.GetLogger().Info("Migration version after", zap.Uint("version", versionAfter))
|
||||
|
||||
logging.GetLogger().Info("Migration completed")
|
||||
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
-- Drop indexes for sessions table
|
||||
DROP INDEX IF EXISTS idx_sessions_expires;
|
||||
DROP INDEX IF EXISTS idx_sessions_token;
|
||||
DROP INDEX IF EXISTS idx_sessions_ip;
|
||||
DROP INDEX IF EXISTS idx_sessions_created;
|
||||
|
||||
-- Drop sessions table
|
||||
DROP TABLE IF EXISTS sessions;
|
||||
|
||||
-- Drop performance indexes for song_list
|
||||
DROP INDEX IF EXISTS idx_song_list_match_date;
|
||||
DROP INDEX IF EXISTS idx_song_list_match_id;
|
||||
|
||||
-- Drop performance indexes for song
|
||||
DROP INDEX IF EXISTS idx_song_hash;
|
||||
DROP INDEX IF EXISTS idx_song_path;
|
||||
DROP INDEX IF EXISTS idx_song_game_id;
|
||||
DROP INDEX IF EXISTS idx_song_game_id_song_name;
|
||||
|
||||
-- Drop performance indexes for game
|
||||
DROP INDEX IF EXISTS idx_game_deleted;
|
||||
DROP INDEX IF EXISTS idx_game_hash;
|
||||
DROP INDEX IF EXISTS idx_game_path;
|
||||
DROP INDEX IF EXISTS idx_game_name;
|
||||
@@ -0,0 +1,39 @@
|
||||
-- ============================================
|
||||
-- PERFORMANCE INDEXES FOR EXISTING TABLES
|
||||
-- ============================================
|
||||
|
||||
-- Game table indexes
|
||||
CREATE INDEX IF NOT EXISTS idx_game_deleted ON game(deleted) WHERE deleted IS NULL;
|
||||
CREATE INDEX IF NOT EXISTS idx_game_hash ON game(hash);
|
||||
CREATE INDEX IF NOT EXISTS idx_game_path ON game(path);
|
||||
CREATE INDEX IF NOT EXISTS idx_game_name ON game(game_name);
|
||||
|
||||
-- Song table indexes
|
||||
CREATE INDEX IF NOT EXISTS idx_song_hash ON song(hash);
|
||||
CREATE INDEX IF NOT EXISTS idx_song_path ON song(path);
|
||||
CREATE INDEX IF NOT EXISTS idx_song_game_id ON song(game_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_song_game_id_song_name ON song(game_id, song_name);
|
||||
|
||||
-- Song_list table indexes
|
||||
CREATE INDEX IF NOT EXISTS idx_song_list_match_date ON song_list(match_date);
|
||||
CREATE INDEX IF NOT EXISTS idx_song_list_match_id ON song_list(match_id);
|
||||
|
||||
-- ============================================
|
||||
-- SESSIONS TABLE FOR TOKEN MANAGEMENT
|
||||
-- ============================================
|
||||
|
||||
-- Create sessions table for tracking client tokens
|
||||
CREATE TABLE sessions (
|
||||
token VARCHAR(64) PRIMARY KEY,
|
||||
ip_address VARCHAR(45) NOT NULL,
|
||||
user_agent TEXT NOT NULL,
|
||||
client_type VARCHAR(20) DEFAULT 'web',
|
||||
expires_at TIMESTAMP WITH TIME ZONE NOT NULL,
|
||||
created_at TIMESTAMP WITH TIME ZONE DEFAULT NOW()
|
||||
);
|
||||
|
||||
-- Indexes for fast lookup and cleanup
|
||||
CREATE INDEX idx_sessions_expires ON sessions(expires_at);
|
||||
CREATE INDEX idx_sessions_token ON sessions(token);
|
||||
CREATE INDEX idx_sessions_ip ON sessions(ip_address);
|
||||
CREATE INDEX idx_sessions_created ON sessions(created_at);
|
||||
@@ -0,0 +1,23 @@
|
||||
-- name: CreateSession :one
|
||||
INSERT INTO sessions (token, ip_address, user_agent, client_type, expires_at)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING token, ip_address, user_agent, client_type, expires_at, created_at;
|
||||
|
||||
-- name: GetSession :one
|
||||
SELECT token, ip_address, user_agent, client_type, expires_at, created_at
|
||||
FROM sessions
|
||||
WHERE token = $1
|
||||
LIMIT 1;
|
||||
|
||||
-- name: DeleteSession :exec
|
||||
DELETE FROM sessions
|
||||
WHERE token = $1;
|
||||
|
||||
-- name: DeleteExpiredSessions :exec
|
||||
DELETE FROM sessions
|
||||
WHERE expires_at < NOW();
|
||||
|
||||
-- name: ListSessions :many
|
||||
SELECT token, ip_address, user_agent, client_type, expires_at, created_at
|
||||
FROM sessions
|
||||
ORDER BY created_at DESC;
|
||||
@@ -6,6 +6,8 @@ package repository
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
type Game struct {
|
||||
@@ -21,6 +23,15 @@ type Game struct {
|
||||
Hash string `json:"hash"`
|
||||
}
|
||||
|
||||
type Session struct {
|
||||
Token string `json:"token"`
|
||||
IpAddress string `json:"ip_address"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
ClientType *string `json:"client_type"`
|
||||
ExpiresAt pgtype.Timestamptz `json:"expires_at"`
|
||||
CreatedAt pgtype.Timestamptz `json:"created_at"`
|
||||
}
|
||||
|
||||
type Song struct {
|
||||
GameID int32 `json:"game_id"`
|
||||
SongName string `json:"song_name"`
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
// Code generated by sqlc. DO NOT EDIT.
|
||||
// versions:
|
||||
// sqlc v1.31.1
|
||||
// source: session.sql
|
||||
|
||||
package repository
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const createSession = `-- name: CreateSession :one
|
||||
INSERT INTO sessions (token, ip_address, user_agent, client_type, expires_at)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNING token, ip_address, user_agent, client_type, expires_at, created_at
|
||||
`
|
||||
|
||||
type CreateSessionParams struct {
|
||||
Token string `json:"token"`
|
||||
IpAddress string `json:"ip_address"`
|
||||
UserAgent string `json:"user_agent"`
|
||||
ClientType *string `json:"client_type"`
|
||||
ExpiresAt pgtype.Timestamptz `json:"expires_at"`
|
||||
}
|
||||
|
||||
func (q *Queries) CreateSession(ctx context.Context, arg CreateSessionParams) (Session, error) {
|
||||
row := q.db.QueryRow(ctx, createSession,
|
||||
arg.Token,
|
||||
arg.IpAddress,
|
||||
arg.UserAgent,
|
||||
arg.ClientType,
|
||||
arg.ExpiresAt,
|
||||
)
|
||||
var i Session
|
||||
err := row.Scan(
|
||||
&i.Token,
|
||||
&i.IpAddress,
|
||||
&i.UserAgent,
|
||||
&i.ClientType,
|
||||
&i.ExpiresAt,
|
||||
&i.CreatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const deleteExpiredSessions = `-- name: DeleteExpiredSessions :exec
|
||||
DELETE FROM sessions
|
||||
WHERE expires_at < NOW()
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteExpiredSessions(ctx context.Context) error {
|
||||
_, err := q.db.Exec(ctx, deleteExpiredSessions)
|
||||
return err
|
||||
}
|
||||
|
||||
const deleteSession = `-- name: DeleteSession :exec
|
||||
DELETE FROM sessions
|
||||
WHERE token = $1
|
||||
`
|
||||
|
||||
func (q *Queries) DeleteSession(ctx context.Context, token string) error {
|
||||
_, err := q.db.Exec(ctx, deleteSession, token)
|
||||
return err
|
||||
}
|
||||
|
||||
const getSession = `-- name: GetSession :one
|
||||
SELECT token, ip_address, user_agent, client_type, expires_at, created_at
|
||||
FROM sessions
|
||||
WHERE token = $1
|
||||
LIMIT 1
|
||||
`
|
||||
|
||||
func (q *Queries) GetSession(ctx context.Context, token string) (Session, error) {
|
||||
row := q.db.QueryRow(ctx, getSession, token)
|
||||
var i Session
|
||||
err := row.Scan(
|
||||
&i.Token,
|
||||
&i.IpAddress,
|
||||
&i.UserAgent,
|
||||
&i.ClientType,
|
||||
&i.ExpiresAt,
|
||||
&i.CreatedAt,
|
||||
)
|
||||
return i, err
|
||||
}
|
||||
|
||||
const listSessions = `-- name: ListSessions :many
|
||||
SELECT token, ip_address, user_agent, client_type, expires_at, created_at
|
||||
FROM sessions
|
||||
ORDER BY created_at DESC
|
||||
`
|
||||
|
||||
func (q *Queries) ListSessions(ctx context.Context) ([]Session, error) {
|
||||
rows, err := q.db.Query(ctx, listSessions)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
var items []Session
|
||||
for rows.Next() {
|
||||
var i Session
|
||||
if err := rows.Scan(
|
||||
&i.Token,
|
||||
&i.IpAddress,
|
||||
&i.UserAgent,
|
||||
&i.ClientType,
|
||||
&i.ExpiresAt,
|
||||
&i.CreatedAt,
|
||||
); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, i)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return items, nil
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
// Package middleware provides Echo middleware for the MusicServer application.
|
||||
package middleware
|
||||
@@ -0,0 +1,77 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"music-server/internal/db/repository"
|
||||
"music-server/internal/logging"
|
||||
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
"github.com/labstack/echo/v5"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// TokenAuthMiddleware returns an Echo middleware that validates session tokens
|
||||
func TokenAuthMiddleware(pool *pgxpool.Pool) echo.MiddlewareFunc {
|
||||
return func(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c *echo.Context) error {
|
||||
// Extract token from Authorization header
|
||||
authHeader := c.Request().Header.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
return c.JSON(http.StatusUnauthorized, map[string]string{"error": "Authorization header required"})
|
||||
}
|
||||
|
||||
// Bearer token format
|
||||
parts := strings.Split(authHeader, " ")
|
||||
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||
return c.JSON(http.StatusUnauthorized, map[string]string{"error": "Invalid authorization format. Use: Bearer <token>"})
|
||||
}
|
||||
|
||||
token := parts[1]
|
||||
queries := repository.New(pool)
|
||||
session, err := queries.GetSession(c.Request().Context(), token)
|
||||
if err != nil {
|
||||
logging.GetLogger().Warn("Invalid token attempt",
|
||||
zap.String("token", token),
|
||||
zap.String("ip", c.RealIP()),
|
||||
zap.String("error", err.Error()),
|
||||
)
|
||||
return c.JSON(http.StatusUnauthorized, map[string]string{"error": "Invalid or expired token"})
|
||||
}
|
||||
|
||||
// Check if token is expired
|
||||
if time.Now().After(session.ExpiresAt.Time) {
|
||||
// Clean up expired session in background
|
||||
go func() {
|
||||
queries.DeleteSession(c.Request().Context(), token)
|
||||
}()
|
||||
return c.JSON(http.StatusUnauthorized, map[string]string{"error": "Token expired"})
|
||||
}
|
||||
|
||||
// Add session to request context for potential use by handlers
|
||||
c.Set("session", session)
|
||||
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TokenIPCheckMiddleware checks if the request IP matches the session IP
|
||||
func TokenIPCheckMiddleware(next echo.HandlerFunc) echo.HandlerFunc {
|
||||
return func(c *echo.Context) error {
|
||||
sessionVal := c.Get("session")
|
||||
if sessionVal == nil {
|
||||
return c.JSON(http.StatusUnauthorized, map[string]string{"error": "No session in context"})
|
||||
}
|
||||
session := sessionVal.(repository.Session)
|
||||
if session.IpAddress != c.RealIP() {
|
||||
logging.GetLogger().Warn("Token IP mismatch",
|
||||
zap.String("token_ip", session.IpAddress),
|
||||
zap.String("request_ip", c.RealIP()),
|
||||
)
|
||||
}
|
||||
return next(c)
|
||||
}
|
||||
}
|
||||
@@ -2,13 +2,16 @@ package server
|
||||
|
||||
import (
|
||||
"music-server/cmd/web"
|
||||
"music-server/internal/db"
|
||||
"music-server/internal/logging"
|
||||
"music-server/internal/server/middleware"
|
||||
"net/http"
|
||||
"sort"
|
||||
"strings"
|
||||
|
||||
"github.com/a-h/templ"
|
||||
"github.com/labstack/echo/v5"
|
||||
"github.com/labstack/echo/v5/middleware"
|
||||
echoMiddleware "github.com/labstack/echo/v5/middleware"
|
||||
echoSwagger "github.com/swaggo/echo-swagger/v2"
|
||||
"go.uber.org/zap"
|
||||
"music-server/internal/logging"
|
||||
@@ -36,9 +39,9 @@ func (s *Server) RegisterRoutes() http.Handler {
|
||||
http.ServeFile(w, r, "cmd/docs/swagger.json")
|
||||
})))
|
||||
e.Use(logging.RequestLogger())
|
||||
e.Use(middleware.Recover())
|
||||
e.Use(echoMiddleware.Recover())
|
||||
|
||||
e.Use(middleware.CORSWithConfig(middleware.CORSConfig{
|
||||
e.Use(echoMiddleware.CORSWithConfig(echoMiddleware.CORSConfig{
|
||||
AllowOrigins: []string{"https://*", "http://*"},
|
||||
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH"},
|
||||
AllowHeaders: []string{"Accept", "Authorization", "Content-Type", "X-CSRF-Token"},
|
||||
@@ -103,6 +106,33 @@ func (s *Server) RegisterRoutes() http.Handler {
|
||||
musicGroup.GET("/addQue", music.AddLatestToQue)
|
||||
musicGroup.GET("/addPlayed", music.AddLatestPlayed)
|
||||
|
||||
// ============================================
|
||||
// API v1 Routes with Token Authentication
|
||||
// ============================================
|
||||
|
||||
// Create /api/v1 group
|
||||
apiV1 := e.Group("/api/v1")
|
||||
|
||||
// Public endpoints - no token required
|
||||
apiV1.POST("/token", func(c *echo.Context) error {
|
||||
return s.tokenHandler.CreateTokenHandler(c)
|
||||
})
|
||||
apiV1.DELETE("/token", func(c *echo.Context) error {
|
||||
return s.tokenHandler.DeleteTokenHandler(c)
|
||||
})
|
||||
apiV1.POST("/token/cleanup", func(c *echo.Context) error {
|
||||
return s.tokenHandler.CleanupExpiredSessionsHandler(c)
|
||||
})
|
||||
|
||||
// Protected endpoints - require valid token
|
||||
// Create token auth middleware with pool access
|
||||
tokenAuthMiddleware := middleware.TokenAuthMiddleware(db.Dbpool)
|
||||
|
||||
// Protected group with token authentication - will be used by VGMQ and Statistics API
|
||||
_ = apiV1.Group("", tokenAuthMiddleware)
|
||||
|
||||
// Note: Future protected endpoints (VGMQ, Statistics) will be added here
|
||||
|
||||
routes := e.Router().Routes()
|
||||
sort.Slice(routes, func(i, j int) bool {
|
||||
return routes[i].Path < routes[j].Path
|
||||
|
||||
@@ -15,6 +15,7 @@ import (
|
||||
|
||||
type Server struct {
|
||||
port int
|
||||
tokenHandler *TokenHandler
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -39,8 +40,13 @@ func NewServer() *http.Server {
|
||||
logger := logging.GetLogger()
|
||||
|
||||
port, _ := strconv.Atoi(os.Getenv("PORT"))
|
||||
|
||||
// Initialize token handler
|
||||
tokenHandler := NewTokenHandler()
|
||||
|
||||
NewServer := &Server{
|
||||
port: port,
|
||||
tokenHandler: tokenHandler,
|
||||
}
|
||||
|
||||
logger.Info("Starting server",
|
||||
|
||||
@@ -0,0 +1,172 @@
|
||||
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",
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user