package auth import ( "context" "errors" "time" "github.com/google/uuid" "github.com/jackc/pgx/v5/pgxpool" ) type PGRepository struct { db *pgxpool.Pool } func NewPGRepository(db *pgxpool.Pool) *PGRepository { return &PGRepository{db: db} } func (r *PGRepository) FindUserByUsername(ctx context.Context, username string) (UserRecord, error) { const query = ` select u.id, u.username, u.display_name, r.name, u.password_hash, u.is_active from users u join roles r on r.id = u.role_id where u.username = $1 and u.deleted_at is null ` row := r.db.QueryRow(ctx, query, username) record := UserRecord{} if err := row.Scan(&record.ID, &record.Username, &record.DisplayName, &record.Role, &record.PasswordHash, &record.IsActive); err != nil { return UserRecord{}, err } return record, nil } func (r *PGRepository) CreateSession(ctx context.Context, userID uuid.UUID, expiresAt time.Time, ipAddress string, userAgent string) (uuid.UUID, error) { const query = ` insert into sessions (id, user_id, ip_address, user_agent, expires_at) values ($1, $2, nullif($3, '')::inet, $4, $5) ` id := uuid.New() _, err := r.db.Exec(ctx, query, id, userID, ipAddress, userAgent, expiresAt) return id, err } func (r *PGRepository) StoreRefreshToken(ctx context.Context, sessionID uuid.UUID, userID uuid.UUID, tokenHash string, expiresAt time.Time) error { const query = ` insert into refresh_tokens (id, session_id, user_id, token_hash, expires_at) values ($1, $2, $3, $4, $5) ` _, err := r.db.Exec(ctx, query, uuid.New(), sessionID, userID, tokenHash, expiresAt) return err } func (r *PGRepository) FindRefreshToken(ctx context.Context, tokenHash string) (UserRecord, uuid.UUID, error) { const query = ` select u.id, u.username, u.display_name, roles.name, u.password_hash, u.is_active, rt.session_id from refresh_tokens rt join users u on u.id = rt.user_id join roles on roles.id = u.role_id where rt.token_hash = $1 and rt.revoked_at is null and rt.expires_at > now() ` record := UserRecord{} var sessionID uuid.UUID row := r.db.QueryRow(ctx, query, tokenHash) if err := row.Scan(&record.ID, &record.Username, &record.DisplayName, &record.Role, &record.PasswordHash, &record.IsActive, &sessionID); err != nil { return UserRecord{}, uuid.Nil, err } if !record.IsActive { return UserRecord{}, uuid.Nil, errors.New("user inactive") } return record, sessionID, nil } func (r *PGRepository) RevokeRefreshToken(ctx context.Context, tokenHash string) error { const query = `update refresh_tokens set revoked_at = now() where token_hash = $1 and revoked_at is null` _, err := r.db.Exec(ctx, query, tokenHash) return err } func (r *PGRepository) HasUsers(ctx context.Context) (bool, error) { var count int if err := r.db.QueryRow(ctx, `select count(*) from users where deleted_at is null`).Scan(&count); err != nil { return false, err } return count > 0, nil } func (r *PGRepository) CreateBootstrapAdmin(ctx context.Context, username, displayName, passwordHash string) (UserRecord, error) { const query = ` insert into users (id, role_id, username, display_name, password_hash, is_active) values ($1, (select id from roles where name = 'admin'), $2, $3, $4, true) returning id, username, display_name, password_hash, is_active ` record := UserRecord{Role: "admin"} err := r.db.QueryRow(ctx, query, uuid.New(), username, displayName, passwordHash). Scan(&record.ID, &record.Username, &record.DisplayName, &record.PasswordHash, &record.IsActive) return record, err }