625 lines
19 KiB
Go
625 lines
19 KiB
Go
package postgres
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
|
|
"github.com/google/uuid"
|
|
"github.com/jmoiron/sqlx"
|
|
"go.uber.org/zap"
|
|
|
|
"github.com/kms/api-key-service/internal/domain"
|
|
"github.com/kms/api-key-service/internal/errors"
|
|
"github.com/kms/api-key-service/internal/repository"
|
|
)
|
|
|
|
// sessionRepository implements the SessionRepository interface
|
|
type sessionRepository struct {
|
|
db *sqlx.DB
|
|
logger *zap.Logger
|
|
}
|
|
|
|
// NewSessionRepository creates a new session repository
|
|
func NewSessionRepository(db *sqlx.DB, logger *zap.Logger) repository.SessionRepository {
|
|
return &sessionRepository{
|
|
db: db,
|
|
logger: logger,
|
|
}
|
|
}
|
|
|
|
// Create creates a new user session
|
|
func (r *sessionRepository) Create(ctx context.Context, session *domain.UserSession) error {
|
|
r.logger.Debug("Creating new session",
|
|
zap.String("user_id", session.UserID),
|
|
zap.String("app_id", session.AppID),
|
|
zap.String("session_type", string(session.SessionType)))
|
|
|
|
// Generate ID if not provided
|
|
if session.ID == uuid.Nil {
|
|
session.ID = uuid.New()
|
|
}
|
|
|
|
// Set timestamps
|
|
now := time.Now()
|
|
session.CreatedAt = now
|
|
session.UpdatedAt = now
|
|
session.LastActivity = now
|
|
|
|
// Serialize metadata
|
|
metadataJSON, err := json.Marshal(session.Metadata)
|
|
if err != nil {
|
|
return errors.NewInternalError("Failed to serialize session metadata").WithInternal(err)
|
|
}
|
|
|
|
query := `
|
|
INSERT INTO user_sessions (
|
|
id, user_id, app_id, session_type, status, access_token,
|
|
refresh_token, id_token, ip_address, user_agent,
|
|
last_activity, expires_at, created_at, updated_at, metadata
|
|
) VALUES (
|
|
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15
|
|
)`
|
|
|
|
_, err = r.db.ExecContext(ctx, query,
|
|
session.ID,
|
|
session.UserID,
|
|
session.AppID,
|
|
session.SessionType,
|
|
session.Status,
|
|
session.AccessToken,
|
|
session.RefreshToken,
|
|
session.IDToken,
|
|
session.IPAddress,
|
|
session.UserAgent,
|
|
session.LastActivity,
|
|
session.ExpiresAt,
|
|
session.CreatedAt,
|
|
session.UpdatedAt,
|
|
metadataJSON,
|
|
)
|
|
|
|
if err != nil {
|
|
r.logger.Error("Failed to create session", zap.Error(err))
|
|
return errors.NewInternalError("Failed to create session").WithInternal(err)
|
|
}
|
|
|
|
r.logger.Debug("Session created successfully", zap.String("session_id", session.ID.String()))
|
|
return nil
|
|
}
|
|
|
|
// GetByID retrieves a session by its ID
|
|
func (r *sessionRepository) GetByID(ctx context.Context, sessionID uuid.UUID) (*domain.UserSession, error) {
|
|
r.logger.Debug("Getting session by ID", zap.String("session_id", sessionID.String()))
|
|
|
|
query := `
|
|
SELECT id, user_id, app_id, session_type, status, access_token,
|
|
refresh_token, id_token, ip_address, user_agent,
|
|
last_activity, expires_at, created_at, updated_at,
|
|
revoked_at, revoked_by, metadata
|
|
FROM user_sessions
|
|
WHERE id = $1`
|
|
|
|
var session domain.UserSession
|
|
var metadataJSON []byte
|
|
var revokedAt sql.NullTime
|
|
var revokedBy sql.NullString
|
|
|
|
err := r.db.QueryRowContext(ctx, query, sessionID).Scan(
|
|
&session.ID,
|
|
&session.UserID,
|
|
&session.AppID,
|
|
&session.SessionType,
|
|
&session.Status,
|
|
&session.AccessToken,
|
|
&session.RefreshToken,
|
|
&session.IDToken,
|
|
&session.IPAddress,
|
|
&session.UserAgent,
|
|
&session.LastActivity,
|
|
&session.ExpiresAt,
|
|
&session.CreatedAt,
|
|
&session.UpdatedAt,
|
|
&revokedAt,
|
|
&revokedBy,
|
|
&metadataJSON,
|
|
)
|
|
|
|
if err != nil {
|
|
if err == sql.ErrNoRows {
|
|
return nil, errors.NewNotFoundError("Session not found")
|
|
}
|
|
r.logger.Error("Failed to get session by ID", zap.Error(err))
|
|
return nil, errors.NewInternalError("Failed to retrieve session").WithInternal(err)
|
|
}
|
|
|
|
// Handle nullable fields
|
|
if revokedAt.Valid {
|
|
session.RevokedAt = &revokedAt.Time
|
|
}
|
|
if revokedBy.Valid {
|
|
session.RevokedBy = &revokedBy.String
|
|
}
|
|
|
|
// Deserialize metadata
|
|
if err := json.Unmarshal(metadataJSON, &session.Metadata); err != nil {
|
|
r.logger.Warn("Failed to deserialize session metadata", zap.Error(err))
|
|
session.Metadata = domain.SessionMetadata{} // Use empty metadata on error
|
|
}
|
|
|
|
r.logger.Debug("Session retrieved successfully", zap.String("session_id", sessionID.String()))
|
|
return &session, nil
|
|
}
|
|
|
|
// GetByUserID retrieves all sessions for a user
|
|
func (r *sessionRepository) GetByUserID(ctx context.Context, userID string) ([]*domain.UserSession, error) {
|
|
r.logger.Debug("Getting sessions by user ID", zap.String("user_id", userID))
|
|
|
|
query := `
|
|
SELECT id, user_id, app_id, session_type, status, access_token,
|
|
refresh_token, id_token, ip_address, user_agent,
|
|
last_activity, expires_at, created_at, updated_at,
|
|
revoked_at, revoked_by, metadata
|
|
FROM user_sessions
|
|
WHERE user_id = $1
|
|
ORDER BY created_at DESC`
|
|
|
|
return r.scanSessions(ctx, query, userID)
|
|
}
|
|
|
|
// GetByUserAndApp retrieves sessions for a specific user and application
|
|
func (r *sessionRepository) GetByUserAndApp(ctx context.Context, userID, appID string) ([]*domain.UserSession, error) {
|
|
r.logger.Debug("Getting sessions by user and app",
|
|
zap.String("user_id", userID),
|
|
zap.String("app_id", appID))
|
|
|
|
query := `
|
|
SELECT id, user_id, app_id, session_type, status, access_token,
|
|
refresh_token, id_token, ip_address, user_agent,
|
|
last_activity, expires_at, created_at, updated_at,
|
|
revoked_at, revoked_by, metadata
|
|
FROM user_sessions
|
|
WHERE user_id = $1 AND app_id = $2
|
|
ORDER BY created_at DESC`
|
|
|
|
return r.scanSessions(ctx, query, userID, appID)
|
|
}
|
|
|
|
// GetActiveByUserID retrieves all active sessions for a user
|
|
func (r *sessionRepository) GetActiveByUserID(ctx context.Context, userID string) ([]*domain.UserSession, error) {
|
|
r.logger.Debug("Getting active sessions by user ID", zap.String("user_id", userID))
|
|
|
|
query := `
|
|
SELECT id, user_id, app_id, session_type, status, access_token,
|
|
refresh_token, id_token, ip_address, user_agent,
|
|
last_activity, expires_at, created_at, updated_at,
|
|
revoked_at, revoked_by, metadata
|
|
FROM user_sessions
|
|
WHERE user_id = $1 AND status = $2 AND expires_at > NOW()
|
|
ORDER BY last_activity DESC`
|
|
|
|
return r.scanSessions(ctx, query, userID, domain.SessionStatusActive)
|
|
}
|
|
|
|
// List retrieves sessions with filtering and pagination
|
|
func (r *sessionRepository) List(ctx context.Context, req *domain.SessionListRequest) (*domain.SessionListResponse, error) {
|
|
r.logger.Debug("Listing sessions with filters",
|
|
zap.String("user_id", req.UserID),
|
|
zap.String("app_id", req.AppID),
|
|
zap.Int("limit", req.Limit),
|
|
zap.Int("offset", req.Offset))
|
|
|
|
// Build WHERE clause dynamically
|
|
whereClause := "WHERE 1=1"
|
|
args := []interface{}{}
|
|
argIndex := 1
|
|
|
|
if req.UserID != "" {
|
|
whereClause += fmt.Sprintf(" AND user_id = $%d", argIndex)
|
|
args = append(args, req.UserID)
|
|
argIndex++
|
|
}
|
|
|
|
if req.AppID != "" {
|
|
whereClause += fmt.Sprintf(" AND app_id = $%d", argIndex)
|
|
args = append(args, req.AppID)
|
|
argIndex++
|
|
}
|
|
|
|
if req.Status != nil {
|
|
whereClause += fmt.Sprintf(" AND status = $%d", argIndex)
|
|
args = append(args, *req.Status)
|
|
argIndex++
|
|
}
|
|
|
|
if req.SessionType != nil {
|
|
whereClause += fmt.Sprintf(" AND session_type = $%d", argIndex)
|
|
args = append(args, *req.SessionType)
|
|
argIndex++
|
|
}
|
|
|
|
if req.TenantID != "" {
|
|
whereClause += fmt.Sprintf(" AND metadata->>'tenant_id' = $%d", argIndex)
|
|
args = append(args, req.TenantID)
|
|
argIndex++
|
|
}
|
|
|
|
// Get total count
|
|
countQuery := fmt.Sprintf("SELECT COUNT(*) FROM user_sessions %s", whereClause)
|
|
var total int
|
|
err := r.db.QueryRowContext(ctx, countQuery, args...).Scan(&total)
|
|
if err != nil {
|
|
r.logger.Error("Failed to get session count", zap.Error(err))
|
|
return nil, errors.NewInternalError("Failed to count sessions").WithInternal(err)
|
|
}
|
|
|
|
// Get sessions with pagination
|
|
query := fmt.Sprintf(`
|
|
SELECT id, user_id, app_id, session_type, status, access_token,
|
|
refresh_token, id_token, ip_address, user_agent,
|
|
last_activity, expires_at, created_at, updated_at,
|
|
revoked_at, revoked_by, metadata
|
|
FROM user_sessions
|
|
%s
|
|
ORDER BY created_at DESC
|
|
LIMIT $%d OFFSET $%d`, whereClause, argIndex, argIndex+1)
|
|
|
|
args = append(args, req.Limit, req.Offset)
|
|
sessions, err := r.scanSessions(ctx, query, args...)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return &domain.SessionListResponse{
|
|
Sessions: sessions,
|
|
Total: total,
|
|
Limit: req.Limit,
|
|
Offset: req.Offset,
|
|
}, nil
|
|
}
|
|
|
|
// Update updates an existing session
|
|
func (r *sessionRepository) Update(ctx context.Context, sessionID uuid.UUID, updates *domain.UpdateSessionRequest) error {
|
|
r.logger.Debug("Updating session", zap.String("session_id", sessionID.String()))
|
|
|
|
// Build UPDATE clause dynamically
|
|
setParts := []string{"updated_at = NOW()"}
|
|
args := []interface{}{}
|
|
argIndex := 1
|
|
|
|
if updates.Status != nil {
|
|
setParts = append(setParts, fmt.Sprintf("status = $%d", argIndex))
|
|
args = append(args, *updates.Status)
|
|
argIndex++
|
|
}
|
|
|
|
if updates.LastActivity != nil {
|
|
setParts = append(setParts, fmt.Sprintf("last_activity = $%d", argIndex))
|
|
args = append(args, *updates.LastActivity)
|
|
argIndex++
|
|
}
|
|
|
|
if updates.ExpiresAt != nil {
|
|
setParts = append(setParts, fmt.Sprintf("expires_at = $%d", argIndex))
|
|
args = append(args, *updates.ExpiresAt)
|
|
argIndex++
|
|
}
|
|
|
|
if updates.IPAddress != nil {
|
|
setParts = append(setParts, fmt.Sprintf("ip_address = $%d", argIndex))
|
|
args = append(args, *updates.IPAddress)
|
|
argIndex++
|
|
}
|
|
|
|
if updates.UserAgent != nil {
|
|
setParts = append(setParts, fmt.Sprintf("user_agent = $%d", argIndex))
|
|
args = append(args, *updates.UserAgent)
|
|
argIndex++
|
|
}
|
|
|
|
if len(setParts) == 1 {
|
|
return errors.NewValidationError("No fields to update")
|
|
}
|
|
|
|
// Build the complete query
|
|
setClause := fmt.Sprintf("%s", setParts[0])
|
|
for i := 1; i < len(setParts); i++ {
|
|
setClause += fmt.Sprintf(", %s", setParts[i])
|
|
}
|
|
|
|
query := fmt.Sprintf("UPDATE user_sessions SET %s WHERE id = $%d", setClause, argIndex)
|
|
args = append(args, sessionID)
|
|
|
|
result, err := r.db.ExecContext(ctx, query, args...)
|
|
if err != nil {
|
|
r.logger.Error("Failed to update session", zap.Error(err))
|
|
return errors.NewInternalError("Failed to update session").WithInternal(err)
|
|
}
|
|
|
|
rowsAffected, err := result.RowsAffected()
|
|
if err != nil {
|
|
return errors.NewInternalError("Failed to get affected rows").WithInternal(err)
|
|
}
|
|
|
|
if rowsAffected == 0 {
|
|
return errors.NewNotFoundError("Session not found")
|
|
}
|
|
|
|
r.logger.Debug("Session updated successfully", zap.String("session_id", sessionID.String()))
|
|
return nil
|
|
}
|
|
|
|
// UpdateActivity updates the last activity timestamp for a session
|
|
func (r *sessionRepository) UpdateActivity(ctx context.Context, sessionID uuid.UUID) error {
|
|
r.logger.Debug("Updating session activity", zap.String("session_id", sessionID.String()))
|
|
|
|
query := `UPDATE user_sessions SET last_activity = NOW(), updated_at = NOW() WHERE id = $1`
|
|
|
|
result, err := r.db.ExecContext(ctx, query, sessionID)
|
|
if err != nil {
|
|
r.logger.Error("Failed to update session activity", zap.Error(err))
|
|
return errors.NewInternalError("Failed to update session activity").WithInternal(err)
|
|
}
|
|
|
|
rowsAffected, err := result.RowsAffected()
|
|
if err != nil {
|
|
return errors.NewInternalError("Failed to get affected rows").WithInternal(err)
|
|
}
|
|
|
|
if rowsAffected == 0 {
|
|
return errors.NewNotFoundError("Session not found")
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Revoke revokes a session
|
|
func (r *sessionRepository) Revoke(ctx context.Context, sessionID uuid.UUID, revokedBy string) error {
|
|
r.logger.Debug("Revoking session",
|
|
zap.String("session_id", sessionID.String()),
|
|
zap.String("revoked_by", revokedBy))
|
|
|
|
query := `
|
|
UPDATE user_sessions
|
|
SET status = $1, revoked_at = NOW(), revoked_by = $2, updated_at = NOW()
|
|
WHERE id = $3`
|
|
|
|
result, err := r.db.ExecContext(ctx, query, domain.SessionStatusRevoked, revokedBy, sessionID)
|
|
if err != nil {
|
|
r.logger.Error("Failed to revoke session", zap.Error(err))
|
|
return errors.NewInternalError("Failed to revoke session").WithInternal(err)
|
|
}
|
|
|
|
rowsAffected, err := result.RowsAffected()
|
|
if err != nil {
|
|
return errors.NewInternalError("Failed to get affected rows").WithInternal(err)
|
|
}
|
|
|
|
if rowsAffected == 0 {
|
|
return errors.NewNotFoundError("Session not found")
|
|
}
|
|
|
|
r.logger.Debug("Session revoked successfully", zap.String("session_id", sessionID.String()))
|
|
return nil
|
|
}
|
|
|
|
// RevokeAllByUser revokes all sessions for a user
|
|
func (r *sessionRepository) RevokeAllByUser(ctx context.Context, userID string, revokedBy string) error {
|
|
r.logger.Debug("Revoking all sessions for user",
|
|
zap.String("user_id", userID),
|
|
zap.String("revoked_by", revokedBy))
|
|
|
|
query := `
|
|
UPDATE user_sessions
|
|
SET status = $1, revoked_at = NOW(), revoked_by = $2, updated_at = NOW()
|
|
WHERE user_id = $3 AND status = $4`
|
|
|
|
result, err := r.db.ExecContext(ctx, query, domain.SessionStatusRevoked, revokedBy, userID, domain.SessionStatusActive)
|
|
if err != nil {
|
|
r.logger.Error("Failed to revoke user sessions", zap.Error(err))
|
|
return errors.NewInternalError("Failed to revoke user sessions").WithInternal(err)
|
|
}
|
|
|
|
rowsAffected, err := result.RowsAffected()
|
|
if err != nil {
|
|
return errors.NewInternalError("Failed to get affected rows").WithInternal(err)
|
|
}
|
|
|
|
r.logger.Debug("User sessions revoked",
|
|
zap.String("user_id", userID),
|
|
zap.Int64("sessions_revoked", rowsAffected))
|
|
return nil
|
|
}
|
|
|
|
// RevokeAllByUserAndApp revokes all sessions for a user and application
|
|
func (r *sessionRepository) RevokeAllByUserAndApp(ctx context.Context, userID, appID string, revokedBy string) error {
|
|
r.logger.Debug("Revoking all sessions for user and app",
|
|
zap.String("user_id", userID),
|
|
zap.String("app_id", appID),
|
|
zap.String("revoked_by", revokedBy))
|
|
|
|
query := `
|
|
UPDATE user_sessions
|
|
SET status = $1, revoked_at = NOW(), revoked_by = $2, updated_at = NOW()
|
|
WHERE user_id = $3 AND app_id = $4 AND status = $5`
|
|
|
|
result, err := r.db.ExecContext(ctx, query, domain.SessionStatusRevoked, revokedBy, userID, appID, domain.SessionStatusActive)
|
|
if err != nil {
|
|
r.logger.Error("Failed to revoke user app sessions", zap.Error(err))
|
|
return errors.NewInternalError("Failed to revoke user app sessions").WithInternal(err)
|
|
}
|
|
|
|
rowsAffected, err := result.RowsAffected()
|
|
if err != nil {
|
|
return errors.NewInternalError("Failed to get affected rows").WithInternal(err)
|
|
}
|
|
|
|
r.logger.Debug("User app sessions revoked",
|
|
zap.String("user_id", userID),
|
|
zap.String("app_id", appID),
|
|
zap.Int64("sessions_revoked", rowsAffected))
|
|
return nil
|
|
}
|
|
|
|
// ExpireOldSessions marks expired sessions as expired
|
|
func (r *sessionRepository) ExpireOldSessions(ctx context.Context) (int, error) {
|
|
r.logger.Debug("Expiring old sessions")
|
|
|
|
query := `
|
|
UPDATE user_sessions
|
|
SET status = $1, updated_at = NOW()
|
|
WHERE expires_at < NOW() AND status = $2`
|
|
|
|
result, err := r.db.ExecContext(ctx, query, domain.SessionStatusExpired, domain.SessionStatusActive)
|
|
if err != nil {
|
|
r.logger.Error("Failed to expire old sessions", zap.Error(err))
|
|
return 0, errors.NewInternalError("Failed to expire old sessions").WithInternal(err)
|
|
}
|
|
|
|
rowsAffected, err := result.RowsAffected()
|
|
if err != nil {
|
|
return 0, errors.NewInternalError("Failed to get affected rows").WithInternal(err)
|
|
}
|
|
|
|
r.logger.Debug("Old sessions expired", zap.Int64("sessions_expired", rowsAffected))
|
|
return int(rowsAffected), nil
|
|
}
|
|
|
|
// DeleteExpiredSessions removes expired sessions older than the specified duration
|
|
func (r *sessionRepository) DeleteExpiredSessions(ctx context.Context, olderThan time.Duration) (int, error) {
|
|
r.logger.Debug("Deleting expired sessions", zap.Duration("older_than", olderThan))
|
|
|
|
cutoffTime := time.Now().Add(-olderThan)
|
|
query := `DELETE FROM user_sessions WHERE status = $1 AND updated_at < $2`
|
|
|
|
result, err := r.db.ExecContext(ctx, query, domain.SessionStatusExpired, cutoffTime)
|
|
if err != nil {
|
|
r.logger.Error("Failed to delete expired sessions", zap.Error(err))
|
|
return 0, errors.NewInternalError("Failed to delete expired sessions").WithInternal(err)
|
|
}
|
|
|
|
rowsAffected, err := result.RowsAffected()
|
|
if err != nil {
|
|
return 0, errors.NewInternalError("Failed to get affected rows").WithInternal(err)
|
|
}
|
|
|
|
r.logger.Debug("Expired sessions deleted", zap.Int64("sessions_deleted", rowsAffected))
|
|
return int(rowsAffected), nil
|
|
}
|
|
|
|
// Exists checks if a session exists
|
|
func (r *sessionRepository) Exists(ctx context.Context, sessionID uuid.UUID) (bool, error) {
|
|
r.logger.Debug("Checking if session exists", zap.String("session_id", sessionID.String()))
|
|
|
|
query := `SELECT EXISTS(SELECT 1 FROM user_sessions WHERE id = $1)`
|
|
|
|
var exists bool
|
|
err := r.db.QueryRowContext(ctx, query, sessionID).Scan(&exists)
|
|
if err != nil {
|
|
r.logger.Error("Failed to check session existence", zap.Error(err))
|
|
return false, errors.NewInternalError("Failed to check session existence").WithInternal(err)
|
|
}
|
|
|
|
return exists, nil
|
|
}
|
|
|
|
// GetSessionCount returns the total number of sessions for a user
|
|
func (r *sessionRepository) GetSessionCount(ctx context.Context, userID string) (int, error) {
|
|
r.logger.Debug("Getting session count for user", zap.String("user_id", userID))
|
|
|
|
query := `SELECT COUNT(*) FROM user_sessions WHERE user_id = $1`
|
|
|
|
var count int
|
|
err := r.db.QueryRowContext(ctx, query, userID).Scan(&count)
|
|
if err != nil {
|
|
r.logger.Error("Failed to get session count", zap.Error(err))
|
|
return 0, errors.NewInternalError("Failed to get session count").WithInternal(err)
|
|
}
|
|
|
|
return count, nil
|
|
}
|
|
|
|
// GetActiveSessionCount returns the number of active sessions for a user
|
|
func (r *sessionRepository) GetActiveSessionCount(ctx context.Context, userID string) (int, error) {
|
|
r.logger.Debug("Getting active session count for user", zap.String("user_id", userID))
|
|
|
|
query := `SELECT COUNT(*) FROM user_sessions WHERE user_id = $1 AND status = $2 AND expires_at > NOW()`
|
|
|
|
var count int
|
|
err := r.db.QueryRowContext(ctx, query, userID, domain.SessionStatusActive).Scan(&count)
|
|
if err != nil {
|
|
r.logger.Error("Failed to get active session count", zap.Error(err))
|
|
return 0, errors.NewInternalError("Failed to get active session count").WithInternal(err)
|
|
}
|
|
|
|
return count, nil
|
|
}
|
|
|
|
// scanSessions is a helper method to scan multiple sessions from query results
|
|
func (r *sessionRepository) scanSessions(ctx context.Context, query string, args ...interface{}) ([]*domain.UserSession, error) {
|
|
rows, err := r.db.QueryContext(ctx, query, args...)
|
|
if err != nil {
|
|
r.logger.Error("Failed to execute session query", zap.Error(err))
|
|
return nil, errors.NewInternalError("Failed to retrieve sessions").WithInternal(err)
|
|
}
|
|
defer rows.Close()
|
|
|
|
var sessions []*domain.UserSession
|
|
for rows.Next() {
|
|
var session domain.UserSession
|
|
var metadataJSON []byte
|
|
var revokedAt sql.NullTime
|
|
var revokedBy sql.NullString
|
|
|
|
err := rows.Scan(
|
|
&session.ID,
|
|
&session.UserID,
|
|
&session.AppID,
|
|
&session.SessionType,
|
|
&session.Status,
|
|
&session.AccessToken,
|
|
&session.RefreshToken,
|
|
&session.IDToken,
|
|
&session.IPAddress,
|
|
&session.UserAgent,
|
|
&session.LastActivity,
|
|
&session.ExpiresAt,
|
|
&session.CreatedAt,
|
|
&session.UpdatedAt,
|
|
&revokedAt,
|
|
&revokedBy,
|
|
&metadataJSON,
|
|
)
|
|
|
|
if err != nil {
|
|
r.logger.Error("Failed to scan session row", zap.Error(err))
|
|
return nil, errors.NewInternalError("Failed to scan session data").WithInternal(err)
|
|
}
|
|
|
|
// Handle nullable fields
|
|
if revokedAt.Valid {
|
|
session.RevokedAt = &revokedAt.Time
|
|
}
|
|
if revokedBy.Valid {
|
|
session.RevokedBy = &revokedBy.String
|
|
}
|
|
|
|
// Deserialize metadata
|
|
if err := json.Unmarshal(metadataJSON, &session.Metadata); err != nil {
|
|
r.logger.Warn("Failed to deserialize session metadata", zap.Error(err))
|
|
session.Metadata = domain.SessionMetadata{} // Use empty metadata on error
|
|
}
|
|
|
|
sessions = append(sessions, &session)
|
|
}
|
|
|
|
if err := rows.Err(); err != nil {
|
|
r.logger.Error("Error iterating session rows", zap.Error(err))
|
|
return nil, errors.NewInternalError("Failed to iterate session results").WithInternal(err)
|
|
}
|
|
|
|
return sessions, nil
|
|
}
|