113 lines
2.5 KiB
Go
113 lines
2.5 KiB
Go
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
|
|
}
|