package backend import ( "context" "database/sql" "encoding/json" "errors" "fmt" "git.faercol.me/faercol/polyculeconnect/polyculeconnect/internal/model" "git.faercol.me/faercol/polyculeconnect/polyculeconnect/logger" "github.com/google/uuid" ) var ErrNotFound = errors.New("backend not found") const backendRows = `"id", "name", "oidc_issuer", "oidc_client_id", "oidc_client_secret", "oidc_redirect_uri", "oidc_scopes"` type scannable interface { Scan(dest ...any) error } type BackendDB interface { GetAllBackends(ctx context.Context) ([]*model.Backend, error) GetBackendByID(ctx context.Context, id uuid.UUID) (*model.Backend, error) GetBackendByName(ctx context.Context, name string) (*model.Backend, error) AddBackend(ctx context.Context, newBackend *model.Backend) error DeleteBackend(ctx context.Context, id uuid.UUID) error } type sqlBackendDB struct { db *sql.DB } func backendFromRow(row scannable) (*model.Backend, error) { var res model.Backend var scopesStr []byte fmt.Println(string(scopesStr)) if err := row.Scan(&res.ID, &res.Name, &res.Config.Issuer, &res.Config.ClientID, &res.Config.ClientSecret, &res.Config.RedirectURI, &scopesStr); err != nil { if errors.Is(err, sql.ErrNoRows) { return nil, ErrNotFound } return nil, fmt.Errorf("invalid format for backend: %w", err) } if err := json.Unmarshal(scopesStr, &res.Config.Scopes); err != nil { return nil, fmt.Errorf("invalid value for scopes: %w", err) } return &res, nil } func (db *sqlBackendDB) GetBackendByName(ctx context.Context, name string) (*model.Backend, error) { logger.L.Debugf("Getting backend with name %s from DB", name) query := fmt.Sprintf(`SELECT %s FROM "backend" WHERE "name" = ?`, backendRows) row := db.db.QueryRowContext(ctx, query, name) return backendFromRow(row) } func (db *sqlBackendDB) GetBackendByID(ctx context.Context, id uuid.UUID) (*model.Backend, error) { logger.L.Debugf("Getting backend with ID %s from DB", id) query := fmt.Sprintf(`SELECT %s FROM "backend" WHERE "id" = ?`, backendRows) row := db.db.QueryRowContext(ctx, query, id) return backendFromRow(row) } func (db *sqlBackendDB) GetAllBackends(ctx context.Context) ([]*model.Backend, error) { logger.L.Debug("Getting all backends from DB") rows, err := db.db.QueryContext(ctx, fmt.Sprintf(`SELECT %s FROM "backend"`, backendRows)) if err != nil { return nil, err } var res []*model.Backend for rows.Next() { b, err := backendFromRow(rows) if err != nil { return nil, err } res = append(res, b) } return res, rows.Err() } func (db *sqlBackendDB) AddBackend(ctx context.Context, newBackend *model.Backend) error { tx, err := db.db.BeginTx(ctx, nil) if err != nil { return fmt.Errorf("failed to start transaction: %w", err) } defer func() { _ = tx.Rollback() }() scopesStr, err := json.Marshal(newBackend.Config.Scopes) if err != nil { return fmt.Errorf("failed to serialize scopes: %w", err) } query := fmt.Sprintf(`INSERT INTO "backend" (%s) VALUES ($1, $2, $3, $4, $5, $6, $7)`, backendRows) _, err = tx.ExecContext( ctx, query, newBackend.ID, newBackend.Name, newBackend.Config.Issuer, newBackend.Config.ClientID, newBackend.Config.ClientSecret, newBackend.Config.RedirectURI, scopesStr, ) if err != nil { return fmt.Errorf("failed to insert in DB: %w", err) } if err := tx.Commit(); err != nil { return fmt.Errorf("failed to commit transaction: %w", err) } return nil } func (db *sqlBackendDB) DeleteBackend(ctx context.Context, id uuid.UUID) error { tx, err := db.db.BeginTx(ctx, nil) if err != nil { return fmt.Errorf("failed to start transaction: %w", err) } defer func() { _ = tx.Rollback() }() if _, err := tx.ExecContext(ctx, `DELETE FROM "backend" WHERE id = $1`, id.String()); err != nil { return fmt.Errorf("failed to run query: %w", err) } if err := tx.Commit(); err != nil { return fmt.Errorf("failed to commit transaction: %w", err) } return nil } func New(db *sql.DB) *sqlBackendDB { return &sqlBackendDB{db: db} }