package auth import ( "context" "crypto/rand" "crypto/sha256" "encoding/base64" "encoding/hex" "errors" "time" "mira/backend/internal/users" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" ) var ErrRefreshTokenNotFound = errors.New("refresh token not found") type RefreshSession struct { ID string TokenHash string User users.User ExpiresAt time.Time RevokedAt *time.Time } type SessionStore struct { db *pgxpool.Pool } func NewSessionStore(db *pgxpool.Pool) SessionStore { return SessionStore{db: db} } func GenerateRefreshToken() (string, error) { bytes := make([]byte, 32) if _, err := rand.Read(bytes); err != nil { return "", err } return base64.RawURLEncoding.EncodeToString(bytes), nil } func HashRefreshToken(token string) string { hash := sha256.Sum256([]byte(token)) return hex.EncodeToString(hash[:]) } func (s SessionStore) CreateRefreshSession(ctx context.Context, userID string, tokenHash string, expiresAt time.Time) error { _, err := s.db.Exec(ctx, ` INSERT INTO refresh_tokens (user_id, token_hash, expires_at) VALUES ($1::uuid, $2, $3) `, userID, tokenHash, expiresAt) return err } func (s SessionStore) FindValidRefreshSession(ctx context.Context, tokenHash string) (RefreshSession, error) { row := s.db.QueryRow(ctx, ` SELECT refresh_tokens.id::text, refresh_tokens.token_hash, users.id::text, users.email, users.name, users.password_hash, users.role, users.status, users.created_at, users.updated_at, refresh_tokens.expires_at, refresh_tokens.revoked_at FROM refresh_tokens INNER JOIN users ON users.id = refresh_tokens.user_id WHERE refresh_tokens.token_hash = $1 AND refresh_tokens.revoked_at IS NULL AND refresh_tokens.expires_at > now() AND users.status = 'active' `, tokenHash) var session RefreshSession err := row.Scan( &session.ID, &session.TokenHash, &session.User.ID, &session.User.Email, &session.User.Name, &session.User.PasswordHash, &session.User.Role, &session.User.Status, &session.User.CreatedAt, &session.User.UpdatedAt, &session.ExpiresAt, &session.RevokedAt, ) if errors.Is(err, pgx.ErrNoRows) { return RefreshSession{}, ErrRefreshTokenNotFound } if err != nil { return RefreshSession{}, err } return session, nil } func (s SessionStore) RevokeRefreshSession(ctx context.Context, tokenHash string) error { _, err := s.db.Exec(ctx, ` UPDATE refresh_tokens SET revoked_at = now() WHERE token_hash = $1 AND revoked_at IS NULL `, tokenHash) return err }