package repository import ( "context" "errors" "time" "veyra/services/api/internal/platform/apperror" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgconn" "github.com/jackc/pgx/v5/pgxpool" "veyra/services/api/internal/identity/domain" ) type Postgres struct{ pool *pgxpool.Pool } func NewPostgres(pool *pgxpool.Pool) *Postgres { return &Postgres{pool: pool} } func (r *Postgres) CreateUser(ctx context.Context, user domain.User, credential domain.Credential) (domain.User, error) { tx, err := r.pool.Begin(ctx) if err != nil { return domain.User{}, apperror.ErrUnavailable } defer tx.Rollback(context.Background()) err = tx.QueryRow(ctx, `INSERT INTO veyra_user(id,display_name,status,role) VALUES($1,$2,$3,$4) RETURNING created_at,updated_at`, user.ID, user.DisplayName, user.Status, user.Role).Scan(&user.CreatedAt, &user.UpdatedAt) if err != nil { return domain.User{}, mapError(err) } _, err = tx.Exec(ctx, `INSERT INTO veyra_credential(id,user_id,login_name,password_hash) VALUES($1,$2,$3,$4)`, credential.ID, credential.UserID, credential.LoginName, credential.PasswordHash) if err != nil { return domain.User{}, mapError(err) } if err = tx.Commit(ctx); err != nil { return domain.User{}, apperror.ErrUnavailable } return user, nil } func (r *Postgres) FindCredential(ctx context.Context, login string) (domain.User, domain.Credential, error) { var u domain.User var c domain.Credential err := r.pool.QueryRow(ctx, `SELECT u.id,u.display_name,u.status,u.role,u.created_at,u.updated_at,c.id,c.user_id,c.login_name,c.password_hash,c.created_at FROM veyra_user u JOIN veyra_credential c ON c.user_id=u.id WHERE c.login_name=$1`, login).Scan(&u.ID, &u.DisplayName, &u.Status, &u.Role, &u.CreatedAt, &u.UpdatedAt, &c.ID, &c.UserID, &c.LoginName, &c.PasswordHash, &c.CreatedAt) return u, c, mapError(err) } func (r *Postgres) CreateSession(ctx context.Context, s domain.Session) error { _, err := r.pool.Exec(ctx, `INSERT INTO veyra_session(id,user_id,token_hash,audience,expires_at) VALUES($1,$2,$3,$4,$5)`, s.ID, s.UserID, s.TokenHash, s.Audience, s.ExpiresAt) return mapError(err) } func (r *Postgres) FindSessionUser(ctx context.Context, hash, audience string, now time.Time) (domain.User, error) { var u domain.User err := r.pool.QueryRow(ctx, `SELECT u.id,u.display_name,u.status,u.role,u.created_at,u.updated_at FROM veyra_session s JOIN veyra_user u ON u.id=s.user_id WHERE s.token_hash=$1 AND s.audience=$2 AND s.expires_at>$3 AND s.revoked_at IS NULL AND u.status='ACTIVE' AND (($2='admin' AND u.role='ADMIN') OR ($2='app' AND u.role='USER'))`, hash, audience, now).Scan(&u.ID, &u.DisplayName, &u.Status, &u.Role, &u.CreatedAt, &u.UpdatedAt) if errors.Is(err, pgx.ErrNoRows) { return domain.User{}, apperror.ErrUnauthenticated } return u, mapError(err) } func (r *Postgres) RevokeSession(ctx context.Context, hash, audience string, now time.Time) error { _, err := r.pool.Exec(ctx, `UPDATE veyra_session SET revoked_at=$3 WHERE token_hash=$1 AND audience=$2 AND revoked_at IS NULL`, hash, audience, now) return mapError(err) } func (r *Postgres) ListUsers(ctx context.Context, limit int) ([]domain.User, error) { rows, err := r.pool.Query(ctx, `SELECT id,display_name,status,role,created_at,updated_at FROM veyra_user WHERE role='USER' ORDER BY created_at DESC,id DESC LIMIT $1`, limit) if err != nil { return nil, apperror.ErrUnavailable } defer rows.Close() users := make([]domain.User, 0) for rows.Next() { var u domain.User if err := rows.Scan(&u.ID, &u.DisplayName, &u.Status, &u.Role, &u.CreatedAt, &u.UpdatedAt); err != nil { return nil, apperror.ErrUnavailable } users = append(users, u) } return users, mapError(rows.Err()) } func mapError(err error) error { if err == nil { return nil } if errors.Is(err, pgx.ErrNoRows) { return apperror.ErrNotFound } var pgErr *pgconn.PgError if errors.As(err, &pgErr) && pgErr.Code == "23505" { return apperror.ErrConflict } return apperror.ErrUnavailable }