Files
apocapoc-api/internal/infrastructure/persistence/sqlite/refresh_token_repository.go
T
david bbe0757ab6
CI/CD Pipeline / Test (push) Has been cancelled
CI/CD Pipeline / Lint (push) Has been cancelled
CI/CD Pipeline / Build and Push Docker Image (push) Has been cancelled
Add refresh token authentication system
Implement complete refresh token flow for improved security:
- Short-lived access tokens (configurable, default 1h)
- Long-lived refresh tokens (configurable, default 7d)
- Automatic token rotation on refresh
- Token revocation for proper logout

Domain layer:
- Add RefreshToken entity with validation and revocation
- Add RefreshTokenRepository interface

Application layer:
- Add RefreshTokenHandler for token refresh operations
- Add RevokeTokenHandler for single token revocation
- Add RevokeAllTokensHandler for user-wide revocation

Infrastructure layer:
- Implement SQLite RefreshTokenRepository
- Add refresh_tokens table migration with indexes
- Add parseDuration helper for flexible time configuration

HTTP layer:
- Add POST /api/v1/auth/refresh endpoint
- Add POST /api/v1/auth/logout endpoint
- Update login/register to return refresh tokens
- Improve Swagger documentation with clear descriptions

Configuration:
- Update .env.example with secure token expiry defaults
- Add support for minute/hour/day duration formats

Tests:
- Fix test suite to work with new signatures
- All existing tests passing
2025-11-27 09:57:54 +01:00

169 lines
3.4 KiB
Go

package sqlite
import (
"context"
"database/sql"
"fmt"
"time"
"apocapoc-api/internal/domain/entities"
"apocapoc-api/internal/shared/errors"
"github.com/google/uuid"
)
type RefreshTokenRepository struct {
db *sql.DB
}
func NewRefreshTokenRepository(db *sql.DB) *RefreshTokenRepository {
return &RefreshTokenRepository{db: db}
}
func (r *RefreshTokenRepository) Create(ctx context.Context, token *entities.RefreshToken) error {
token.ID = uuid.New().String()
query := `
INSERT INTO refresh_tokens (id, user_id, token, expires_at, created_at, revoked_at)
VALUES (?, ?, ?, ?, ?, ?)
`
_, err := r.db.ExecContext(ctx, query,
token.ID,
token.UserID,
token.Token,
token.ExpiresAt,
token.CreatedAt,
token.RevokedAt,
)
if err != nil {
return fmt.Errorf("failed to create refresh token: %w", err)
}
return nil
}
func (r *RefreshTokenRepository) FindByToken(ctx context.Context, token string) (*entities.RefreshToken, error) {
query := `
SELECT id, user_id, token, expires_at, created_at, revoked_at
FROM refresh_tokens
WHERE token = ?
`
var rt entities.RefreshToken
var revokedAt sql.NullTime
err := r.db.QueryRowContext(ctx, query, token).Scan(
&rt.ID,
&rt.UserID,
&rt.Token,
&rt.ExpiresAt,
&rt.CreatedAt,
&revokedAt,
)
if err == sql.ErrNoRows {
return nil, errors.ErrNotFound
}
if err != nil {
return nil, fmt.Errorf("failed to find refresh token: %w", err)
}
if revokedAt.Valid {
rt.RevokedAt = &revokedAt.Time
}
return &rt, nil
}
func (r *RefreshTokenRepository) FindByUserID(ctx context.Context, userID string) ([]*entities.RefreshToken, error) {
query := `
SELECT id, user_id, token, expires_at, created_at, revoked_at
FROM refresh_tokens
WHERE user_id = ?
ORDER BY created_at DESC
`
rows, err := r.db.QueryContext(ctx, query, userID)
if err != nil {
return nil, fmt.Errorf("failed to find refresh tokens: %w", err)
}
defer rows.Close()
var tokens []*entities.RefreshToken
for rows.Next() {
var rt entities.RefreshToken
var revokedAt sql.NullTime
err := rows.Scan(
&rt.ID,
&rt.UserID,
&rt.Token,
&rt.ExpiresAt,
&rt.CreatedAt,
&revokedAt,
)
if err != nil {
return nil, fmt.Errorf("failed to scan refresh token: %w", err)
}
if revokedAt.Valid {
rt.RevokedAt = &revokedAt.Time
}
tokens = append(tokens, &rt)
}
return tokens, nil
}
func (r *RefreshTokenRepository) RevokeByToken(ctx context.Context, token string) error {
query := `
UPDATE refresh_tokens
SET revoked_at = ?
WHERE token = ? AND revoked_at IS NULL
`
result, err := r.db.ExecContext(ctx, query, time.Now(), token)
if err != nil {
return fmt.Errorf("failed to revoke refresh token: %w", err)
}
rows, _ := result.RowsAffected()
if rows == 0 {
return errors.ErrNotFound
}
return nil
}
func (r *RefreshTokenRepository) RevokeAllByUserID(ctx context.Context, userID string) error {
query := `
UPDATE refresh_tokens
SET revoked_at = ?
WHERE user_id = ? AND revoked_at IS NULL
`
_, err := r.db.ExecContext(ctx, query, time.Now(), userID)
if err != nil {
return fmt.Errorf("failed to revoke all refresh tokens: %w", err)
}
return nil
}
func (r *RefreshTokenRepository) DeleteExpired(ctx context.Context) error {
query := `
DELETE FROM refresh_tokens
WHERE expires_at < ?
`
_, err := r.db.ExecContext(ctx, query, time.Now())
if err != nil {
return fmt.Errorf("failed to delete expired refresh tokens: %w", err)
}
return nil
}