Repository: dstotijn/hetty Branch: main Commit: 52c83a198927 Files: 161 Total size: 688.4 KB Directory structure: gitextract_7r6v1eh1/ ├── .dockerignore ├── .github/ │ ├── FUNDING.yml │ ├── ISSUE_TEMPLATE/ │ │ ├── bug_report.md │ │ ├── config.yml │ │ └── feature_request.md │ └── workflows/ │ ├── build-test.yml │ └── lint.yml ├── .gitignore ├── .golangci.yml ├── .goreleaser.yml ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── Makefile ├── README.md ├── admin/ │ ├── .eslintrc.json │ ├── .gitignore │ ├── .prettierignore │ ├── .prettierrc.json │ ├── gqlcodegen.yml │ ├── next-env.d.ts │ ├── next.config.js │ ├── package.json │ ├── public/ │ │ ├── site.webmanifest │ │ └── style.css │ ├── src/ │ │ ├── features/ │ │ │ ├── Layout.tsx │ │ │ ├── intercept/ │ │ │ │ ├── components/ │ │ │ │ │ ├── EditRequest.tsx │ │ │ │ │ ├── Intercept.tsx │ │ │ │ │ └── Requests.tsx │ │ │ │ └── graphql/ │ │ │ │ ├── cancelRequest.graphql │ │ │ │ ├── cancelResponse.graphql │ │ │ │ ├── interceptedRequest.graphql │ │ │ │ ├── modifyRequest.graphql │ │ │ │ └── modifyResponse.graphql │ │ │ ├── projects/ │ │ │ │ ├── components/ │ │ │ │ │ ├── NewProject.tsx │ │ │ │ │ └── ProjectList.tsx │ │ │ │ ├── graphql/ │ │ │ │ │ ├── activeProject.graphql │ │ │ │ │ ├── closeProject.graphql │ │ │ │ │ ├── createProject.graphql │ │ │ │ │ ├── deleteProject.graphql │ │ │ │ │ ├── openProject.graphql │ │ │ │ │ └── projects.graphql │ │ │ │ └── hooks/ │ │ │ │ └── useOpenProjectMutation.ts │ │ │ ├── reqlog/ │ │ │ │ ├── components/ │ │ │ │ │ ├── Actions.tsx │ │ │ │ │ ├── LogDetail.tsx │ │ │ │ │ ├── RequestDetail.tsx │ │ │ │ │ ├── RequestLogs.tsx │ │ │ │ │ └── Search.tsx │ │ │ │ ├── graphql/ │ │ │ │ │ ├── clearHttpRequestLog.graphql │ │ │ │ │ ├── httpRequestLog.graphql │ │ │ │ │ ├── httpRequestLogFilter.graphql │ │ │ │ │ ├── httpRequestLogs.graphql │ │ │ │ │ └── setHttpRequestLogFilter.graphql │ │ │ │ └── index.ts │ │ │ ├── scope/ │ │ │ │ ├── components/ │ │ │ │ │ ├── AddRule.tsx │ │ │ │ │ ├── RuleListItem.tsx │ │ │ │ │ └── Rules.tsx │ │ │ │ └── graphql/ │ │ │ │ ├── scope.graphql │ │ │ │ └── setScope.graphql │ │ │ ├── sender/ │ │ │ │ ├── components/ │ │ │ │ │ ├── EditRequest.tsx │ │ │ │ │ ├── History.tsx │ │ │ │ │ └── Sender.tsx │ │ │ │ ├── graphql/ │ │ │ │ │ ├── createOrUpdateRequest.graphql │ │ │ │ │ ├── createSenderRequestFromRequestLog.graphql │ │ │ │ │ ├── sendRequest.graphql │ │ │ │ │ ├── senderRequest.graphql │ │ │ │ │ └── senderRequests.graphql │ │ │ │ └── index.ts │ │ │ └── settings/ │ │ │ ├── components/ │ │ │ │ └── Settings.tsx │ │ │ └── graphql/ │ │ │ └── updateInterceptSettings.graphql │ │ ├── lib/ │ │ │ ├── ActiveProjectContext.tsx │ │ │ ├── InterceptedRequestsContext.tsx │ │ │ ├── components/ │ │ │ │ ├── ConfirmationDialog.tsx │ │ │ │ ├── Editor.tsx │ │ │ │ ├── HttpStatusIcon.tsx │ │ │ │ ├── KeyValuePair.tsx │ │ │ │ ├── Link.tsx │ │ │ │ ├── RequestTabs.tsx │ │ │ │ ├── RequestsTable.tsx │ │ │ │ ├── Response.tsx │ │ │ │ ├── ResponseStatus.tsx │ │ │ │ ├── ResponseTabs.tsx │ │ │ │ ├── SplitPane.tsx │ │ │ │ ├── UrlBar.tsx │ │ │ │ └── useContextMenu.tsx │ │ │ ├── graphql/ │ │ │ │ ├── generated.tsx │ │ │ │ ├── interceptedRequests.graphql │ │ │ │ ├── omitTypename.ts │ │ │ │ └── useApollo.ts │ │ │ ├── mui/ │ │ │ │ ├── createEmotionCache.ts │ │ │ │ └── theme.ts │ │ │ ├── queryParamsFromURL.tsx │ │ │ ├── updateKeyPairItem.ts │ │ │ └── updateURLQueryParams.ts │ │ ├── pages/ │ │ │ ├── _app.tsx │ │ │ ├── _document.tsx │ │ │ ├── index.tsx │ │ │ ├── projects/ │ │ │ │ └── index.tsx │ │ │ ├── proxy/ │ │ │ │ ├── index.tsx │ │ │ │ ├── intercept/ │ │ │ │ │ └── index.tsx │ │ │ │ └── logs/ │ │ │ │ └── index.tsx │ │ │ ├── scope/ │ │ │ │ └── index.tsx │ │ │ ├── sender/ │ │ │ │ └── index.tsx │ │ │ └── settings/ │ │ │ └── index.tsx │ │ └── styles.css │ └── tsconfig.json ├── cmd/ │ └── hetty/ │ ├── cert.go │ ├── config.go │ ├── hetty.go │ └── main.go ├── go.mod ├── go.sum ├── gqlgen.yml ├── pkg/ │ ├── api/ │ │ ├── generated.go │ │ ├── http.go │ │ ├── models.go │ │ ├── models_gen.go │ │ ├── resolvers.go │ │ └── schema.graphql │ ├── chrome/ │ │ └── chrome.go │ ├── db/ │ │ └── bolt/ │ │ ├── bolt.go │ │ ├── logger.go │ │ ├── proj.go │ │ ├── proj_test.go │ │ ├── reqlog.go │ │ ├── reqlog_test.go │ │ ├── sender.go │ │ └── sender_test.go │ ├── filter/ │ │ ├── ast.go │ │ ├── ast_test.go │ │ ├── http.go │ │ ├── lexer.go │ │ ├── lexer_test.go │ │ ├── parser.go │ │ └── parser_test.go │ ├── log/ │ │ └── log.go │ ├── proj/ │ │ ├── proj.go │ │ └── repo.go │ ├── proxy/ │ │ ├── cert.go │ │ ├── gzip.go │ │ ├── intercept/ │ │ │ ├── filter.go │ │ │ ├── intercept.go │ │ │ ├── intercept_test.go │ │ │ └── settings.go │ │ ├── modify.go │ │ ├── net.go │ │ └── proxy.go │ ├── reqlog/ │ │ ├── repo.go │ │ ├── reqlog.go │ │ ├── reqlog_test.go │ │ ├── search.go │ │ └── search_test.go │ ├── scope/ │ │ └── scope.go │ └── sender/ │ ├── repo.go │ ├── search.go │ ├── search_test.go │ ├── sender.go │ ├── sender_test.go │ └── transport.go └── tools.go ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dockerignore ================================================ /admin/.env /admin/.next /admin/dist /admin/node_modules /dist /docs /hetty /cmd/hetty/admin ================================================ FILE: .github/FUNDING.yml ================================================ github: dstotijn patreon: dstotijn custom: "https://www.paypal.com/paypalme/dstotijn" ================================================ FILE: .github/ISSUE_TEMPLATE/bug_report.md ================================================ --- name: Bug report about: Create a report to help us improve title: '' labels: '' assignees: '' --- **Describe the bug** A clear and concise description of what the bug is. **To Reproduce** Steps to reproduce the behavior: 1. Go to '...' 2. Click on '....' 3. Scroll down to '....' 4. See error **Expected behavior** A clear and concise description of what you expected to happen. **Screenshots** If applicable, add screenshots to help explain your problem. **Desktop (please complete the following information):** - OS: [e.g. iOS] - Browser [e.g. chrome, safari] - Version [e.g. 22] **Smartphone (please complete the following information):** - Device: [e.g. iPhone6] - OS: [e.g. iOS8.1] - Browser [e.g. stock browser, safari] - Version [e.g. 22] **Additional context** Add any other context about the problem here. ================================================ FILE: .github/ISSUE_TEMPLATE/config.yml ================================================ blank_issues_enabled: false contact_links: - name: Ask a question url: https://github.com/dstotijn/hetty/discussions about: Ask questions and discuss with other community members ================================================ FILE: .github/ISSUE_TEMPLATE/feature_request.md ================================================ --- name: Feature request about: Suggest an idea for this project title: '' labels: '' assignees: '' --- **Is your feature request related to a problem? Please describe.** A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] **Describe the solution you'd like** A clear and concise description of what you want to happen. **Describe alternatives you've considered** A clear and concise description of any alternative solutions or features you've considered. **Additional context** Add any other context or screenshots about the feature request here. ================================================ FILE: .github/workflows/build-test.yml ================================================ name: Build and Test on: [push, pull_request] jobs: build: runs-on: ubuntu-latest strategy: matrix: go: ["1.23", "1.22", "1.21"] name: Go ${{ matrix.go }} - Build steps: - uses: actions/checkout@v2 - uses: actions/setup-go@v2 with: go-version: ${{ matrix.go }} - uses: actions/setup-node@v2 with: node-version: "16" - uses: actions/cache@v2 with: path: | ~/.cache/go-build ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go- - uses: actions/cache@v2 with: path: ${{ github.workspace }}/admin/.next/cache key: ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}-${{ hashFiles('**.[jt]s', '**.[jt]sx') }} restore-keys: | ${{ runner.os }}-nextjs-${{ hashFiles('**/package-lock.json') }}- - run: make build test: runs-on: ubuntu-latest strategy: matrix: go: ["1.23", "1.22", "1.21"] name: Go ${{ matrix.go }} - Test steps: - uses: actions/checkout@v2 - uses: actions/setup-go@v2 with: go-version: ${{ matrix.go }} - uses: actions/cache@v2 with: path: | ~/.cache/go-build ~/go/pkg/mod key: ${{ runner.os }}-go-${{ hashFiles('**/go.sum') }} restore-keys: | ${{ runner.os }}-go- - run: go test ./pkg/... ================================================ FILE: .github/workflows/lint.yml ================================================ name: Lint on: [push, pull_request] defaults: run: working-directory: ./admin jobs: lint-admin: runs-on: ubuntu-latest name: Admin (Next.js) steps: - uses: actions/checkout@v2 - uses: actions/setup-node@v2 with: node-version: "16" - run: yarn install - run: yarn run lint ================================================ FILE: .gitignore ================================================ *.vscode /dist /hetty /cmd/hetty/admin *.pem *.test ================================================ FILE: .golangci.yml ================================================ linters: presets: - bugs - comment - error - format - import - metalinter - module - performance - style - test - unused disable: - dupl - exhaustive - exhaustivestruct - gochecknoglobals - gochecknoinits - godox - goerr113 - gomnd - interfacer - maligned - nilnil - nlreturn - scopelint - testpackage - varnamelen - wrapcheck linters-settings: gci: local-prefixes: github.com/dstotijn/hetty godot: capital: true ireturn: allow: "error,empty,anon,stdlib,.*(or|er)$,github.com/99designs/gqlgen/graphql.Marshaler,github.com/dstotijn/hetty/pkg/api.QueryResolver,github.com/dstotijn/hetty/pkg/filter.Expression" issues: exclude-rules: - linters: - gosec # Ignore SHA1 usage. text: "G(401|505):" - linters: - wsl # Ignore cuddled defer statements. text: "only one cuddle assignment allowed before defer statement" - linters: - nlreturn # Ignore `break` without leading blank line. text: "break with no blank line before" ================================================ FILE: .goreleaser.yml ================================================ before: hooks: - make clean - sh -c "NEXT_PUBLIC_VERSION={{ .Version}} make build-admin" - go mod tidy builds: - env: - CGO_ENABLED=0 main: ./cmd/hetty ldflags: - -s -w -X main.version={{.Version}} goos: - linux - windows - darwin goarch: - amd64 - arm64 archives: - replacements: darwin: macOS linux: Linux windows: Windows amd64: x86_64 format_overrides: - goos: windows format: zip brews: - tap: owner: hettysoft name: homebrew-tap folder: Formula homepage: https://hetty.xyz description: An HTTP toolkit for security research. license: MIT commit_author: name: David Stotijn email: dstotijn@gmail.com test: | system "#{bin}/hetty -v" snapcrafts: - publish: true summary: An HTTP toolkit for security research. description: | Hetty is an HTTP toolkit for security research. It aims to become an open source alternative to commercial software like Burp Suite Pro, with powerful features tailored to the needs of the infosec and bug bounty community. grade: stable confinement: strict license: MIT apps: hetty: command: hetty plugs: ["network", "network-bind"] scoop: bucket: owner: hettysoft name: scoop-bucket commit_author: name: David Stotijn email: dstotijn@gmail.com homepage: https://hetty.xyz description: An HTTP toolkit for security research. license: MIT dockers: - extra_files: - go.mod - go.sum - pkg - cmd - admin image_templates: - "ghcr.io/dstotijn/hetty:{{ .Version }}" - "ghcr.io/dstotijn/hetty:{{ .Major }}" - "ghcr.io/dstotijn/hetty:{{ .Major }}.{{ .Minor }}" - "ghcr.io/dstotijn/hetty:latest" - "dstotijn/hetty:{{ .Version }}" - "dstotijn/hetty:{{ .Major }}" - "dstotijn/hetty:{{ .Major }}.{{ .Minor }}" - "dstotijn/hetty:latest" build_flag_templates: - "--pull" - "--label=org.opencontainers.image.created={{.Date}}" - "--label=org.opencontainers.image.title={{.ProjectName}}" - "--label=org.opencontainers.image.revision={{.FullCommit}}" - "--label=org.opencontainers.image.version={{.Version}}" - "--label=org.opencontainers.image.source=https://github.com/dstotijn/hetty" - "--build-arg=HETTY_VERSION={{.Version}}" checksum: name_template: "checksums.txt" snapshot: name_template: "{{ incpatch .Version }}-next" changelog: sort: asc filters: exclude: - "^docs:" - "^test:" ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, sex characteristics, gender identity and expression, level of experience, education, socio-economic status, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at dstotijn@gmail.com. All complaints will be reviewed and investigated and will result in a response that is deemed necessary and appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at https://www.contributor-covenant.org/version/1/4/code-of-conduct.html [homepage]: https://www.contributor-covenant.org For answers to common questions about this code of conduct, see https://www.contributor-covenant.org/faq ================================================ FILE: CONTRIBUTING.md ================================================ # Contribution Guidelines Thank you for taking an interest in Hetty! If you want to contribute to the project, please read the guidelines below to ensure a smooth develop experience. ## Code of conduct Please first read the [code of conduct](CODE_OF_CONDUCT.md), and abide to it whenever you interact with the community. ## Issues Use [issues](https://github.com/dstotijn/hetty/issues) for reporting bugs, adding feature requests and giving context to PRs you submit. Please use [labels](https://github.com/dstotijn/hetty/labels) in favor of category prefixes in issue titles. To keep the issue tracker focused on development, use [discussions](https://github.com/dstotijn/hetty/discussions) for usage questions and non-code related discourse. Before submitting new feature requests, check out the Kanban board for the status of on-going work. There might already be a card/issue. ## Pull requests Before submitting a pull request that introduces a new feature or significantly changes the behavior of Hetty, please consider first using [discussions](https://github.com/dstotijn/hetty/discussions) or commenting on a relevant existing issue to share what you have in mind. Because the project is in an early stage, this is especially important; there are still a lot of major design decisions to be made. Until the foundation has solidified, design and implementation leading up to the first milestone (v1.0) is highly in flux, and your work might not align/be applicable for what the maintainers have envisioned. ## Development _Todo: Write steps for setting up local development environment._ ================================================ FILE: Dockerfile ================================================ ARG GO_VERSION=1.17 ARG NODE_VERSION=16.13 ARG ALPINE_VERSION=3.15 FROM node:${NODE_VERSION}-alpine AS node-builder WORKDIR /app COPY admin/package.json admin/yarn.lock ./ RUN yarn install --frozen-lockfile COPY admin/ . ENV NEXT_TELEMETRY_DISABLED=1 RUN yarn run export FROM golang:${GO_VERSION}-alpine AS go-builder ARG HETTY_VERSION=0.0.0 ENV CGO_ENABLED=0 WORKDIR /app COPY go.mod go.sum ./ RUN go mod download COPY cmd ./cmd COPY pkg ./pkg COPY --from=node-builder /app/dist ./cmd/hetty/admin RUN go build -ldflags="-s -w -X main.version=${HETTY_VERSION}" ./cmd/hetty FROM alpine:${ALPINE_VERSION} WORKDIR /app COPY --from=go-builder /app/hetty . ENTRYPOINT ["./hetty"] EXPOSE 8080 ================================================ FILE: LICENSE ================================================ MIT License Copyright (c) 2021 David Stotijn Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. ================================================ FILE: Makefile ================================================ export CGO_ENABLED = 0 export NEXT_TELEMETRY_DISABLED = 1 .PHONY: build build: build-admin go build ./cmd/hetty .PHONY: build-admin build-admin: cd admin && \ yarn install --frozen-lockfile && \ yarn run export && \ mv dist ../cmd/hetty/admin .PHONY: clean clean: rm -f hetty rm -rf ./cmd/hetty/admin rm -rf ./admin/dist rm -rf ./admin/.next ================================================ FILE: README.md ================================================ [![Latest GitHub release](https://img.shields.io/github/v/release/dstotijn/hetty?color=25ae8f)](https://github.com/dstotijn/hetty/releases/latest) [![Build Status](https://img.shields.io/endpoint.svg?url=https%3A%2F%2Factions-badge.atrox.dev%2Fdstotijn%2Fhetty%2Fbadge%3Fref%3Dmain&label=build&color=24ae8f)](https://github.com/dstotijn/hetty/actions/workflows/build-test.yml) ![GitHub download count](https://img.shields.io/github/downloads/dstotijn/hetty/total?color=25ae8f) [![GitHub](https://img.shields.io/github/license/dstotijn/hetty?color=25ae8f)](https://github.com/dstotijn/hetty/blob/master/LICENSE) [![Documentation](https://img.shields.io/badge/hetty-docs-25ae8f)](https://hetty.xyz/) **Hetty** is an HTTP toolkit for security research. It aims to become an open source alternative to commercial software like Burp Suite Pro, with powerful features tailored to the needs of the infosec and bug bounty community. Hetty proxy logs (screenshot) ## Features - Machine-in-the-middle (MITM) HTTP proxy, with logs and advanced search - HTTP client for manually creating/editing requests, and replay proxied requests - Intercept requests and responses for manual review (edit, send/receive, cancel) - Scope support, to help keep work organized - Easy-to-use web based admin interface - Project based database storage, to help keep work organized 👷‍♂️ Hetty is under active development. Check the backlog for the current status. 📣 Are you pen testing professionaly in a team? I would love to hear your thoughts on tooling via [this 5 minute survey](https://forms.gle/36jtgNc3TJ2imi5A8). Thank you! ## Getting started 💡 The [Getting started](https://hetty.xyz/docs/getting-started) doc has more detailed install and usage instructions. ### Installation The quickest way to install and update Hetty is via a package manager: #### macOS ```sh brew install hettysoft/tap/hetty ``` #### Linux ```sh sudo snap install hetty ``` #### Windows ```sh scoop bucket add hettysoft https://github.com/hettysoft/scoop-bucket.git scoop install hettysoft/hetty ``` #### Other Alternatively, you can [download the latest release from GitHub](https://github.com/dstotijn/hetty/releases/latest) for your OS and architecture, and move the binary to a directory in your `$PATH`. If your OS is not available for one of the package managers or not listed in the GitHub releases, you can compile from source _(link coming soon)_. #### Docker Docker images are distributed via [GitHub's Container registry](https://github.com/dstotijn/hetty/pkgs/container/hetty) and [Docker Hub](https://hub.docker.com/r/dstotijn/hetty). To run Hetty via with a volume for database and certificate storage, and port 8080 forwarded: ``` docker run -v $HOME/.hetty:/root/.hetty -p 8080:8080 \ ghcr.io/dstotijn/hetty:latest ``` ### Usage Once installed, start Hetty via: ```sh hetty ``` 💡 Read the [Getting started](https://hetty.xyz/docs/getting-started) doc for more details. To list all available options, run: `hetty --help`: ``` $ hetty --help Usage: hetty [flags] [subcommand] [flags] Runs an HTTP server with (MITM) proxy, GraphQL service, and a web based admin interface. Options: --cert Path to root CA certificate. Creates file if it doesn't exist. (Default: "~/.hetty/hetty_cert.pem") --key Path to root CA private key. Creates file if it doesn't exist. (Default: "~/.hetty/hetty_key.pem") --db Database file path. Creates file if it doesn't exist. (Default: "~/.hetty/hetty.db") --addr TCP address for HTTP server to listen on, in the form \"host:port\". (Default: ":8080") --chrome Launch Chrome with proxy settings applied and certificate errors ignored. (Default: false) --verbose Enable verbose logging. --json Encode logs as JSON, instead of pretty/human readable output. --version, -v Output version. --help, -h Output this usage text. Subcommands: - cert Certificate management Run `hetty --help` for subcommand specific usage instructions. Visit https://hetty.xyz to learn more about Hetty. ``` ## Documentation 📖 [Read the docs](https://hetty.xyz/docs) ## Support Use [issues](https://github.com/dstotijn/hetty/issues) for bug reports and feature requests, and [discussions](https://github.com/dstotijn/hetty/discussions) for questions and troubleshooting. ## Community 💬 [Join the Hetty Discord server](https://discord.gg/3HVsj5pTFP) ## Contributing Want to contribute? Great! Please check the [Contribution Guidelines](CONTRIBUTING.md) for details. ## Acknowledgements - Thanks to the [Hacker101 community on Discord](https://www.hacker101.com/discord) for the encouragement and early feedback. - The font used in the logo and admin interface is [JetBrains Mono](https://www.jetbrains.com/lp/mono/). ## Sponsors 💖 Are you enjoying Hetty? You can [sponsor me](https://github.com/sponsors/dstotijn)! ## License [MIT](LICENSE) © 2019–2025 Hetty Software ================================================ FILE: admin/.eslintrc.json ================================================ { "root": true, "extends": ["next/core-web-vitals", "prettier", "plugin:@typescript-eslint/recommended", "plugin:import/typescript"], "plugins": ["prettier", "@typescript-eslint", "import"], "ignorePatterns": ["next*", "src/lib/graphql/generated.tsx"], "settings": { "import/parsers": { "@typescript-eslint/parser": [".ts", ".tsx"] }, "import/resolver": { "typescript": { "alwaysTryTypes": true } } }, "rules": { "prettier/prettier": ["error"], "@next/next/no-css-tags": "off", "no-unused-vars": "off", "@typescript-eslint/no-unused-vars": [ "error", { "ignoreRestSiblings": true } ], "import/default": "off", "import/no-unresolved": "error", "import/named": "error", "import/namespace": "error", "import/export": "error", "import/no-deprecated": "error", "import/no-cycle": "error", "import/no-named-as-default": "warn", "import/no-named-as-default-member": "warn", "import/no-duplicates": "warn", "import/newline-after-import": "warn", "import/order": [ "warn", { "alphabetize": { "order": "asc", "caseInsensitive": false }, "newlines-between": "always", "groups": ["builtin", "external", "parent", "sibling", "index"] } ], "import/no-unused-modules": [ "error", { "missingExports": true, "ignoreExports": ["./src/pages"] } ] } } ================================================ FILE: admin/.gitignore ================================================ /node_modules /.next /dist *.log* ================================================ FILE: admin/.prettierignore ================================================ /.next/ /out/ /build /coverage ================================================ FILE: admin/.prettierrc.json ================================================ { "printWidth": 120 } ================================================ FILE: admin/gqlcodegen.yml ================================================ overwrite: true schema: "../pkg/api/schema.graphql" documents: "src/**/*.graphql" generates: src/lib/graphql/generated.tsx: plugins: - "typescript" - "typescript-operations" - "typescript-react-apollo" ================================================ FILE: admin/next-env.d.ts ================================================ /// /// // NOTE: This file should not be edited // see https://nextjs.org/docs/basic-features/typescript for more information. ================================================ FILE: admin/next.config.js ================================================ // @ts-check /** * @type {import('next').NextConfig} **/ const nextConfig = { reactStrictMode: true, trailingSlash: true, async rewrites() { return [ { source: "/api/:path/", destination: "http://localhost:8080/api/:path/", }, ]; }, }; module.exports = nextConfig; ================================================ FILE: admin/package.json ================================================ { "name": "hetty-admin", "version": "0.1.0", "private": true, "scripts": { "dev": "next dev", "build": "next build", "start": "next start", "lint": "next lint", "export": "next build && next export -o dist", "generate": "graphql-codegen --config gqlcodegen.yml" }, "dependencies": { "@apollo/client": "^3.2.0", "@emotion/react": "^11.7.1", "@emotion/server": "^11.4.0", "@emotion/styled": "^11.6.0", "@monaco-editor/react": "^4.3.1", "@mui/icons-material": "^5.3.1", "@mui/lab": "^5.0.0-alpha.66", "@mui/material": "^5.3.1", "@mui/styles": "^5.4.2", "allotment": "^1.9.0", "deepmerge": "^4.2.2", "graphql": "^16.2.0", "lodash": "^4.17.21", "monaco-editor": "^0.31.1", "next": "^12.0.8", "next-fonts": "^1.0.3", "react": "^17.0.2", "react-dom": "^17.0.2", "react-split-pane": "^0.1.92" }, "devDependencies": { "@babel/core": "^7.0.0", "@graphql-codegen/cli": "2.6.1", "@graphql-codegen/introspection": "2.1.1", "@graphql-codegen/typescript": "2.4.3", "@graphql-codegen/typescript-operations": "2.3.0", "@graphql-codegen/typescript-react-apollo": "3.2.6", "@types/lodash": "^4.14.178", "@types/node": "^17.0.12", "@types/react": "^17.0.38", "@typescript-eslint/eslint-plugin": "^5.12.0", "@typescript-eslint/parser": "^5.12.0", "eslint": "^8.7.0", "eslint-config-next": "12.0.8", "eslint-config-prettier": "^8.3.0", "eslint-import-resolver-typescript": "^2.5.0", "eslint-plugin-import": "^2.25.4", "eslint-plugin-prettier": "^4.0.0", "prettier": "^2.1.2", "typescript": "^4.0.3", "webpack": "^5.67.0" } } ================================================ FILE: admin/public/site.webmanifest ================================================ {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} ================================================ FILE: admin/public/style.css ================================================ @font-face { font-family: "JetBrains Mono"; src: url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff2/JetBrainsMono-Bold-Italic.woff2") format("woff2"), url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff/JetBrainsMono-Bold-Italic.woff") format("woff"); font-weight: 700; font-style: italic; font-display: swap; } @font-face { font-family: "JetBrains Mono"; src: url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff2/JetBrainsMono-Bold.woff2") format("woff2"), url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff/JetBrainsMono-Bold.woff") format("woff"); font-weight: 700; font-style: normal; font-display: swap; } @font-face { font-family: "JetBrains Mono"; src: url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff2/JetBrainsMono-ExtraBold-Italic.woff2") format("woff2"), url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff/JetBrainsMono-ExtraBold-Italic.woff") format("woff"); font-weight: 800; font-style: italic; font-display: swap; } @font-face { font-family: "JetBrains Mono"; src: url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff2/JetBrainsMono-ExtraBold.woff2") format("woff2"), url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff/JetBrainsMono-ExtraBold.woff") format("woff"); font-weight: 800; font-style: normal; font-display: swap; } @font-face { font-family: "JetBrains Mono"; src: url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff2/JetBrainsMono-Italic.woff2") format("woff2"), url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff/JetBrainsMono-Italic.woff") format("woff"); font-weight: 400; font-style: italic; font-display: swap; } @font-face { font-family: "JetBrains Mono"; src: url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff2/JetBrainsMono-Medium-Italic.woff2") format("woff2"), url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff/JetBrainsMono-Medium-Italic.woff") format("woff"); font-weight: 500; font-style: italic; font-display: swap; } @font-face { font-family: "JetBrains Mono"; src: url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff2/JetBrainsMono-Medium.woff2") format("woff2"), url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff/JetBrainsMono-Medium.woff") format("woff"); font-weight: 500; font-style: normal; font-display: swap; } @font-face { font-family: "JetBrains Mono"; src: url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff2/JetBrainsMono-Regular.woff2") format("woff2"), url("https://cdn.jsdelivr.net/gh/JetBrains/JetBrainsMono/web/woff/JetBrainsMono-Regular.woff") format("woff"); font-weight: 400; font-style: normal; font-display: swap; } code { font-family: "JetBrains Mono", monospace; } ================================================ FILE: admin/src/features/Layout.tsx ================================================ import AltRouteIcon from "@mui/icons-material/AltRoute"; import ChevronLeftIcon from "@mui/icons-material/ChevronLeft"; import ChevronRightIcon from "@mui/icons-material/ChevronRight"; import FolderIcon from "@mui/icons-material/Folder"; import FormatListBulletedIcon from "@mui/icons-material/FormatListBulleted"; import HomeIcon from "@mui/icons-material/Home"; import LocationSearchingIcon from "@mui/icons-material/LocationSearching"; import MenuIcon from "@mui/icons-material/Menu"; import SendIcon from "@mui/icons-material/Send"; import { Theme, useTheme, Toolbar, IconButton, Typography, Divider, List, Tooltip, styled, CSSObject, Box, ListItemText, Badge, } from "@mui/material"; import MuiAppBar, { AppBarProps as MuiAppBarProps } from "@mui/material/AppBar"; import MuiDrawer from "@mui/material/Drawer"; import MuiListItemButton, { ListItemButtonProps } from "@mui/material/ListItemButton"; import MuiListItemIcon, { ListItemIconProps } from "@mui/material/ListItemIcon"; import Link from "next/link"; import React, { useState } from "react"; import { useActiveProject } from "lib/ActiveProjectContext"; import { useInterceptedRequests } from "lib/InterceptedRequestsContext"; export enum Page { Home, GetStarted, Intercept, Projects, ProxySetup, ProxyLogs, Sender, Scope, Settings, } const drawerWidth = 240; const openedMixin = (theme: Theme): CSSObject => ({ width: drawerWidth, transition: theme.transitions.create("width", { easing: theme.transitions.easing.sharp, duration: theme.transitions.duration.enteringScreen, }), overflowX: "hidden", }); const closedMixin = (theme: Theme): CSSObject => ({ transition: theme.transitions.create("width", { easing: theme.transitions.easing.sharp, duration: theme.transitions.duration.leavingScreen, }), overflowX: "hidden", width: 56, }); const DrawerHeader = styled("div")(({ theme }) => ({ display: "flex", alignItems: "center", justifyContent: "flex-start", padding: theme.spacing(0, 1), // necessary for content to be below app bar ...theme.mixins.toolbar, })); interface AppBarProps extends MuiAppBarProps { open?: boolean; } const AppBar = styled(MuiAppBar, { shouldForwardProp: (prop) => prop !== "open", })(({ theme, open }) => ({ backgroundColor: theme.palette.secondary.dark, zIndex: theme.zIndex.drawer + 1, transition: theme.transitions.create(["width", "margin"], { easing: theme.transitions.easing.sharp, duration: theme.transitions.duration.leavingScreen, }), ...(open && { marginLeft: drawerWidth, width: `calc(100% - ${drawerWidth}px)`, transition: theme.transitions.create(["width", "margin"], { easing: theme.transitions.easing.sharp, duration: theme.transitions.duration.enteringScreen, }), }), })); const Drawer = styled(MuiDrawer, { shouldForwardProp: (prop) => prop !== "open" })(({ theme, open }) => ({ width: drawerWidth, flexShrink: 0, whiteSpace: "nowrap", boxSizing: "border-box", ...(open && { ...openedMixin(theme), "& .MuiDrawer-paper": openedMixin(theme), }), ...(!open && { ...closedMixin(theme), "& .MuiDrawer-paper": closedMixin(theme), }), })); const ListItemButton = styled(MuiListItemButton)(({ theme }) => ({ [theme.breakpoints.up("sm")]: { px: 1, }, "&.MuiListItemButton-root": { "&.Mui-selected": { backgroundColor: theme.palette.primary.main, "& .MuiListItemIcon-root": { color: theme.palette.secondary.dark, }, "& .MuiListItemText-root": { color: theme.palette.secondary.dark, }, }, }, })); const ListItemIcon = styled(MuiListItemIcon)(() => ({ minWidth: 42, })); interface Props { children: React.ReactNode; title: string; page: Page; } export function Layout({ title, page, children }: Props): JSX.Element { const activeProject = useActiveProject(); const interceptedRequests = useInterceptedRequests(); const theme = useTheme(); const [open, setOpen] = useState(false); const handleDrawerOpen = () => { setOpen(true); }; const handleDrawerClose = () => { setOpen(false); }; const SiteTitle = styled("span")({ ...(title !== "" && { color: theme.palette.primary.main, marginRight: 4, }), }); return ( Hetty:// {title} v{process.env.NEXT_PUBLIC_VERSION || "0.0"} {theme.direction === "rtl" ? : } {children} ); } ================================================ FILE: admin/src/features/intercept/components/EditRequest.tsx ================================================ import CancelIcon from "@mui/icons-material/Cancel"; import DownloadIcon from "@mui/icons-material/Download"; import SendIcon from "@mui/icons-material/Send"; import SettingsIcon from "@mui/icons-material/Settings"; import { Alert, Box, Button, CircularProgress, IconButton, Tooltip, Typography } from "@mui/material"; import { useRouter } from "next/router"; import React, { useEffect, useState } from "react"; import { useInterceptedRequests } from "lib/InterceptedRequestsContext"; import { KeyValuePair } from "lib/components/KeyValuePair"; import Link from "lib/components/Link"; import RequestTabs from "lib/components/RequestTabs"; import ResponseStatus from "lib/components/ResponseStatus"; import ResponseTabs from "lib/components/ResponseTabs"; import UrlBar, { HttpMethod, HttpProto, httpProtoMap } from "lib/components/UrlBar"; import { HttpProtocol, HttpRequest, useCancelRequestMutation, useCancelResponseMutation, useGetInterceptedRequestQuery, useModifyRequestMutation, useModifyResponseMutation, } from "lib/graphql/generated"; import { queryParamsFromURL } from "lib/queryParamsFromURL"; import updateKeyPairItem from "lib/updateKeyPairItem"; import updateURLQueryParams from "lib/updateURLQueryParams"; function EditRequest(): JSX.Element { const router = useRouter(); const interceptedRequests = useInterceptedRequests(); useEffect(() => { // If there's no request selected and there are pending reqs, navigate to // the first one in the list. This helps you quickly review/handle reqs // without having to manually select the next one in the requests table. if (router.isReady && !router.query.id && interceptedRequests?.length) { const req = interceptedRequests[0]; router.replace(`/proxy/intercept?id=${req.id}`); } }, [router, interceptedRequests]); const reqId = router.query.id as string | undefined; const [method, setMethod] = useState(HttpMethod.Get); const [url, setURL] = useState(""); const [proto, setProto] = useState(HttpProto.Http20); const [queryParams, setQueryParams] = useState([{ key: "", value: "" }]); const [reqHeaders, setReqHeaders] = useState([{ key: "", value: "" }]); const [resHeaders, setResHeaders] = useState([{ key: "", value: "" }]); const [reqBody, setReqBody] = useState(""); const [resBody, setResBody] = useState(""); const handleQueryParamChange = (key: string, value: string, idx: number) => { setQueryParams((prev) => { const updated = updateKeyPairItem(key, value, idx, prev); setURL((prev) => updateURLQueryParams(prev, updated)); return updated; }); }; const handleQueryParamDelete = (idx: number) => { setQueryParams((prev) => { const updated = prev.slice(0, idx).concat(prev.slice(idx + 1, prev.length)); setURL((prev) => updateURLQueryParams(prev, updated)); return updated; }); }; const handleReqHeaderChange = (key: string, value: string, idx: number) => { setReqHeaders((prev) => updateKeyPairItem(key, value, idx, prev)); }; const handleReqHeaderDelete = (idx: number) => { setReqHeaders((prev) => prev.slice(0, idx).concat(prev.slice(idx + 1, prev.length))); }; const handleResHeaderChange = (key: string, value: string, idx: number) => { setResHeaders((prev) => updateKeyPairItem(key, value, idx, prev)); }; const handleResHeaderDelete = (idx: number) => { setResHeaders((prev) => prev.slice(0, idx).concat(prev.slice(idx + 1, prev.length))); }; const handleURLChange = (url: string) => { setURL(url); const questionMarkIndex = url.indexOf("?"); if (questionMarkIndex === -1) { setQueryParams([{ key: "", value: "" }]); return; } const newQueryParams = queryParamsFromURL(url); // Push empty row. newQueryParams.push({ key: "", value: "" }); setQueryParams(newQueryParams); }; const getReqResult = useGetInterceptedRequestQuery({ variables: { id: reqId as string }, skip: reqId === undefined, onCompleted: ({ interceptedRequest }) => { if (!interceptedRequest) { return; } setURL(interceptedRequest.url); setMethod(interceptedRequest.method); setReqBody(interceptedRequest.body || ""); const newQueryParams = queryParamsFromURL(interceptedRequest.url); // Push empty row. newQueryParams.push({ key: "", value: "" }); setQueryParams(newQueryParams); const newReqHeaders = interceptedRequest.headers || []; setReqHeaders([...newReqHeaders.map(({ key, value }) => ({ key, value })), { key: "", value: "" }]); setResBody(interceptedRequest.response?.body || ""); const newResHeaders = interceptedRequest.response?.headers || []; setResHeaders([...newResHeaders.map(({ key, value }) => ({ key, value })), { key: "", value: "" }]); }, }); const interceptedReq = reqId && !getReqResult?.data?.interceptedRequest?.response ? getReqResult?.data?.interceptedRequest : undefined; const interceptedRes = reqId ? getReqResult?.data?.interceptedRequest?.response : undefined; const [modifyRequest, modifyReqResult] = useModifyRequestMutation(); const [cancelRequest, cancelReqResult] = useCancelRequestMutation(); const [modifyResponse, modifyResResult] = useModifyResponseMutation(); const [cancelResponse, cancelResResult] = useCancelResponseMutation(); const onActionCompleted = () => { setURL(""); setMethod(HttpMethod.Get); setReqBody(""); setQueryParams([]); setReqHeaders([]); router.replace(`/proxy/intercept`); }; const handleFormSubmit: React.FormEventHandler = (e) => { e.preventDefault(); if (interceptedReq) { modifyRequest({ variables: { request: { id: interceptedReq.id, url, method, proto: httpProtoMap.get(proto) || HttpProtocol.Http20, headers: reqHeaders.filter((kv) => kv.key !== ""), body: reqBody || undefined, }, }, update(cache) { cache.modify({ fields: { interceptedRequests(existing: HttpRequest[], { readField }) { return existing.filter((ref) => interceptedReq.id !== readField("id", ref)); }, }, }); }, onCompleted: onActionCompleted, }); } if (interceptedRes) { modifyResponse({ variables: { response: { requestID: interceptedRes.id, proto: interceptedRes.proto, // TODO: Allow modifying statusCode: interceptedRes.statusCode, // TODO: Allow modifying statusReason: interceptedRes.statusReason, // TODO: Allow modifying headers: resHeaders.filter((kv) => kv.key !== ""), body: resBody || undefined, }, }, update(cache) { cache.modify({ fields: { interceptedRequests(existing: HttpRequest[], { readField }) { return existing.filter((ref) => interceptedRes.id !== readField("id", ref)); }, }, }); }, onCompleted: onActionCompleted, }); } }; const handleReqCancelClick = () => { if (!interceptedReq) { return; } cancelRequest({ variables: { id: interceptedReq.id, }, update(cache) { cache.modify({ fields: { interceptedRequests(existing: HttpRequest[], { readField }) { return existing.filter((ref) => interceptedReq.id !== readField("id", ref)); }, }, }); }, onCompleted: onActionCompleted, }); }; const handleResCancelClick = () => { if (!interceptedRes) { return; } cancelResponse({ variables: { requestID: interceptedRes.id, }, update(cache) { cache.modify({ fields: { interceptedRequests(existing: HttpRequest[], { readField }) { return existing.filter((ref) => interceptedRes.id !== readField("id", ref)); }, }, }); }, onCompleted: onActionCompleted, }); }; return ( {!interceptedRes && ( <> )} {interceptedRes && ( <> )} {modifyReqResult.error && ( {modifyReqResult.error.message} )} {cancelReqResult.error && ( {cancelReqResult.error.message} )} {interceptedReq && ( Request )} {interceptedRes && ( Response {interceptedRes && ( )} )} ); } export default EditRequest; ================================================ FILE: admin/src/features/intercept/components/Intercept.tsx ================================================ import { Box } from "@mui/material"; import EditRequest from "./EditRequest"; import Requests from "./Requests"; import SplitPane from "lib/components/SplitPane"; export default function Sender(): JSX.Element { return ( ); } ================================================ FILE: admin/src/features/intercept/components/Requests.tsx ================================================ import { Box, Paper, Typography } from "@mui/material"; import { useRouter } from "next/router"; import { useInterceptedRequests } from "lib/InterceptedRequestsContext"; import RequestsTable from "lib/components/RequestsTable"; function Requests(): JSX.Element { const interceptedRequests = useInterceptedRequests(); const router = useRouter(); const activeId = router.query.id as string | undefined; const handleRowClick = (id: string) => { router.push(`/proxy/intercept?id=${id}`); }; return ( {interceptedRequests && interceptedRequests.length > 0 && ( )} {interceptedRequests?.length === 0 && ( No pending intercepted requests. )} ); } export default Requests; ================================================ FILE: admin/src/features/intercept/graphql/cancelRequest.graphql ================================================ mutation CancelRequest($id: ID!) { cancelRequest(id: $id) { success } } ================================================ FILE: admin/src/features/intercept/graphql/cancelResponse.graphql ================================================ mutation CancelResponse($requestID: ID!) { cancelResponse(requestID: $requestID) { success } } ================================================ FILE: admin/src/features/intercept/graphql/interceptedRequest.graphql ================================================ query GetInterceptedRequest($id: ID!) { interceptedRequest(id: $id) { id url method proto headers { key value } body response { id proto statusCode statusReason headers { key value } body } } } ================================================ FILE: admin/src/features/intercept/graphql/modifyRequest.graphql ================================================ mutation ModifyRequest($request: ModifyRequestInput!) { modifyRequest(request: $request) { success } } ================================================ FILE: admin/src/features/intercept/graphql/modifyResponse.graphql ================================================ mutation ModifyResponse($response: ModifyResponseInput!) { modifyResponse(response: $response) { success } } ================================================ FILE: admin/src/features/projects/components/NewProject.tsx ================================================ import AddIcon from "@mui/icons-material/Add"; import { Box, Button, CircularProgress, TextField, Typography } from "@mui/material"; import React, { useState } from "react"; import useOpenProjectMutation from "../hooks/useOpenProjectMutation"; import { useCreateProjectMutation } from "lib/graphql/generated"; function NewProject(): JSX.Element { const [name, setName] = useState(""); const [createProject, createProjResult] = useCreateProjectMutation({ onCompleted(data) { setName(""); if (data?.createProject) { openProject({ variables: { id: data.createProject?.id } }); } }, }); const [openProject, openProjResult] = useOpenProjectMutation(); const handleCreateAndOpenProjectForm = (e: React.SyntheticEvent) => { e.preventDefault(); createProject({ variables: { name } }); }; return (
New project
setName(e.target.value)} error={Boolean(createProjResult.error || openProjResult.error)} helperText={ (createProjResult.error && createProjResult.error.message) || (openProjResult.error && openProjResult.error.message) } />
); } export default NewProject; ================================================ FILE: admin/src/features/projects/components/ProjectList.tsx ================================================ import CloseIcon from "@mui/icons-material/Close"; import DeleteIcon from "@mui/icons-material/Delete"; import DescriptionIcon from "@mui/icons-material/Description"; import LaunchIcon from "@mui/icons-material/Launch"; import SettingsIcon from "@mui/icons-material/Settings"; import { Alert } from "@mui/lab"; import { Avatar, Box, Button, CircularProgress, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle, IconButton, List, ListItem, ListItemAvatar, ListItemSecondaryAction, ListItemText, Paper, Snackbar, Tooltip, Typography, useTheme, } from "@mui/material"; import React, { useState } from "react"; import useOpenProjectMutation from "../hooks/useOpenProjectMutation"; import Link, { NextLinkComposed } from "lib/components/Link"; import { ProjectsQuery, useCloseProjectMutation, useDeleteProjectMutation, useProjectsQuery, } from "lib/graphql/generated"; function ProjectList(): JSX.Element { const theme = useTheme(); const projResult = useProjectsQuery({ fetchPolicy: "network-only" }); const [openProject, openProjResult] = useOpenProjectMutation(); const [closeProject, closeProjResult] = useCloseProjectMutation({ errorPolicy: "all", onCompleted() { closeProjResult.client.resetStore(); }, update(cache) { cache.modify({ fields: { activeProject() { return null; }, projects(_, { DELETE }) { return DELETE; }, httpRequestLogFilter(_, { DELETE }) { return DELETE; }, }, }); }, }); const [deleteProject, deleteProjResult] = useDeleteProjectMutation({ errorPolicy: "all", update(cache) { cache.modify({ fields: { projects(_, { DELETE }) { return DELETE; }, }, }); setDeleteDiagOpen(false); setDeleteNotifOpen(true); }, }); const [deleteProj, setDeleteProj] = useState(); const [deleteDiagOpen, setDeleteDiagOpen] = useState(false); const handleDeleteButtonClick = (project: ProjectsQuery["projects"][number]) => { setDeleteProj(project); setDeleteDiagOpen(true); }; const handleDeleteConfirm = () => { if (deleteProj) { deleteProject({ variables: { id: deleteProj.id } }); } }; const handleDeleteCancel = () => { setDeleteDiagOpen(false); }; const [deleteNotifOpen, setDeleteNotifOpen] = useState(false); const handleCloseDeleteNotif = (_: Event | React.SyntheticEvent, reason?: string) => { if (reason === "clickaway") { return; } setDeleteNotifOpen(false); }; return (
Delete project “{deleteProj?.name}”? Deleting a project permanently removes all its data from the database. This action is irreversible. {deleteProjResult.error && ( Error closing project: {deleteProjResult.error.message} )} Project {deleteProj?.name} was deleted. Manage projects {projResult.loading && } {projResult.error && Error fetching projects: {projResult.error.message}} {openProjResult.error && Error opening project: {openProjResult.error.message}} {closeProjResult.error && ( Error closing project: {closeProjResult.error.message} )} {projResult.data && projResult.data.projects.length > 0 && ( {projResult.data.projects.map((project) => ( {project.name} {project.isActive && (Active)} {project.isActive && ( closeProject()}> )} {!project.isActive && ( openProject({ variables: { id: project.id }, }) } > )} handleDeleteButtonClick(project)} disabled={project.isActive}> ))} )} {projResult.data?.projects.length === 0 && ( There are no projects. Create one to get started. )}
); } export default ProjectList; ================================================ FILE: admin/src/features/projects/graphql/activeProject.graphql ================================================ query ActiveProject { activeProject { id name isActive settings { intercept { requestsEnabled responsesEnabled requestFilter responseFilter } } } } ================================================ FILE: admin/src/features/projects/graphql/closeProject.graphql ================================================ mutation CloseProject { closeProject { success } } ================================================ FILE: admin/src/features/projects/graphql/createProject.graphql ================================================ mutation CreateProject($name: String!) { createProject(name: $name) { id name } } ================================================ FILE: admin/src/features/projects/graphql/deleteProject.graphql ================================================ mutation DeleteProject($id: ID!) { deleteProject(id: $id) { success } } ================================================ FILE: admin/src/features/projects/graphql/openProject.graphql ================================================ mutation OpenProject($id: ID!) { openProject(id: $id) { id name isActive } } ================================================ FILE: admin/src/features/projects/graphql/projects.graphql ================================================ query Projects { projects { id name isActive } } ================================================ FILE: admin/src/features/projects/hooks/useOpenProjectMutation.ts ================================================ import { gql } from "@apollo/client"; import { useOpenProjectMutation as _useOpenProjectMutation } from "lib/graphql/generated"; export default function useOpenProjectMutation() { return _useOpenProjectMutation({ errorPolicy: "all", update(cache, { data }) { cache.modify({ fields: { activeProject() { const activeProjRef = cache.writeFragment({ data: data?.openProject, fragment: gql` fragment ActiveProject on Project { id name isActive type } `, }); return activeProjRef; }, projects(_, { DELETE }) { cache.writeFragment({ id: data?.openProject?.id, data: data?.openProject, fragment: gql` fragment OpenProject on Project { id name isActive type } `, }); return DELETE; }, httpRequestLogFilter(_, { DELETE }) { return DELETE; }, }, }); }, }); } ================================================ FILE: admin/src/features/reqlog/components/Actions.tsx ================================================ import AltRouteIcon from "@mui/icons-material/AltRoute"; import DeleteIcon from "@mui/icons-material/Delete"; import { Alert } from "@mui/lab"; import { Badge, Button, IconButton, Tooltip } from "@mui/material"; import Link from "next/link"; import { useActiveProject } from "lib/ActiveProjectContext"; import { useInterceptedRequests } from "lib/InterceptedRequestsContext"; import { ConfirmationDialog, useConfirmationDialog } from "lib/components/ConfirmationDialog"; import { HttpRequestLogsDocument, useClearHttpRequestLogMutation } from "lib/graphql/generated"; function Actions(): JSX.Element { const activeProject = useActiveProject(); const interceptedRequests = useInterceptedRequests(); const [clearHTTPRequestLog, clearLogsResult] = useClearHttpRequestLogMutation({ refetchQueries: [{ query: HttpRequestLogsDocument }], }); const clearHTTPConfirmationDialog = useConfirmationDialog(); return (
All proxy logs are going to be removed. This action cannot be undone. {clearLogsResult.error && Failed to clear HTTP logs: {clearLogsResult.error}} {(activeProject?.settings.intercept.requestsEnabled || activeProject?.settings.intercept.responsesEnabled) && ( )}
); } export default Actions; ================================================ FILE: admin/src/features/reqlog/components/LogDetail.tsx ================================================ import Alert from "@mui/lab/Alert"; import { Box, CircularProgress, Paper, Typography } from "@mui/material"; import RequestDetail from "./RequestDetail"; import Response from "lib/components/Response"; import SplitPane from "lib/components/SplitPane"; import { useHttpRequestLogQuery } from "lib/graphql/generated"; interface Props { id?: string; } function LogDetail({ id }: Props): JSX.Element { const { loading, error, data } = useHttpRequestLogQuery({ variables: { id: id as string }, skip: id === undefined, }); if (loading) { return ; } if (error) { return Error fetching logs details: {error.message}; } if (data && !data.httpRequestLog) { return ( Request {id} was not found. ); } if (!data?.httpRequestLog) { return ( Select a log entry… ); } const reqLog = data.httpRequestLog; return ( {reqLog.response && ( )} ); } export default LogDetail; ================================================ FILE: admin/src/features/reqlog/components/RequestDetail.tsx ================================================ import { Typography, Box } from "@mui/material"; import React from "react"; import RequestTabs from "lib/components/RequestTabs"; import { HttpRequestLogQuery } from "lib/graphql/generated"; import { queryParamsFromURL } from "lib/queryParamsFromURL"; interface Props { request: NonNullable; } function RequestDetail({ request }: Props): JSX.Element { const { method, url, headers, body } = request; const parsedUrl = new URL(url); return ( Request {method} {decodeURIComponent(parsedUrl.pathname + parsedUrl.search)} ); } export default RequestDetail; ================================================ FILE: admin/src/features/reqlog/components/RequestLogs.tsx ================================================ import ContentCopyIcon from "@mui/icons-material/ContentCopy"; import { Alert, Box, IconButton, Link, MenuItem, Snackbar, styled, TableCell, TableCellProps, Tooltip, } from "@mui/material"; import { useRouter } from "next/router"; import { useState } from "react"; import Actions from "./Actions"; import LogDetail from "./LogDetail"; import Search from "./Search"; import RequestsTable from "lib/components/RequestsTable"; import SplitPane from "lib/components/SplitPane"; import useContextMenu from "lib/components/useContextMenu"; import { useCreateSenderRequestFromHttpRequestLogMutation, useHttpRequestLogsQuery } from "lib/graphql/generated"; const ActionsTableCell = styled(TableCell)(() => ({ paddingTop: 0, paddingBottom: 0, })); export function RequestLogs(): JSX.Element { const router = useRouter(); const id = router.query.id as string | undefined; const { data } = useHttpRequestLogsQuery({ pollInterval: 1000, }); const [createSenderReqFromLog] = useCreateSenderRequestFromHttpRequestLogMutation({ onCompleted({ createSenderRequestFromHttpRequestLog }) { const { id } = createSenderRequestFromHttpRequestLog; setNewSenderReqId(id); setCopiedReqNotifOpen(true); }, }); const [copyToSenderId, setCopyToSenderId] = useState(""); const [Menu, handleContextMenu, handleContextMenuClose] = useContextMenu(); const handleCopyToSenderClick = () => { createSenderReqFromLog({ variables: { id: copyToSenderId, }, }); handleContextMenuClose(); }; const [newSenderReqId, setNewSenderReqId] = useState(""); const [copiedReqNotifOpen, setCopiedReqNotifOpen] = useState(false); const handleCloseCopiedNotif = (_: Event | React.SyntheticEvent, reason?: string) => { if (reason === "clickaway") { return; } setCopiedReqNotifOpen(false); }; const handleRowClick = (id: string) => { router.push(`/proxy/logs?id=${id}`); }; const handleRowContextClick = (e: React.MouseEvent, id: string) => { setCopyToSenderId(id); handleContextMenu(e); }; const actionsCell = (id: string) => ( { setCopyToSenderId(id); createSenderReqFromLog({ variables: { id, }, }); }} > ); return ( Copy request to Sender Request was copied. Edit in Sender. ); } ================================================ FILE: admin/src/features/reqlog/components/Search.tsx ================================================ import FilterListIcon from "@mui/icons-material/FilterList"; import SearchIcon from "@mui/icons-material/Search"; import { Alert } from "@mui/lab"; import { Box, Checkbox, CircularProgress, ClickAwayListener, FormControlLabel, InputBase, Paper, Popper, Tooltip, useTheme, } from "@mui/material"; import IconButton from "@mui/material/IconButton"; import React, { useRef, useState } from "react"; import { HttpRequestLogFilterDocument, useHttpRequestLogFilterQuery, useSetHttpRequestLogFilterMutation, } from "lib/graphql/generated"; import { withoutTypename } from "lib/graphql/omitTypename"; function Search(): JSX.Element { const theme = useTheme(); const [searchExpr, setSearchExpr] = useState(""); const filterResult = useHttpRequestLogFilterQuery({ onCompleted: (data) => { setSearchExpr(data.httpRequestLogFilter?.searchExpression || ""); }, }); const filter = filterResult.data?.httpRequestLogFilter; const [setFilterMutate, setFilterResult] = useSetHttpRequestLogFilterMutation({ update(cache, { data }) { cache.writeQuery({ query: HttpRequestLogFilterDocument, data: { httpRequestLogFilter: data?.setHttpRequestLogFilter, }, }); }, }); const filterRef = useRef(null); const [filterOpen, setFilterOpen] = useState(false); const handleSubmit = (e: React.SyntheticEvent) => { setFilterMutate({ variables: { filter: { ...withoutTypename(filter), searchExpression: searchExpr, }, }, }); setFilterOpen(false); e.preventDefault(); }; const handleClickAway = (event: MouseEvent | TouchEvent) => { if (filterRef?.current && filterRef.current.contains(event.target as HTMLElement)) { return; } setFilterOpen(false); }; return ( setFilterOpen(!filterOpen)} sx={{ p: 1, color: filter?.onlyInScope ? "primary.main" : "inherit", }} > {filterResult.loading || setFilterResult.loading ? ( ) : ( )} setSearchExpr(e.target.value)} onFocus={() => setFilterOpen(true)} autoCorrect="false" spellCheck="false" /> setFilterMutate({ variables: { filter: { ...withoutTypename(filter), onlyInScope: e.target.checked, }, }, }) } /> } label="Only show in-scope requests" /> ); } function Error(props: { prefix: string; error?: Error }) { if (!props.error) return null; return ( {props.prefix}: {props.error.message} ); } export default Search; ================================================ FILE: admin/src/features/reqlog/graphql/clearHttpRequestLog.graphql ================================================ mutation ClearHTTPRequestLog { clearHTTPRequestLog { success } } ================================================ FILE: admin/src/features/reqlog/graphql/httpRequestLog.graphql ================================================ query HttpRequestLog($id: ID!) { httpRequestLog(id: $id) { id method url proto headers { key value } body response { id proto headers { key value } statusCode statusReason body } } } ================================================ FILE: admin/src/features/reqlog/graphql/httpRequestLogFilter.graphql ================================================ query HttpRequestLogFilter { httpRequestLogFilter { onlyInScope searchExpression } } ================================================ FILE: admin/src/features/reqlog/graphql/httpRequestLogs.graphql ================================================ query HttpRequestLogs { httpRequestLogs { id method url timestamp response { statusCode statusReason } } } ================================================ FILE: admin/src/features/reqlog/graphql/setHttpRequestLogFilter.graphql ================================================ mutation SetHttpRequestLogFilter($filter: HttpRequestLogFilterInput) { setHttpRequestLogFilter(filter: $filter) { onlyInScope searchExpression } } ================================================ FILE: admin/src/features/reqlog/index.ts ================================================ import { RequestLogs } from "./components/RequestLogs"; export default RequestLogs; ================================================ FILE: admin/src/features/scope/components/AddRule.tsx ================================================ import { useApolloClient } from "@apollo/client"; import AddIcon from "@mui/icons-material/Add"; import { Alert } from "@mui/lab"; import { Box, Button, CircularProgress, FormControl, FormControlLabel, FormLabel, Radio, RadioGroup, TextField, } from "@mui/material"; import React, { useState } from "react"; import { ScopeDocument, ScopeQuery, ScopeRule, useSetScopeMutation } from "lib/graphql/generated"; function AddRule(): JSX.Element { const [ruleType, setRuleType] = useState("url"); const [expression, setExpression] = useState(""); const client = useApolloClient(); const [setScope, { error, loading }] = useSetScopeMutation({ onCompleted({ setScope }) { client.writeQuery({ query: ScopeDocument, data: { scope: setScope }, }); setExpression(""); }, }); const handleTypeChange = (e: React.ChangeEvent, value: string) => { setRuleType(value); }; const handleSubmit = (e: React.SyntheticEvent) => { e.preventDefault(); let scope: ScopeRule[] = []; try { const data = client.readQuery({ query: ScopeDocument, }); if (data) { scope = data.scope; } } catch (e) {} setScope({ variables: { scope: [...scope.map(({ url }) => ({ url })), { url: expression }], }, }); }; return (
{error && ( Error adding rule: {error.message} )}
Rule Type } label="URL" /> setExpression(e.target.value)} InputProps={{ sx: { fontFamily: "'JetBrains Mono', monospace" }, }} InputLabelProps={{ shrink: true, }} margin="normal" />
); } export default AddRule; ================================================ FILE: admin/src/features/scope/components/RuleListItem.tsx ================================================ import { useApolloClient } from "@apollo/client"; import CodeIcon from "@mui/icons-material/Code"; import DeleteIcon from "@mui/icons-material/Delete"; import { Avatar, Chip, IconButton, ListItem, ListItemAvatar, ListItemSecondaryAction, ListItemText, Tooltip, } from "@mui/material"; import React from "react"; import { ScopeDocument, ScopeQuery, useSetScopeMutation } from "lib/graphql/generated"; type ScopeRule = ScopeQuery["scope"][number]; type RuleListItemProps = { scope: ScopeQuery["scope"]; rule: ScopeRule; index: number; }; function RuleListItem({ scope, rule, index }: RuleListItemProps): JSX.Element { const client = useApolloClient(); const [setScope, { loading }] = useSetScopeMutation({ onCompleted({ setScope }) { client.writeQuery({ query: ScopeDocument, data: { scope: setScope }, }); }, }); const handleDelete = (index: number) => { const clone = [...scope]; clone.splice(index, 1); setScope({ variables: { scope: clone.map(({ url }) => ({ url })), }, }); }; return ( handleDelete(index)} disabled={loading}> ); } function RuleListItemText({ rule }: { rule: ScopeRule }): JSX.Element { let text: JSX.Element =
; if (rule.url) { text = {rule.url}; } // TODO: Parse and handle rule.header and rule.body. return {text}; } function RuleTypeChip({ rule }: { rule: ScopeRule }): JSX.Element { let label = "Unknown"; if (rule.url) { label = "URL"; } return ; } export default RuleListItem; ================================================ FILE: admin/src/features/scope/components/Rules.tsx ================================================ import { Alert } from "@mui/lab"; import { CircularProgress, List } from "@mui/material"; import React from "react"; import RuleListItem from "./RuleListItem"; import { useScopeQuery } from "lib/graphql/generated"; function Rules(): JSX.Element { const { loading, error, data } = useScopeQuery(); return (
{loading && } {error && Error fetching scope: {error.message}} {data && data.scope.length > 0 && ( {data.scope.map((rule, index) => ( ))} )}
); } export default Rules; ================================================ FILE: admin/src/features/scope/graphql/scope.graphql ================================================ query Scope { scope { url } } ================================================ FILE: admin/src/features/scope/graphql/setScope.graphql ================================================ mutation SetScope($scope: [ScopeRuleInput!]!) { setScope(scope: $scope) { url } } ================================================ FILE: admin/src/features/sender/components/EditRequest.tsx ================================================ import AddIcon from "@mui/icons-material/Add"; import { Alert, Box, Button, Fab, Tooltip, Typography, useTheme } from "@mui/material"; import { useRouter } from "next/router"; import React, { useState } from "react"; import { KeyValuePair } from "lib/components/KeyValuePair"; import RequestTabs from "lib/components/RequestTabs"; import Response from "lib/components/Response"; import SplitPane from "lib/components/SplitPane"; import UrlBar, { HttpMethod, HttpProto, httpProtoMap } from "lib/components/UrlBar"; import { GetSenderRequestQuery, useCreateOrUpdateSenderRequestMutation, useGetSenderRequestQuery, useSendRequestMutation, } from "lib/graphql/generated"; import { queryParamsFromURL } from "lib/queryParamsFromURL"; import updateKeyPairItem from "lib/updateKeyPairItem"; import updateURLQueryParams from "lib/updateURLQueryParams"; const defaultMethod = HttpMethod.Get; const defaultProto = HttpProto.Http20; const emptyKeyPair = [{ key: "", value: "" }]; function EditRequest(): JSX.Element { const router = useRouter(); const reqId = router.query.id as string | undefined; const theme = useTheme(); const [method, setMethod] = useState(defaultMethod); const [url, setURL] = useState(""); const [proto, setProto] = useState(defaultProto); const [queryParams, setQueryParams] = useState(emptyKeyPair); const [headers, setHeaders] = useState(emptyKeyPair); const [body, setBody] = useState(""); const handleQueryParamChange = (key: string, value: string, idx: number) => { setQueryParams((prev) => { const updated = updateKeyPairItem(key, value, idx, prev); setURL((prev) => updateURLQueryParams(prev, updated)); return updated; }); }; const handleQueryParamDelete = (idx: number) => { setQueryParams((prev) => { const updated = prev.slice(0, idx).concat(prev.slice(idx + 1, prev.length)); setURL((prev) => updateURLQueryParams(prev, updated)); return updated; }); }; const handleHeaderChange = (key: string, value: string, idx: number) => { setHeaders((prev) => updateKeyPairItem(key, value, idx, prev)); }; const handleHeaderDelete = (idx: number) => { setHeaders((prev) => prev.slice(0, idx).concat(prev.slice(idx + 1, prev.length))); }; const handleURLChange = (url: string) => { setURL(url); const questionMarkIndex = url.indexOf("?"); if (questionMarkIndex === -1) { setQueryParams([{ key: "", value: "" }]); return; } const newQueryParams = queryParamsFromURL(url); // Push empty row. newQueryParams.push({ key: "", value: "" }); setQueryParams(newQueryParams); }; const [response, setResponse] = useState["response"]>(null); const getReqResult = useGetSenderRequestQuery({ variables: { id: reqId as string }, skip: reqId === undefined, onCompleted: ({ senderRequest }) => { if (!senderRequest) { return; } setURL(senderRequest.url); setMethod(senderRequest.method); setBody(senderRequest.body || ""); const newQueryParams = queryParamsFromURL(senderRequest.url); // Push empty row. newQueryParams.push({ key: "", value: "" }); setQueryParams(newQueryParams); const newHeaders = senderRequest.headers || []; setHeaders([...newHeaders.map(({ key, value }) => ({ key, value })), { key: "", value: "" }]); setResponse(senderRequest.response); }, }); const [createOrUpdateRequest, createResult] = useCreateOrUpdateSenderRequestMutation(); const [sendRequest, sendResult] = useSendRequestMutation(); const createOrUpdateRequestAndSend = () => { const senderReq = getReqResult?.data?.senderRequest; createOrUpdateRequest({ variables: { request: { // Update existing sender request if it was cloned from a request log // and it doesn't have a response body yet (e.g. not sent yet). ...(senderReq && senderReq.sourceRequestLogID && !senderReq.response && { id: senderReq.id }), url, method, proto: httpProtoMap.get(proto), headers: headers.filter((kv) => kv.key !== ""), body: body || undefined, }, }, onCompleted: ({ createOrUpdateSenderRequest }) => { const { id } = createOrUpdateSenderRequest; sendRequestAndPushRoute(id); }, }); }; const sendRequestAndPushRoute = (id: string) => { sendRequest({ errorPolicy: "all", onCompleted: () => { router.push(`/sender?id=${id}`); }, variables: { id, }, }); }; const handleFormSubmit: React.FormEventHandler = (e) => { e.preventDefault(); createOrUpdateRequestAndSend(); }; const handleNewRequest = () => { setURL(""); setMethod(defaultMethod); setProto(defaultProto); setQueryParams(emptyKeyPair); setHeaders(emptyKeyPair); setBody(""); setResponse(null); router.push(`/sender`); }; return ( {createResult.error && ( {createResult.error.message} )} {sendResult.error && ( {sendResult.error.message} )} Request ); } export default EditRequest; ================================================ FILE: admin/src/features/sender/components/History.tsx ================================================ import { Box, Paper, Typography } from "@mui/material"; import { useRouter } from "next/router"; import RequestsTable from "lib/components/RequestsTable"; import { useGetSenderRequestsQuery } from "lib/graphql/generated"; function History(): JSX.Element { const { data, loading } = useGetSenderRequestsQuery({ pollInterval: 1000, }); const router = useRouter(); const activeId = router.query.id as string | undefined; const handleRowClick = (id: string) => { router.push(`/sender?id=${id}`); }; return ( {!loading && data?.senderRequests && data?.senderRequests.length > 0 && ( )} {!loading && data?.senderRequests.length === 0 && ( No requests created yet. )} ); } export default History; ================================================ FILE: admin/src/features/sender/components/Sender.tsx ================================================ import { Box } from "@mui/material"; import EditRequest from "./EditRequest"; import History from "./History"; import SplitPane from "lib/components/SplitPane"; export default function Sender(): JSX.Element { return ( ); } ================================================ FILE: admin/src/features/sender/graphql/createOrUpdateRequest.graphql ================================================ mutation CreateOrUpdateSenderRequest($request: SenderRequestInput!) { createOrUpdateSenderRequest(request: $request) { id } } ================================================ FILE: admin/src/features/sender/graphql/createSenderRequestFromRequestLog.graphql ================================================ mutation CreateSenderRequestFromHttpRequestLog($id: ID!) { createSenderRequestFromHttpRequestLog(id: $id) { id } } ================================================ FILE: admin/src/features/sender/graphql/sendRequest.graphql ================================================ mutation SendRequest($id: ID!) { sendRequest(id: $id) { id } } ================================================ FILE: admin/src/features/sender/graphql/senderRequest.graphql ================================================ query GetSenderRequest($id: ID!) { senderRequest(id: $id) { id sourceRequestLogID url method proto headers { key value } body timestamp response { id proto statusCode statusReason body headers { key value } } } } ================================================ FILE: admin/src/features/sender/graphql/senderRequests.graphql ================================================ query GetSenderRequests { senderRequests { id url method response { id statusCode statusReason } } } ================================================ FILE: admin/src/features/sender/index.ts ================================================ import Sender from "./components/Sender"; export default Sender; ================================================ FILE: admin/src/features/settings/components/Settings.tsx ================================================ import { useApolloClient } from "@apollo/client"; import { TabContext, TabPanel } from "@mui/lab"; import TabList from "@mui/lab/TabList"; import { Alert, Box, Button, CircularProgress, FormControl, FormControlLabel, FormHelperText, Snackbar, Switch, Tab, TextField, TextFieldProps, Typography, } from "@mui/material"; import MaterialLink from "@mui/material/Link"; import { SwitchBaseProps } from "@mui/material/internal/SwitchBase"; import { useEffect, useState } from "react"; import { useActiveProject } from "lib/ActiveProjectContext"; import Link from "lib/components/Link"; import { ActiveProjectDocument, useUpdateInterceptSettingsMutation } from "lib/graphql/generated"; import { withoutTypename } from "lib/graphql/omitTypename"; enum TabValue { Intercept = "intercept", } function FilterTextField(props: TextFieldProps): JSX.Element { return ( ); } export default function Settings(): JSX.Element { const client = useApolloClient(); const activeProject = useActiveProject(); const [updateInterceptSettings, updateIntercepSettingsResult] = useUpdateInterceptSettingsMutation({ onCompleted(data) { client.cache.updateQuery({ query: ActiveProjectDocument }, (cachedData) => ({ activeProject: { ...cachedData.activeProject, settings: { ...cachedData.activeProject.settings, intercept: data.updateInterceptSettings, }, }, })); setInterceptReqFilter(data.updateInterceptSettings.requestFilter || ""); setInterceptResFilter(data.updateInterceptSettings.responseFilter || ""); setSettingsUpdatedOpen(true); }, }); const [interceptReqFilter, setInterceptReqFilter] = useState(""); const [interceptResFilter, setInterceptResFilter] = useState(""); useEffect(() => { setInterceptReqFilter(activeProject?.settings.intercept.requestFilter || ""); }, [activeProject?.settings.intercept.requestFilter]); useEffect(() => { setInterceptResFilter(activeProject?.settings.intercept.responseFilter || ""); }, [activeProject?.settings.intercept.responseFilter]); const handleReqInterceptEnabled: SwitchBaseProps["onChange"] = (e, checked) => { if (!activeProject) { e.preventDefault(); return; } updateInterceptSettings({ variables: { input: { ...withoutTypename(activeProject.settings.intercept), requestsEnabled: checked, }, }, }); }; const handleResInterceptEnabled: SwitchBaseProps["onChange"] = (e, checked) => { if (!activeProject) { e.preventDefault(); return; } updateInterceptSettings({ variables: { input: { ...withoutTypename(activeProject.settings.intercept), responsesEnabled: checked, }, }, }); }; const handleInterceptReqFilter = () => { if (!activeProject) { return; } updateInterceptSettings({ variables: { input: { ...withoutTypename(activeProject.settings.intercept), requestFilter: interceptReqFilter, }, }, }); }; const handleInterceptResFilter = () => { if (!activeProject) { return; } updateInterceptSettings({ variables: { input: { ...withoutTypename(activeProject.settings.intercept), responseFilter: interceptResFilter, }, }, }); }; const [tabValue, setTabValue] = useState(TabValue.Intercept); const [settingsUpdatedOpen, setSettingsUpdatedOpen] = useState(false); const handleSettingsUpdatedClose = (_: Event | React.SyntheticEvent, reason?: string) => { if (reason === "clickaway") { return; } setSettingsUpdatedOpen(false); }; const tabSx = { textTransform: "none", }; return ( Intercept settings have been updated. Settings Settings allow you to tweak the behaviour of Hetty’s features. Project settings {!activeProject && ( There is no project active. To configure project settings, first open a project. )} {activeProject && ( <> setTabValue(value)} sx={{ borderBottom: 1, borderColor: "divider" }}> Requests } label="Enable request interception" labelPlacement="start" sx={{ display: "inline-block", m: 0 }} /> When enabled, incoming HTTP requests to the proxy are stalled for{" "} manual review.
setInterceptReqFilter(e.target.value)} /> Filter expression to match incoming requests on. When set, only matching requests are intercepted.{" "} Read docs.
Responses } label="Enable response interception" labelPlacement="start" sx={{ display: "inline-block", m: 0 }} /> When enabled, HTTP responses received by the proxy are stalled for{" "} manual review.
setInterceptResFilter(e.target.value)} /> Filter expression to match received responses on. When set, only matching responses are intercepted.{" "} Read docs.
)}
); } ================================================ FILE: admin/src/features/settings/graphql/updateInterceptSettings.graphql ================================================ mutation UpdateInterceptSettings($input: UpdateInterceptSettingsInput!) { updateInterceptSettings(input: $input) { requestsEnabled responsesEnabled requestFilter responseFilter } } ================================================ FILE: admin/src/lib/ActiveProjectContext.tsx ================================================ import React, { createContext, useContext } from "react"; import { Project, useActiveProjectQuery } from "./graphql/generated"; const ActiveProjectContext = createContext(null); interface Props { children?: React.ReactNode | undefined; } export function ActiveProjectProvider({ children }: Props): JSX.Element { const { data } = useActiveProjectQuery(); const project = data?.activeProject || null; return {children}; } export function useActiveProject() { return useContext(ActiveProjectContext); } ================================================ FILE: admin/src/lib/InterceptedRequestsContext.tsx ================================================ import React, { createContext, useContext } from "react"; import { GetInterceptedRequestsQuery, useGetInterceptedRequestsQuery } from "./graphql/generated"; const InterceptedRequestsContext = createContext(null); interface Props { children?: React.ReactNode | undefined; } export function InterceptedRequestsProvider({ children }: Props): JSX.Element { const { data } = useGetInterceptedRequestsQuery({ pollInterval: 1000, }); const reqs = data?.interceptedRequests || null; return {children}; } export function useInterceptedRequests() { return useContext(InterceptedRequestsContext); } ================================================ FILE: admin/src/lib/components/ConfirmationDialog.tsx ================================================ import Button from "@mui/material/Button"; import Dialog from "@mui/material/Dialog"; import DialogActions from "@mui/material/DialogActions"; import DialogContent from "@mui/material/DialogContent"; import DialogContentText from "@mui/material/DialogContentText"; import DialogTitle from "@mui/material/DialogTitle"; import React, { useState } from "react"; export function useConfirmationDialog() { const [isOpen, setIsOpen] = useState(false); const close = () => setIsOpen(false); const open = () => setIsOpen(true); return { open, close, isOpen }; } interface ConfirmationDialog { isOpen: boolean; onClose: () => void; onConfirm: () => void; children: React.ReactNode; } export function ConfirmationDialog(props: ConfirmationDialog) { const { onClose, onConfirm, isOpen, children } = props; function confirm() { onConfirm(); onClose(); } return ( Are you sure? {children} ); } ================================================ FILE: admin/src/lib/components/Editor.tsx ================================================ import MonacoEditor, { EditorProps } from "@monaco-editor/react"; const defaultMonacoOptions: EditorProps["options"] = { readOnly: true, wordWrap: "on", minimap: { enabled: false, }, }; type language = "html" | "typescript" | "json"; function languageForContentType(contentType?: string): language | undefined { switch (contentType?.toLowerCase()) { case "text/html": case "text/html; charset=utf-8": return "html"; case "application/json": case "application/json; charset=utf-8": return "json"; case "application/javascript": case "application/javascript; charset=utf-8": return "typescript"; default: return; } } interface Props { content: string; contentType?: string; monacoOptions?: EditorProps["options"]; onChange?: EditorProps["onChange"]; } function Editor({ content, contentType, monacoOptions, onChange }: Props): JSX.Element { return ( ); } export default Editor; ================================================ FILE: admin/src/lib/components/HttpStatusIcon.tsx ================================================ import FiberManualRecordIcon from "@mui/icons-material/FiberManualRecord"; import { SvgIconTypeMap } from "@mui/material"; interface Props { status: number; } export default function HttpStatusIcon({ status }: Props): JSX.Element { let color: SvgIconTypeMap["props"]["color"] = "inherit"; switch (Math.floor(status / 100)) { case 2: case 3: color = "primary"; break; case 4: color = "warning"; break; case 5: color = "error"; break; } return ; } ================================================ FILE: admin/src/lib/components/KeyValuePair.tsx ================================================ import ClearIcon from "@mui/icons-material/Clear"; import { Alert, IconButton, InputBase, InputBaseProps, Snackbar, styled, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TableRowProps, } from "@mui/material"; import { useState } from "react"; const StyledInputBase = styled(InputBase)(() => ({ fontSize: "0.875rem", "&.MuiInputBase-root input": { p: 0, }, })); const StyledTableRow = styled(TableRow)(() => ({ "& .delete-button": { visibility: "hidden", }, "&:hover .delete-button": { visibility: "inherit", }, })); export interface KeyValuePair { key: string; value: string; } export interface KeyValuePairTableProps { items: KeyValuePair[]; onChange?: (key: string, value: string, index: number) => void; onDelete?: (index: number) => void; } export function KeyValuePairTable({ items, onChange, onDelete }: KeyValuePairTableProps): JSX.Element { const [copyConfOpen, setCopyConfOpen] = useState(false); const handleCellClick = (e: React.MouseEvent) => { e.preventDefault(); const windowSel = window.getSelection(); if (!windowSel || !document) { return; } const r = document.createRange(); r.selectNode(e.currentTarget); windowSel.removeAllRanges(); windowSel.addRange(r); document.execCommand("copy"); windowSel.removeAllRanges(); setCopyConfOpen(true); }; const handleCopyConfClose = (_: Event | React.SyntheticEvent, reason?: string) => { if (reason === "clickaway") { return; } setCopyConfOpen(false); }; return (
Copied to clipboard. Key Value {onDelete && } {items.map(({ key, value }, idx) => ( { !onChange && handleCellClick(e); }} sx={{ ...(!onChange && { "&:hover": { cursor: "copy", }, }), }} > {!onChange && {key}} {onChange && ( { onChange && onChange(e.target.value, value, idx); }} /> )} { !onChange && handleCellClick(e); }} sx={{ width: "60%", wordBreak: "break-all", ...(!onChange && { "&:hover": { cursor: "copy", }, }), }} > {!onChange && value} {onChange && ( { onChange && onChange(key, e.target.value, idx); }} /> )} {onDelete && (
{ onDelete && onDelete(idx); }} sx={{ visibility: onDelete === undefined || items.length === idx + 1 ? "hidden" : "inherit", }} >
)}
))}
); } export default KeyValuePairTable; ================================================ FILE: admin/src/lib/components/Link.tsx ================================================ import MuiLink, { LinkProps as MuiLinkProps } from "@mui/material/Link"; import { styled } from "@mui/material/styles"; import clsx from "clsx"; import NextLink, { LinkProps as NextLinkProps } from "next/link"; import { useRouter } from "next/router"; import * as React from "react"; // Add support for the sx prop for consistency with the other branches. const Anchor = styled("a")({}); interface NextLinkComposedProps extends Omit, "href">, Omit { to: NextLinkProps["href"]; linkAs?: NextLinkProps["as"]; } export const NextLinkComposed = React.forwardRef(function NextLinkComposed( props, ref ) { const { to, linkAs, replace, scroll, shallow, prefetch, locale, ...other } = props; return ( ); }); export type LinkProps = { activeClassName?: string; as?: NextLinkProps["as"]; href: NextLinkProps["href"]; linkAs?: NextLinkProps["as"]; // Useful when the as prop is shallow by styled(). noLinkStyle?: boolean; } & Omit & Omit; // A styled version of the Next.js Link component: // https://nextjs.org/docs/api-reference/next/link const Link = React.forwardRef(function Link(props, ref) { const { activeClassName = "active", as, className: classNameProps, href, linkAs: linkAsProp, locale, noLinkStyle, prefetch, replace, role, // Link don't have roles. scroll, shallow, ...other } = props; const router = useRouter(); const pathname = typeof href === "string" ? href : href.pathname; const className = clsx(classNameProps, { [activeClassName]: router.pathname === pathname && activeClassName, }); const isExternal = typeof href === "string" && (href.indexOf("http") === 0 || href.indexOf("mailto:") === 0); if (isExternal) { if (noLinkStyle) { return ; } return ; } const linkAs = linkAsProp || as; const nextjsProps = { to: href, linkAs, replace, scroll, shallow, prefetch, locale }; if (noLinkStyle) { return ; } return ; }); export default Link; ================================================ FILE: admin/src/lib/components/RequestTabs.tsx ================================================ import { TabContext, TabList, TabPanel } from "@mui/lab"; import { Box, Tab } from "@mui/material"; import React, { useState } from "react"; import { KeyValuePairTable, KeyValuePair, KeyValuePairTableProps } from "./KeyValuePair"; import Editor from "lib/components/Editor"; enum TabValue { QueryParams = "queryParams", Headers = "headers", Body = "body", } interface RequestTabsProps { queryParams: KeyValuePair[]; headers: KeyValuePair[]; onQueryParamChange?: KeyValuePairTableProps["onChange"]; onQueryParamDelete?: KeyValuePairTableProps["onDelete"]; onHeaderChange?: KeyValuePairTableProps["onChange"]; onHeaderDelete?: KeyValuePairTableProps["onDelete"]; body?: string | null; onBodyChange?: (value: string) => void; } function RequestTabs(props: RequestTabsProps): JSX.Element { const { queryParams, onQueryParamChange, onQueryParamDelete, headers, onHeaderChange, onHeaderDelete, body, onBodyChange, } = props; const [tabValue, setTabValue] = useState(TabValue.QueryParams); const tabSx = { textTransform: "none", }; const queryParamsLength = onQueryParamChange ? queryParams.length - 1 : queryParams.length; const headersLength = onHeaderChange ? headers.length - 1 : headers.length; return ( setTabValue(value)}> 1 ? "s" : "") + ")" : "")} sx={tabSx} /> { onBodyChange && onBodyChange(value || ""); }} monacoOptions={{ readOnly: onBodyChange === undefined }} contentType={headers.find(({ key }) => key.toLowerCase() === "content-type")?.value} /> ); } export default RequestTabs; ================================================ FILE: admin/src/lib/components/RequestsTable.tsx ================================================ import { TableContainer, Table, TableHead, TableRow, TableCell, TableBody, styled, TableCellProps, TableRowProps, } from "@mui/material"; import HttpStatusIcon from "./HttpStatusIcon"; import { HttpMethod } from "lib/graphql/generated"; const baseCellStyle = { whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis", } as const; const MethodTableCell = styled(TableCell)(() => ({ ...baseCellStyle, width: "100px", })); const OriginTableCell = styled(TableCell)(() => ({ ...baseCellStyle, maxWidth: "100px", })); const PathTableCell = styled(TableCell)(() => ({ ...baseCellStyle, maxWidth: "200px", })); const StatusTableCell = styled(TableCell)(() => ({ ...baseCellStyle, width: "100px", })); const RequestTableRow = styled(TableRow)(() => ({ "&:hover": { cursor: "pointer", }, })); interface HttpRequest { id: string; url: string; method: HttpMethod; response?: HttpResponse | null; } interface HttpResponse { statusCode: number; statusReason: string; body?: string; } interface Props { requests: HttpRequest[]; activeRowId?: string; actionsCell?: (id: string) => JSX.Element; onRowClick?: (id: string) => void; onContextMenu?: (e: React.MouseEvent, id: string) => void; } export default function RequestsTable(props: Props): JSX.Element { const { requests, activeRowId, actionsCell, onRowClick, onContextMenu } = props; return ( Method Origin Path Status {actionsCell && } {requests.map(({ id, method, url, response }) => { const { origin, pathname, search, hash } = new URL(url); return ( { onRowClick && onRowClick(id); }} onContextMenu={(e) => { onContextMenu && onContextMenu(e, id); }} > {method} {origin} {decodeURIComponent(pathname + search + hash)} {response && } {actionsCell && actionsCell(id)} ); })}
); } function Status({ code, reason }: { code: number; reason: string }): JSX.Element { return (
{" "} {code} {reason}
); } ================================================ FILE: admin/src/lib/components/Response.tsx ================================================ import { Box, Typography } from "@mui/material"; import ResponseTabs from "./ResponseTabs"; import ResponseStatus from "lib/components/ResponseStatus"; import { HttpResponseLog } from "lib/graphql/generated"; interface ResponseProps { response?: HttpResponseLog | null; } function Response({ response }: ResponseProps): JSX.Element { return ( Response {response && ( )} ); } export default Response; ================================================ FILE: admin/src/lib/components/ResponseStatus.tsx ================================================ import { Typography } from "@mui/material"; import HttpStatusIcon from "./HttpStatusIcon"; import { HttpProtocol } from "lib/graphql/generated"; type ResponseStatusProps = { proto: HttpProtocol; statusCode: number; statusReason: string; }; function mapProto(proto: HttpProtocol): string { switch (proto) { case HttpProtocol.Http10: return "HTTP/1.0"; case HttpProtocol.Http11: return "HTTP/1.1"; case HttpProtocol.Http20: return "HTTP/2.0"; default: return proto; } } export default function ResponseStatus({ proto, statusCode, statusReason }: ResponseStatusProps): JSX.Element { return ( {" "} {mapProto(proto)} {" "} {statusCode} {statusReason} ); } ================================================ FILE: admin/src/lib/components/ResponseTabs.tsx ================================================ import { TabContext, TabList, TabPanel } from "@mui/lab"; import { Box, Paper, Tab, Typography } from "@mui/material"; import React, { useState } from "react"; import { KeyValuePairTable, KeyValuePair, KeyValuePairTableProps } from "./KeyValuePair"; import Editor from "lib/components/Editor"; interface ResponseTabsProps { headers: KeyValuePair[]; onHeaderChange?: KeyValuePairTableProps["onChange"]; onHeaderDelete?: KeyValuePairTableProps["onDelete"]; body?: string | null; onBodyChange?: (value: string) => void; hasResponse: boolean; } enum TabValue { Body = "body", Headers = "headers", } const reqNotSent = ( Response not received yet. ); function ResponseTabs(props: ResponseTabsProps): JSX.Element { const { headers, onHeaderChange, onHeaderDelete, body, onBodyChange, hasResponse } = props; const [tabValue, setTabValue] = useState(TabValue.Body); const contentType = headers.find((header) => header.key.toLowerCase() === "content-type")?.value; const tabSx = { textTransform: "none", }; const headersLength = onHeaderChange ? headers.length - 1 : headers.length; return ( setTabValue(value)}> 1 ? "s" : "") + ")" : "")} sx={tabSx} /> {hasResponse && ( { onBodyChange && onBodyChange(value || ""); }} monacoOptions={{ readOnly: onBodyChange === undefined }} contentType={contentType} /> )} {!hasResponse && reqNotSent} {hasResponse && } {!hasResponse && reqNotSent} ); } export default ResponseTabs; ================================================ FILE: admin/src/lib/components/SplitPane.tsx ================================================ import { alpha, styled } from "@mui/material/styles"; import ReactSplitPane, { SplitPaneProps } from "react-split-pane"; const BORDER_WIDTH_FACTOR = 1.75; const SIZE_FACTOR = 4; const MARGIN_FACTOR = -1.75; const SplitPane = styled(ReactSplitPane)(({ theme }) => ({ ".Resizer": { zIndex: theme.zIndex.mobileStepper, boxSizing: "border-box", backgroundClip: "padding-box", backgroundColor: alpha(theme.palette.grey[400], 0.05), }, ".Resizer:hover": { transition: "all 0.5s ease", backgroundColor: alpha(theme.palette.primary.main, 1), }, ".Resizer.horizontal": { height: theme.spacing(SIZE_FACTOR), marginTop: theme.spacing(MARGIN_FACTOR), marginBottom: theme.spacing(MARGIN_FACTOR), borderTop: `${theme.spacing(BORDER_WIDTH_FACTOR)} solid rgba(255, 255, 255, 0)`, borderBottom: `${theme.spacing(BORDER_WIDTH_FACTOR)} solid rgba(255, 255, 255, 0)`, borderBottomColor: "rgba(255, 255, 255, 0)", cursor: "row-resize", width: "100%", }, ".Resizer.vertical": { width: theme.spacing(SIZE_FACTOR), marginLeft: theme.spacing(MARGIN_FACTOR), marginRight: theme.spacing(MARGIN_FACTOR), borderLeft: `${theme.spacing(BORDER_WIDTH_FACTOR)} solid rgba(255, 255, 255, 0)`, borderRight: `${theme.spacing(BORDER_WIDTH_FACTOR)} solid rgba(255, 255, 255, 0)`, cursor: "col-resize", }, ".Resizer.disabled": { cursor: "not-allowed", }, ".Resizer.disabled:hover": { borderColor: "transparent", }, ".Pane": { overflow: "hidden", }, })); export default SplitPane; ================================================ FILE: admin/src/lib/components/UrlBar.tsx ================================================ import { Box, BoxProps, FormControl, InputLabel, MenuItem, Select, TextField } from "@mui/material"; import { HttpProtocol } from "lib/graphql/generated"; export enum HttpMethod { Get = "GET", Post = "POST", Put = "PUT", Patch = "PATCH", Delete = "DELETE", Head = "HEAD", Options = "OPTIONS", Connect = "CONNECT", Trace = "TRACE", } export enum HttpProto { Http10 = "HTTP/1.0", Http11 = "HTTP/1.1", Http20 = "HTTP/2.0", } export const httpProtoMap = new Map([ [HttpProto.Http10, HttpProtocol.Http10], [HttpProto.Http11, HttpProtocol.Http11], [HttpProto.Http20, HttpProtocol.Http20], ]); interface UrlBarProps extends BoxProps { method: HttpMethod; onMethodChange?: (method: HttpMethod) => void; url: string; onUrlChange?: (url: string) => void; proto: HttpProto; onProtoChange?: (proto: HttpProto) => void; } function UrlBar(props: UrlBarProps) { const { method, onMethodChange, url, onUrlChange, proto, onProtoChange, ...other } = props; return ( Method onUrlChange && onUrlChange(e.target.value)} required variant="outlined" InputLabelProps={{ shrink: true, }} InputProps={{ sx: { ".MuiOutlinedInput-notchedOutline": { borderRadius: 0, }, }, }} sx={{ flexGrow: 1 }} /> Protocol ); } export default UrlBar; ================================================ FILE: admin/src/lib/components/useContextMenu.tsx ================================================ import { Menu } from "@mui/material"; import React, { useState } from "react"; interface ContextMenuProps { children?: React.ReactNode; } export default function useContextMenu(): [ (props: ContextMenuProps) => JSX.Element, (e: React.MouseEvent) => void, () => void ] { const [contextMenu, setContextMenu] = useState<{ mouseX: number; mouseY: number; } | null>(null); const handleContextMenu = (event: React.MouseEvent) => { event.preventDefault(); setContextMenu( contextMenu === null ? { mouseX: event.clientX - 2, mouseY: event.clientY - 4, } : // repeated contextmenu when it is already open closes it with Chrome 84 on Ubuntu // Other native context menus might behave different. // With this behavior we prevent contextmenu from the backdrop to re-locale existing context menus. null ); }; const handleClose = () => { setContextMenu(null); }; const menu = ({ children }: ContextMenuProps): JSX.Element => ( {children} ); return [menu, handleContextMenu, handleClose]; } ================================================ FILE: admin/src/lib/graphql/generated.tsx ================================================ import { gql } from '@apollo/client'; import * as Apollo from '@apollo/client'; export type Maybe = T | null; export type InputMaybe = Maybe; export type Exact = { [K in keyof T]: T[K] }; export type MakeOptional = Omit & { [SubKey in K]?: Maybe }; export type MakeMaybe = Omit & { [SubKey in K]: Maybe }; const defaultOptions = {} as const; /** All built-in and custom scalars, mapped to their actual values */ export type Scalars = { ID: string; String: string; Boolean: boolean; Int: number; Float: number; Regexp: any; Time: any; URL: any; }; export type CancelRequestResult = { __typename?: 'CancelRequestResult'; success: Scalars['Boolean']; }; export type CancelResponseResult = { __typename?: 'CancelResponseResult'; success: Scalars['Boolean']; }; export type ClearHttpRequestLogResult = { __typename?: 'ClearHTTPRequestLogResult'; success: Scalars['Boolean']; }; export type CloseProjectResult = { __typename?: 'CloseProjectResult'; success: Scalars['Boolean']; }; export type DeleteProjectResult = { __typename?: 'DeleteProjectResult'; success: Scalars['Boolean']; }; export type DeleteSenderRequestsResult = { __typename?: 'DeleteSenderRequestsResult'; success: Scalars['Boolean']; }; export type HttpHeader = { __typename?: 'HttpHeader'; key: Scalars['String']; value: Scalars['String']; }; export type HttpHeaderInput = { key: Scalars['String']; value: Scalars['String']; }; export enum HttpMethod { Connect = 'CONNECT', Delete = 'DELETE', Get = 'GET', Head = 'HEAD', Options = 'OPTIONS', Patch = 'PATCH', Post = 'POST', Put = 'PUT', Trace = 'TRACE' } export enum HttpProtocol { Http10 = 'HTTP10', Http11 = 'HTTP11', Http20 = 'HTTP20' } export type HttpRequest = { __typename?: 'HttpRequest'; body?: Maybe; headers: Array; id: Scalars['ID']; method: HttpMethod; proto: HttpProtocol; response?: Maybe; url: Scalars['URL']; }; export type HttpRequestLog = { __typename?: 'HttpRequestLog'; body?: Maybe; headers: Array; id: Scalars['ID']; method: HttpMethod; proto: Scalars['String']; response?: Maybe; timestamp: Scalars['Time']; url: Scalars['String']; }; export type HttpRequestLogFilter = { __typename?: 'HttpRequestLogFilter'; onlyInScope: Scalars['Boolean']; searchExpression?: Maybe; }; export type HttpRequestLogFilterInput = { onlyInScope?: InputMaybe; searchExpression?: InputMaybe; }; export type HttpResponse = { __typename?: 'HttpResponse'; body?: Maybe; headers: Array; /** Will be the same ID as its related request ID. */ id: Scalars['ID']; proto: HttpProtocol; statusCode: Scalars['Int']; statusReason: Scalars['String']; }; export type HttpResponseLog = { __typename?: 'HttpResponseLog'; body?: Maybe; headers: Array; /** Will be the same ID as its related request ID. */ id: Scalars['ID']; proto: HttpProtocol; statusCode: Scalars['Int']; statusReason: Scalars['String']; }; export type InterceptSettings = { __typename?: 'InterceptSettings'; requestFilter?: Maybe; requestsEnabled: Scalars['Boolean']; responseFilter?: Maybe; responsesEnabled: Scalars['Boolean']; }; export type ModifyRequestInput = { body?: InputMaybe; headers?: InputMaybe>; id: Scalars['ID']; method: HttpMethod; modifyResponse?: InputMaybe; proto: HttpProtocol; url: Scalars['URL']; }; export type ModifyRequestResult = { __typename?: 'ModifyRequestResult'; success: Scalars['Boolean']; }; export type ModifyResponseInput = { body?: InputMaybe; headers?: InputMaybe>; proto: HttpProtocol; requestID: Scalars['ID']; statusCode: Scalars['Int']; statusReason: Scalars['String']; }; export type ModifyResponseResult = { __typename?: 'ModifyResponseResult'; success: Scalars['Boolean']; }; export type Mutation = { __typename?: 'Mutation'; cancelRequest: CancelRequestResult; cancelResponse: CancelResponseResult; clearHTTPRequestLog: ClearHttpRequestLogResult; closeProject: CloseProjectResult; createOrUpdateSenderRequest: SenderRequest; createProject?: Maybe; createSenderRequestFromHttpRequestLog: SenderRequest; deleteProject: DeleteProjectResult; deleteSenderRequests: DeleteSenderRequestsResult; modifyRequest: ModifyRequestResult; modifyResponse: ModifyResponseResult; openProject?: Maybe; sendRequest: SenderRequest; setHttpRequestLogFilter?: Maybe; setScope: Array; setSenderRequestFilter?: Maybe; updateInterceptSettings: InterceptSettings; }; export type MutationCancelRequestArgs = { id: Scalars['ID']; }; export type MutationCancelResponseArgs = { requestID: Scalars['ID']; }; export type MutationCreateOrUpdateSenderRequestArgs = { request: SenderRequestInput; }; export type MutationCreateProjectArgs = { name: Scalars['String']; }; export type MutationCreateSenderRequestFromHttpRequestLogArgs = { id: Scalars['ID']; }; export type MutationDeleteProjectArgs = { id: Scalars['ID']; }; export type MutationModifyRequestArgs = { request: ModifyRequestInput; }; export type MutationModifyResponseArgs = { response: ModifyResponseInput; }; export type MutationOpenProjectArgs = { id: Scalars['ID']; }; export type MutationSendRequestArgs = { id: Scalars['ID']; }; export type MutationSetHttpRequestLogFilterArgs = { filter?: InputMaybe; }; export type MutationSetScopeArgs = { scope: Array; }; export type MutationSetSenderRequestFilterArgs = { filter?: InputMaybe; }; export type MutationUpdateInterceptSettingsArgs = { input: UpdateInterceptSettingsInput; }; export type Project = { __typename?: 'Project'; id: Scalars['ID']; isActive: Scalars['Boolean']; name: Scalars['String']; settings: ProjectSettings; }; export type ProjectSettings = { __typename?: 'ProjectSettings'; intercept: InterceptSettings; }; export type Query = { __typename?: 'Query'; activeProject?: Maybe; httpRequestLog?: Maybe; httpRequestLogFilter?: Maybe; httpRequestLogs: Array; interceptedRequest?: Maybe; interceptedRequests: Array; projects: Array; scope: Array; senderRequest?: Maybe; senderRequests: Array; }; export type QueryHttpRequestLogArgs = { id: Scalars['ID']; }; export type QueryInterceptedRequestArgs = { id: Scalars['ID']; }; export type QuerySenderRequestArgs = { id: Scalars['ID']; }; export type ScopeHeader = { __typename?: 'ScopeHeader'; key?: Maybe; value?: Maybe; }; export type ScopeHeaderInput = { key?: InputMaybe; value?: InputMaybe; }; export type ScopeRule = { __typename?: 'ScopeRule'; body?: Maybe; header?: Maybe; url?: Maybe; }; export type ScopeRuleInput = { body?: InputMaybe; header?: InputMaybe; url?: InputMaybe; }; export type SenderRequest = { __typename?: 'SenderRequest'; body?: Maybe; headers?: Maybe>; id: Scalars['ID']; method: HttpMethod; proto: HttpProtocol; response?: Maybe; sourceRequestLogID?: Maybe; timestamp: Scalars['Time']; url: Scalars['URL']; }; export type SenderRequestFilter = { __typename?: 'SenderRequestFilter'; onlyInScope: Scalars['Boolean']; searchExpression?: Maybe; }; export type SenderRequestFilterInput = { onlyInScope?: InputMaybe; searchExpression?: InputMaybe; }; export type SenderRequestInput = { body?: InputMaybe; headers?: InputMaybe>; id?: InputMaybe; method?: InputMaybe; proto?: InputMaybe; url: Scalars['URL']; }; export type UpdateInterceptSettingsInput = { requestFilter?: InputMaybe; requestsEnabled: Scalars['Boolean']; responseFilter?: InputMaybe; responsesEnabled: Scalars['Boolean']; }; export type CancelRequestMutationVariables = Exact<{ id: Scalars['ID']; }>; export type CancelRequestMutation = { __typename?: 'Mutation', cancelRequest: { __typename?: 'CancelRequestResult', success: boolean } }; export type CancelResponseMutationVariables = Exact<{ requestID: Scalars['ID']; }>; export type CancelResponseMutation = { __typename?: 'Mutation', cancelResponse: { __typename?: 'CancelResponseResult', success: boolean } }; export type GetInterceptedRequestQueryVariables = Exact<{ id: Scalars['ID']; }>; export type GetInterceptedRequestQuery = { __typename?: 'Query', interceptedRequest?: { __typename?: 'HttpRequest', id: string, url: any, method: HttpMethod, proto: HttpProtocol, body?: string | null, headers: Array<{ __typename?: 'HttpHeader', key: string, value: string }>, response?: { __typename?: 'HttpResponse', id: string, proto: HttpProtocol, statusCode: number, statusReason: string, body?: string | null, headers: Array<{ __typename?: 'HttpHeader', key: string, value: string }> } | null } | null }; export type ModifyRequestMutationVariables = Exact<{ request: ModifyRequestInput; }>; export type ModifyRequestMutation = { __typename?: 'Mutation', modifyRequest: { __typename?: 'ModifyRequestResult', success: boolean } }; export type ModifyResponseMutationVariables = Exact<{ response: ModifyResponseInput; }>; export type ModifyResponseMutation = { __typename?: 'Mutation', modifyResponse: { __typename?: 'ModifyResponseResult', success: boolean } }; export type ActiveProjectQueryVariables = Exact<{ [key: string]: never; }>; export type ActiveProjectQuery = { __typename?: 'Query', activeProject?: { __typename?: 'Project', id: string, name: string, isActive: boolean, settings: { __typename?: 'ProjectSettings', intercept: { __typename?: 'InterceptSettings', requestsEnabled: boolean, responsesEnabled: boolean, requestFilter?: string | null, responseFilter?: string | null } } } | null }; export type CloseProjectMutationVariables = Exact<{ [key: string]: never; }>; export type CloseProjectMutation = { __typename?: 'Mutation', closeProject: { __typename?: 'CloseProjectResult', success: boolean } }; export type CreateProjectMutationVariables = Exact<{ name: Scalars['String']; }>; export type CreateProjectMutation = { __typename?: 'Mutation', createProject?: { __typename?: 'Project', id: string, name: string } | null }; export type DeleteProjectMutationVariables = Exact<{ id: Scalars['ID']; }>; export type DeleteProjectMutation = { __typename?: 'Mutation', deleteProject: { __typename?: 'DeleteProjectResult', success: boolean } }; export type OpenProjectMutationVariables = Exact<{ id: Scalars['ID']; }>; export type OpenProjectMutation = { __typename?: 'Mutation', openProject?: { __typename?: 'Project', id: string, name: string, isActive: boolean } | null }; export type ProjectsQueryVariables = Exact<{ [key: string]: never; }>; export type ProjectsQuery = { __typename?: 'Query', projects: Array<{ __typename?: 'Project', id: string, name: string, isActive: boolean }> }; export type ClearHttpRequestLogMutationVariables = Exact<{ [key: string]: never; }>; export type ClearHttpRequestLogMutation = { __typename?: 'Mutation', clearHTTPRequestLog: { __typename?: 'ClearHTTPRequestLogResult', success: boolean } }; export type HttpRequestLogQueryVariables = Exact<{ id: Scalars['ID']; }>; export type HttpRequestLogQuery = { __typename?: 'Query', httpRequestLog?: { __typename?: 'HttpRequestLog', id: string, method: HttpMethod, url: string, proto: string, body?: string | null, headers: Array<{ __typename?: 'HttpHeader', key: string, value: string }>, response?: { __typename?: 'HttpResponseLog', id: string, proto: HttpProtocol, statusCode: number, statusReason: string, body?: string | null, headers: Array<{ __typename?: 'HttpHeader', key: string, value: string }> } | null } | null }; export type HttpRequestLogFilterQueryVariables = Exact<{ [key: string]: never; }>; export type HttpRequestLogFilterQuery = { __typename?: 'Query', httpRequestLogFilter?: { __typename?: 'HttpRequestLogFilter', onlyInScope: boolean, searchExpression?: string | null } | null }; export type HttpRequestLogsQueryVariables = Exact<{ [key: string]: never; }>; export type HttpRequestLogsQuery = { __typename?: 'Query', httpRequestLogs: Array<{ __typename?: 'HttpRequestLog', id: string, method: HttpMethod, url: string, timestamp: any, response?: { __typename?: 'HttpResponseLog', statusCode: number, statusReason: string } | null }> }; export type SetHttpRequestLogFilterMutationVariables = Exact<{ filter?: InputMaybe; }>; export type SetHttpRequestLogFilterMutation = { __typename?: 'Mutation', setHttpRequestLogFilter?: { __typename?: 'HttpRequestLogFilter', onlyInScope: boolean, searchExpression?: string | null } | null }; export type ScopeQueryVariables = Exact<{ [key: string]: never; }>; export type ScopeQuery = { __typename?: 'Query', scope: Array<{ __typename?: 'ScopeRule', url?: any | null }> }; export type SetScopeMutationVariables = Exact<{ scope: Array | ScopeRuleInput; }>; export type SetScopeMutation = { __typename?: 'Mutation', setScope: Array<{ __typename?: 'ScopeRule', url?: any | null }> }; export type CreateOrUpdateSenderRequestMutationVariables = Exact<{ request: SenderRequestInput; }>; export type CreateOrUpdateSenderRequestMutation = { __typename?: 'Mutation', createOrUpdateSenderRequest: { __typename?: 'SenderRequest', id: string } }; export type CreateSenderRequestFromHttpRequestLogMutationVariables = Exact<{ id: Scalars['ID']; }>; export type CreateSenderRequestFromHttpRequestLogMutation = { __typename?: 'Mutation', createSenderRequestFromHttpRequestLog: { __typename?: 'SenderRequest', id: string } }; export type SendRequestMutationVariables = Exact<{ id: Scalars['ID']; }>; export type SendRequestMutation = { __typename?: 'Mutation', sendRequest: { __typename?: 'SenderRequest', id: string } }; export type GetSenderRequestQueryVariables = Exact<{ id: Scalars['ID']; }>; export type GetSenderRequestQuery = { __typename?: 'Query', senderRequest?: { __typename?: 'SenderRequest', id: string, sourceRequestLogID?: string | null, url: any, method: HttpMethod, proto: HttpProtocol, body?: string | null, timestamp: any, headers?: Array<{ __typename?: 'HttpHeader', key: string, value: string }> | null, response?: { __typename?: 'HttpResponseLog', id: string, proto: HttpProtocol, statusCode: number, statusReason: string, body?: string | null, headers: Array<{ __typename?: 'HttpHeader', key: string, value: string }> } | null } | null }; export type GetSenderRequestsQueryVariables = Exact<{ [key: string]: never; }>; export type GetSenderRequestsQuery = { __typename?: 'Query', senderRequests: Array<{ __typename?: 'SenderRequest', id: string, url: any, method: HttpMethod, response?: { __typename?: 'HttpResponseLog', id: string, statusCode: number, statusReason: string } | null }> }; export type UpdateInterceptSettingsMutationVariables = Exact<{ input: UpdateInterceptSettingsInput; }>; export type UpdateInterceptSettingsMutation = { __typename?: 'Mutation', updateInterceptSettings: { __typename?: 'InterceptSettings', requestsEnabled: boolean, responsesEnabled: boolean, requestFilter?: string | null, responseFilter?: string | null } }; export type GetInterceptedRequestsQueryVariables = Exact<{ [key: string]: never; }>; export type GetInterceptedRequestsQuery = { __typename?: 'Query', interceptedRequests: Array<{ __typename?: 'HttpRequest', id: string, url: any, method: HttpMethod, response?: { __typename?: 'HttpResponse', statusCode: number, statusReason: string } | null }> }; export const CancelRequestDocument = gql` mutation CancelRequest($id: ID!) { cancelRequest(id: $id) { success } } `; export type CancelRequestMutationFn = Apollo.MutationFunction; /** * __useCancelRequestMutation__ * * To run a mutation, you first call `useCancelRequestMutation` within a React component and pass it any options that fit your needs. * When your component renders, `useCancelRequestMutation` returns a tuple that includes: * - A mutate function that you can call at any time to execute the mutation * - An object with fields that represent the current status of the mutation's execution * * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; * * @example * const [cancelRequestMutation, { data, loading, error }] = useCancelRequestMutation({ * variables: { * id: // value for 'id' * }, * }); */ export function useCancelRequestMutation(baseOptions?: Apollo.MutationHookOptions) { const options = {...defaultOptions, ...baseOptions} return Apollo.useMutation(CancelRequestDocument, options); } export type CancelRequestMutationHookResult = ReturnType; export type CancelRequestMutationResult = Apollo.MutationResult; export type CancelRequestMutationOptions = Apollo.BaseMutationOptions; export const CancelResponseDocument = gql` mutation CancelResponse($requestID: ID!) { cancelResponse(requestID: $requestID) { success } } `; export type CancelResponseMutationFn = Apollo.MutationFunction; /** * __useCancelResponseMutation__ * * To run a mutation, you first call `useCancelResponseMutation` within a React component and pass it any options that fit your needs. * When your component renders, `useCancelResponseMutation` returns a tuple that includes: * - A mutate function that you can call at any time to execute the mutation * - An object with fields that represent the current status of the mutation's execution * * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; * * @example * const [cancelResponseMutation, { data, loading, error }] = useCancelResponseMutation({ * variables: { * requestID: // value for 'requestID' * }, * }); */ export function useCancelResponseMutation(baseOptions?: Apollo.MutationHookOptions) { const options = {...defaultOptions, ...baseOptions} return Apollo.useMutation(CancelResponseDocument, options); } export type CancelResponseMutationHookResult = ReturnType; export type CancelResponseMutationResult = Apollo.MutationResult; export type CancelResponseMutationOptions = Apollo.BaseMutationOptions; export const GetInterceptedRequestDocument = gql` query GetInterceptedRequest($id: ID!) { interceptedRequest(id: $id) { id url method proto headers { key value } body response { id proto statusCode statusReason headers { key value } body } } } `; /** * __useGetInterceptedRequestQuery__ * * To run a query within a React component, call `useGetInterceptedRequestQuery` and pass it any options that fit your needs. * When your component renders, `useGetInterceptedRequestQuery` returns an object from Apollo Client that contains loading, error, and data properties * you can use to render your UI. * * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; * * @example * const { data, loading, error } = useGetInterceptedRequestQuery({ * variables: { * id: // value for 'id' * }, * }); */ export function useGetInterceptedRequestQuery(baseOptions: Apollo.QueryHookOptions) { const options = {...defaultOptions, ...baseOptions} return Apollo.useQuery(GetInterceptedRequestDocument, options); } export function useGetInterceptedRequestLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { const options = {...defaultOptions, ...baseOptions} return Apollo.useLazyQuery(GetInterceptedRequestDocument, options); } export type GetInterceptedRequestQueryHookResult = ReturnType; export type GetInterceptedRequestLazyQueryHookResult = ReturnType; export type GetInterceptedRequestQueryResult = Apollo.QueryResult; export const ModifyRequestDocument = gql` mutation ModifyRequest($request: ModifyRequestInput!) { modifyRequest(request: $request) { success } } `; export type ModifyRequestMutationFn = Apollo.MutationFunction; /** * __useModifyRequestMutation__ * * To run a mutation, you first call `useModifyRequestMutation` within a React component and pass it any options that fit your needs. * When your component renders, `useModifyRequestMutation` returns a tuple that includes: * - A mutate function that you can call at any time to execute the mutation * - An object with fields that represent the current status of the mutation's execution * * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; * * @example * const [modifyRequestMutation, { data, loading, error }] = useModifyRequestMutation({ * variables: { * request: // value for 'request' * }, * }); */ export function useModifyRequestMutation(baseOptions?: Apollo.MutationHookOptions) { const options = {...defaultOptions, ...baseOptions} return Apollo.useMutation(ModifyRequestDocument, options); } export type ModifyRequestMutationHookResult = ReturnType; export type ModifyRequestMutationResult = Apollo.MutationResult; export type ModifyRequestMutationOptions = Apollo.BaseMutationOptions; export const ModifyResponseDocument = gql` mutation ModifyResponse($response: ModifyResponseInput!) { modifyResponse(response: $response) { success } } `; export type ModifyResponseMutationFn = Apollo.MutationFunction; /** * __useModifyResponseMutation__ * * To run a mutation, you first call `useModifyResponseMutation` within a React component and pass it any options that fit your needs. * When your component renders, `useModifyResponseMutation` returns a tuple that includes: * - A mutate function that you can call at any time to execute the mutation * - An object with fields that represent the current status of the mutation's execution * * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; * * @example * const [modifyResponseMutation, { data, loading, error }] = useModifyResponseMutation({ * variables: { * response: // value for 'response' * }, * }); */ export function useModifyResponseMutation(baseOptions?: Apollo.MutationHookOptions) { const options = {...defaultOptions, ...baseOptions} return Apollo.useMutation(ModifyResponseDocument, options); } export type ModifyResponseMutationHookResult = ReturnType; export type ModifyResponseMutationResult = Apollo.MutationResult; export type ModifyResponseMutationOptions = Apollo.BaseMutationOptions; export const ActiveProjectDocument = gql` query ActiveProject { activeProject { id name isActive settings { intercept { requestsEnabled responsesEnabled requestFilter responseFilter } } } } `; /** * __useActiveProjectQuery__ * * To run a query within a React component, call `useActiveProjectQuery` and pass it any options that fit your needs. * When your component renders, `useActiveProjectQuery` returns an object from Apollo Client that contains loading, error, and data properties * you can use to render your UI. * * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; * * @example * const { data, loading, error } = useActiveProjectQuery({ * variables: { * }, * }); */ export function useActiveProjectQuery(baseOptions?: Apollo.QueryHookOptions) { const options = {...defaultOptions, ...baseOptions} return Apollo.useQuery(ActiveProjectDocument, options); } export function useActiveProjectLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { const options = {...defaultOptions, ...baseOptions} return Apollo.useLazyQuery(ActiveProjectDocument, options); } export type ActiveProjectQueryHookResult = ReturnType; export type ActiveProjectLazyQueryHookResult = ReturnType; export type ActiveProjectQueryResult = Apollo.QueryResult; export const CloseProjectDocument = gql` mutation CloseProject { closeProject { success } } `; export type CloseProjectMutationFn = Apollo.MutationFunction; /** * __useCloseProjectMutation__ * * To run a mutation, you first call `useCloseProjectMutation` within a React component and pass it any options that fit your needs. * When your component renders, `useCloseProjectMutation` returns a tuple that includes: * - A mutate function that you can call at any time to execute the mutation * - An object with fields that represent the current status of the mutation's execution * * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; * * @example * const [closeProjectMutation, { data, loading, error }] = useCloseProjectMutation({ * variables: { * }, * }); */ export function useCloseProjectMutation(baseOptions?: Apollo.MutationHookOptions) { const options = {...defaultOptions, ...baseOptions} return Apollo.useMutation(CloseProjectDocument, options); } export type CloseProjectMutationHookResult = ReturnType; export type CloseProjectMutationResult = Apollo.MutationResult; export type CloseProjectMutationOptions = Apollo.BaseMutationOptions; export const CreateProjectDocument = gql` mutation CreateProject($name: String!) { createProject(name: $name) { id name } } `; export type CreateProjectMutationFn = Apollo.MutationFunction; /** * __useCreateProjectMutation__ * * To run a mutation, you first call `useCreateProjectMutation` within a React component and pass it any options that fit your needs. * When your component renders, `useCreateProjectMutation` returns a tuple that includes: * - A mutate function that you can call at any time to execute the mutation * - An object with fields that represent the current status of the mutation's execution * * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; * * @example * const [createProjectMutation, { data, loading, error }] = useCreateProjectMutation({ * variables: { * name: // value for 'name' * }, * }); */ export function useCreateProjectMutation(baseOptions?: Apollo.MutationHookOptions) { const options = {...defaultOptions, ...baseOptions} return Apollo.useMutation(CreateProjectDocument, options); } export type CreateProjectMutationHookResult = ReturnType; export type CreateProjectMutationResult = Apollo.MutationResult; export type CreateProjectMutationOptions = Apollo.BaseMutationOptions; export const DeleteProjectDocument = gql` mutation DeleteProject($id: ID!) { deleteProject(id: $id) { success } } `; export type DeleteProjectMutationFn = Apollo.MutationFunction; /** * __useDeleteProjectMutation__ * * To run a mutation, you first call `useDeleteProjectMutation` within a React component and pass it any options that fit your needs. * When your component renders, `useDeleteProjectMutation` returns a tuple that includes: * - A mutate function that you can call at any time to execute the mutation * - An object with fields that represent the current status of the mutation's execution * * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; * * @example * const [deleteProjectMutation, { data, loading, error }] = useDeleteProjectMutation({ * variables: { * id: // value for 'id' * }, * }); */ export function useDeleteProjectMutation(baseOptions?: Apollo.MutationHookOptions) { const options = {...defaultOptions, ...baseOptions} return Apollo.useMutation(DeleteProjectDocument, options); } export type DeleteProjectMutationHookResult = ReturnType; export type DeleteProjectMutationResult = Apollo.MutationResult; export type DeleteProjectMutationOptions = Apollo.BaseMutationOptions; export const OpenProjectDocument = gql` mutation OpenProject($id: ID!) { openProject(id: $id) { id name isActive } } `; export type OpenProjectMutationFn = Apollo.MutationFunction; /** * __useOpenProjectMutation__ * * To run a mutation, you first call `useOpenProjectMutation` within a React component and pass it any options that fit your needs. * When your component renders, `useOpenProjectMutation` returns a tuple that includes: * - A mutate function that you can call at any time to execute the mutation * - An object with fields that represent the current status of the mutation's execution * * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; * * @example * const [openProjectMutation, { data, loading, error }] = useOpenProjectMutation({ * variables: { * id: // value for 'id' * }, * }); */ export function useOpenProjectMutation(baseOptions?: Apollo.MutationHookOptions) { const options = {...defaultOptions, ...baseOptions} return Apollo.useMutation(OpenProjectDocument, options); } export type OpenProjectMutationHookResult = ReturnType; export type OpenProjectMutationResult = Apollo.MutationResult; export type OpenProjectMutationOptions = Apollo.BaseMutationOptions; export const ProjectsDocument = gql` query Projects { projects { id name isActive } } `; /** * __useProjectsQuery__ * * To run a query within a React component, call `useProjectsQuery` and pass it any options that fit your needs. * When your component renders, `useProjectsQuery` returns an object from Apollo Client that contains loading, error, and data properties * you can use to render your UI. * * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; * * @example * const { data, loading, error } = useProjectsQuery({ * variables: { * }, * }); */ export function useProjectsQuery(baseOptions?: Apollo.QueryHookOptions) { const options = {...defaultOptions, ...baseOptions} return Apollo.useQuery(ProjectsDocument, options); } export function useProjectsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { const options = {...defaultOptions, ...baseOptions} return Apollo.useLazyQuery(ProjectsDocument, options); } export type ProjectsQueryHookResult = ReturnType; export type ProjectsLazyQueryHookResult = ReturnType; export type ProjectsQueryResult = Apollo.QueryResult; export const ClearHttpRequestLogDocument = gql` mutation ClearHTTPRequestLog { clearHTTPRequestLog { success } } `; export type ClearHttpRequestLogMutationFn = Apollo.MutationFunction; /** * __useClearHttpRequestLogMutation__ * * To run a mutation, you first call `useClearHttpRequestLogMutation` within a React component and pass it any options that fit your needs. * When your component renders, `useClearHttpRequestLogMutation` returns a tuple that includes: * - A mutate function that you can call at any time to execute the mutation * - An object with fields that represent the current status of the mutation's execution * * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; * * @example * const [clearHttpRequestLogMutation, { data, loading, error }] = useClearHttpRequestLogMutation({ * variables: { * }, * }); */ export function useClearHttpRequestLogMutation(baseOptions?: Apollo.MutationHookOptions) { const options = {...defaultOptions, ...baseOptions} return Apollo.useMutation(ClearHttpRequestLogDocument, options); } export type ClearHttpRequestLogMutationHookResult = ReturnType; export type ClearHttpRequestLogMutationResult = Apollo.MutationResult; export type ClearHttpRequestLogMutationOptions = Apollo.BaseMutationOptions; export const HttpRequestLogDocument = gql` query HttpRequestLog($id: ID!) { httpRequestLog(id: $id) { id method url proto headers { key value } body response { id proto headers { key value } statusCode statusReason body } } } `; /** * __useHttpRequestLogQuery__ * * To run a query within a React component, call `useHttpRequestLogQuery` and pass it any options that fit your needs. * When your component renders, `useHttpRequestLogQuery` returns an object from Apollo Client that contains loading, error, and data properties * you can use to render your UI. * * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; * * @example * const { data, loading, error } = useHttpRequestLogQuery({ * variables: { * id: // value for 'id' * }, * }); */ export function useHttpRequestLogQuery(baseOptions: Apollo.QueryHookOptions) { const options = {...defaultOptions, ...baseOptions} return Apollo.useQuery(HttpRequestLogDocument, options); } export function useHttpRequestLogLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { const options = {...defaultOptions, ...baseOptions} return Apollo.useLazyQuery(HttpRequestLogDocument, options); } export type HttpRequestLogQueryHookResult = ReturnType; export type HttpRequestLogLazyQueryHookResult = ReturnType; export type HttpRequestLogQueryResult = Apollo.QueryResult; export const HttpRequestLogFilterDocument = gql` query HttpRequestLogFilter { httpRequestLogFilter { onlyInScope searchExpression } } `; /** * __useHttpRequestLogFilterQuery__ * * To run a query within a React component, call `useHttpRequestLogFilterQuery` and pass it any options that fit your needs. * When your component renders, `useHttpRequestLogFilterQuery` returns an object from Apollo Client that contains loading, error, and data properties * you can use to render your UI. * * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; * * @example * const { data, loading, error } = useHttpRequestLogFilterQuery({ * variables: { * }, * }); */ export function useHttpRequestLogFilterQuery(baseOptions?: Apollo.QueryHookOptions) { const options = {...defaultOptions, ...baseOptions} return Apollo.useQuery(HttpRequestLogFilterDocument, options); } export function useHttpRequestLogFilterLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { const options = {...defaultOptions, ...baseOptions} return Apollo.useLazyQuery(HttpRequestLogFilterDocument, options); } export type HttpRequestLogFilterQueryHookResult = ReturnType; export type HttpRequestLogFilterLazyQueryHookResult = ReturnType; export type HttpRequestLogFilterQueryResult = Apollo.QueryResult; export const HttpRequestLogsDocument = gql` query HttpRequestLogs { httpRequestLogs { id method url timestamp response { statusCode statusReason } } } `; /** * __useHttpRequestLogsQuery__ * * To run a query within a React component, call `useHttpRequestLogsQuery` and pass it any options that fit your needs. * When your component renders, `useHttpRequestLogsQuery` returns an object from Apollo Client that contains loading, error, and data properties * you can use to render your UI. * * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; * * @example * const { data, loading, error } = useHttpRequestLogsQuery({ * variables: { * }, * }); */ export function useHttpRequestLogsQuery(baseOptions?: Apollo.QueryHookOptions) { const options = {...defaultOptions, ...baseOptions} return Apollo.useQuery(HttpRequestLogsDocument, options); } export function useHttpRequestLogsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { const options = {...defaultOptions, ...baseOptions} return Apollo.useLazyQuery(HttpRequestLogsDocument, options); } export type HttpRequestLogsQueryHookResult = ReturnType; export type HttpRequestLogsLazyQueryHookResult = ReturnType; export type HttpRequestLogsQueryResult = Apollo.QueryResult; export const SetHttpRequestLogFilterDocument = gql` mutation SetHttpRequestLogFilter($filter: HttpRequestLogFilterInput) { setHttpRequestLogFilter(filter: $filter) { onlyInScope searchExpression } } `; export type SetHttpRequestLogFilterMutationFn = Apollo.MutationFunction; /** * __useSetHttpRequestLogFilterMutation__ * * To run a mutation, you first call `useSetHttpRequestLogFilterMutation` within a React component and pass it any options that fit your needs. * When your component renders, `useSetHttpRequestLogFilterMutation` returns a tuple that includes: * - A mutate function that you can call at any time to execute the mutation * - An object with fields that represent the current status of the mutation's execution * * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; * * @example * const [setHttpRequestLogFilterMutation, { data, loading, error }] = useSetHttpRequestLogFilterMutation({ * variables: { * filter: // value for 'filter' * }, * }); */ export function useSetHttpRequestLogFilterMutation(baseOptions?: Apollo.MutationHookOptions) { const options = {...defaultOptions, ...baseOptions} return Apollo.useMutation(SetHttpRequestLogFilterDocument, options); } export type SetHttpRequestLogFilterMutationHookResult = ReturnType; export type SetHttpRequestLogFilterMutationResult = Apollo.MutationResult; export type SetHttpRequestLogFilterMutationOptions = Apollo.BaseMutationOptions; export const ScopeDocument = gql` query Scope { scope { url } } `; /** * __useScopeQuery__ * * To run a query within a React component, call `useScopeQuery` and pass it any options that fit your needs. * When your component renders, `useScopeQuery` returns an object from Apollo Client that contains loading, error, and data properties * you can use to render your UI. * * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; * * @example * const { data, loading, error } = useScopeQuery({ * variables: { * }, * }); */ export function useScopeQuery(baseOptions?: Apollo.QueryHookOptions) { const options = {...defaultOptions, ...baseOptions} return Apollo.useQuery(ScopeDocument, options); } export function useScopeLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { const options = {...defaultOptions, ...baseOptions} return Apollo.useLazyQuery(ScopeDocument, options); } export type ScopeQueryHookResult = ReturnType; export type ScopeLazyQueryHookResult = ReturnType; export type ScopeQueryResult = Apollo.QueryResult; export const SetScopeDocument = gql` mutation SetScope($scope: [ScopeRuleInput!]!) { setScope(scope: $scope) { url } } `; export type SetScopeMutationFn = Apollo.MutationFunction; /** * __useSetScopeMutation__ * * To run a mutation, you first call `useSetScopeMutation` within a React component and pass it any options that fit your needs. * When your component renders, `useSetScopeMutation` returns a tuple that includes: * - A mutate function that you can call at any time to execute the mutation * - An object with fields that represent the current status of the mutation's execution * * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; * * @example * const [setScopeMutation, { data, loading, error }] = useSetScopeMutation({ * variables: { * scope: // value for 'scope' * }, * }); */ export function useSetScopeMutation(baseOptions?: Apollo.MutationHookOptions) { const options = {...defaultOptions, ...baseOptions} return Apollo.useMutation(SetScopeDocument, options); } export type SetScopeMutationHookResult = ReturnType; export type SetScopeMutationResult = Apollo.MutationResult; export type SetScopeMutationOptions = Apollo.BaseMutationOptions; export const CreateOrUpdateSenderRequestDocument = gql` mutation CreateOrUpdateSenderRequest($request: SenderRequestInput!) { createOrUpdateSenderRequest(request: $request) { id } } `; export type CreateOrUpdateSenderRequestMutationFn = Apollo.MutationFunction; /** * __useCreateOrUpdateSenderRequestMutation__ * * To run a mutation, you first call `useCreateOrUpdateSenderRequestMutation` within a React component and pass it any options that fit your needs. * When your component renders, `useCreateOrUpdateSenderRequestMutation` returns a tuple that includes: * - A mutate function that you can call at any time to execute the mutation * - An object with fields that represent the current status of the mutation's execution * * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; * * @example * const [createOrUpdateSenderRequestMutation, { data, loading, error }] = useCreateOrUpdateSenderRequestMutation({ * variables: { * request: // value for 'request' * }, * }); */ export function useCreateOrUpdateSenderRequestMutation(baseOptions?: Apollo.MutationHookOptions) { const options = {...defaultOptions, ...baseOptions} return Apollo.useMutation(CreateOrUpdateSenderRequestDocument, options); } export type CreateOrUpdateSenderRequestMutationHookResult = ReturnType; export type CreateOrUpdateSenderRequestMutationResult = Apollo.MutationResult; export type CreateOrUpdateSenderRequestMutationOptions = Apollo.BaseMutationOptions; export const CreateSenderRequestFromHttpRequestLogDocument = gql` mutation CreateSenderRequestFromHttpRequestLog($id: ID!) { createSenderRequestFromHttpRequestLog(id: $id) { id } } `; export type CreateSenderRequestFromHttpRequestLogMutationFn = Apollo.MutationFunction; /** * __useCreateSenderRequestFromHttpRequestLogMutation__ * * To run a mutation, you first call `useCreateSenderRequestFromHttpRequestLogMutation` within a React component and pass it any options that fit your needs. * When your component renders, `useCreateSenderRequestFromHttpRequestLogMutation` returns a tuple that includes: * - A mutate function that you can call at any time to execute the mutation * - An object with fields that represent the current status of the mutation's execution * * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; * * @example * const [createSenderRequestFromHttpRequestLogMutation, { data, loading, error }] = useCreateSenderRequestFromHttpRequestLogMutation({ * variables: { * id: // value for 'id' * }, * }); */ export function useCreateSenderRequestFromHttpRequestLogMutation(baseOptions?: Apollo.MutationHookOptions) { const options = {...defaultOptions, ...baseOptions} return Apollo.useMutation(CreateSenderRequestFromHttpRequestLogDocument, options); } export type CreateSenderRequestFromHttpRequestLogMutationHookResult = ReturnType; export type CreateSenderRequestFromHttpRequestLogMutationResult = Apollo.MutationResult; export type CreateSenderRequestFromHttpRequestLogMutationOptions = Apollo.BaseMutationOptions; export const SendRequestDocument = gql` mutation SendRequest($id: ID!) { sendRequest(id: $id) { id } } `; export type SendRequestMutationFn = Apollo.MutationFunction; /** * __useSendRequestMutation__ * * To run a mutation, you first call `useSendRequestMutation` within a React component and pass it any options that fit your needs. * When your component renders, `useSendRequestMutation` returns a tuple that includes: * - A mutate function that you can call at any time to execute the mutation * - An object with fields that represent the current status of the mutation's execution * * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; * * @example * const [sendRequestMutation, { data, loading, error }] = useSendRequestMutation({ * variables: { * id: // value for 'id' * }, * }); */ export function useSendRequestMutation(baseOptions?: Apollo.MutationHookOptions) { const options = {...defaultOptions, ...baseOptions} return Apollo.useMutation(SendRequestDocument, options); } export type SendRequestMutationHookResult = ReturnType; export type SendRequestMutationResult = Apollo.MutationResult; export type SendRequestMutationOptions = Apollo.BaseMutationOptions; export const GetSenderRequestDocument = gql` query GetSenderRequest($id: ID!) { senderRequest(id: $id) { id sourceRequestLogID url method proto headers { key value } body timestamp response { id proto statusCode statusReason body headers { key value } } } } `; /** * __useGetSenderRequestQuery__ * * To run a query within a React component, call `useGetSenderRequestQuery` and pass it any options that fit your needs. * When your component renders, `useGetSenderRequestQuery` returns an object from Apollo Client that contains loading, error, and data properties * you can use to render your UI. * * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; * * @example * const { data, loading, error } = useGetSenderRequestQuery({ * variables: { * id: // value for 'id' * }, * }); */ export function useGetSenderRequestQuery(baseOptions: Apollo.QueryHookOptions) { const options = {...defaultOptions, ...baseOptions} return Apollo.useQuery(GetSenderRequestDocument, options); } export function useGetSenderRequestLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { const options = {...defaultOptions, ...baseOptions} return Apollo.useLazyQuery(GetSenderRequestDocument, options); } export type GetSenderRequestQueryHookResult = ReturnType; export type GetSenderRequestLazyQueryHookResult = ReturnType; export type GetSenderRequestQueryResult = Apollo.QueryResult; export const GetSenderRequestsDocument = gql` query GetSenderRequests { senderRequests { id url method response { id statusCode statusReason } } } `; /** * __useGetSenderRequestsQuery__ * * To run a query within a React component, call `useGetSenderRequestsQuery` and pass it any options that fit your needs. * When your component renders, `useGetSenderRequestsQuery` returns an object from Apollo Client that contains loading, error, and data properties * you can use to render your UI. * * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; * * @example * const { data, loading, error } = useGetSenderRequestsQuery({ * variables: { * }, * }); */ export function useGetSenderRequestsQuery(baseOptions?: Apollo.QueryHookOptions) { const options = {...defaultOptions, ...baseOptions} return Apollo.useQuery(GetSenderRequestsDocument, options); } export function useGetSenderRequestsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { const options = {...defaultOptions, ...baseOptions} return Apollo.useLazyQuery(GetSenderRequestsDocument, options); } export type GetSenderRequestsQueryHookResult = ReturnType; export type GetSenderRequestsLazyQueryHookResult = ReturnType; export type GetSenderRequestsQueryResult = Apollo.QueryResult; export const UpdateInterceptSettingsDocument = gql` mutation UpdateInterceptSettings($input: UpdateInterceptSettingsInput!) { updateInterceptSettings(input: $input) { requestsEnabled responsesEnabled requestFilter responseFilter } } `; export type UpdateInterceptSettingsMutationFn = Apollo.MutationFunction; /** * __useUpdateInterceptSettingsMutation__ * * To run a mutation, you first call `useUpdateInterceptSettingsMutation` within a React component and pass it any options that fit your needs. * When your component renders, `useUpdateInterceptSettingsMutation` returns a tuple that includes: * - A mutate function that you can call at any time to execute the mutation * - An object with fields that represent the current status of the mutation's execution * * @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2; * * @example * const [updateInterceptSettingsMutation, { data, loading, error }] = useUpdateInterceptSettingsMutation({ * variables: { * input: // value for 'input' * }, * }); */ export function useUpdateInterceptSettingsMutation(baseOptions?: Apollo.MutationHookOptions) { const options = {...defaultOptions, ...baseOptions} return Apollo.useMutation(UpdateInterceptSettingsDocument, options); } export type UpdateInterceptSettingsMutationHookResult = ReturnType; export type UpdateInterceptSettingsMutationResult = Apollo.MutationResult; export type UpdateInterceptSettingsMutationOptions = Apollo.BaseMutationOptions; export const GetInterceptedRequestsDocument = gql` query GetInterceptedRequests { interceptedRequests { id url method response { statusCode statusReason } } } `; /** * __useGetInterceptedRequestsQuery__ * * To run a query within a React component, call `useGetInterceptedRequestsQuery` and pass it any options that fit your needs. * When your component renders, `useGetInterceptedRequestsQuery` returns an object from Apollo Client that contains loading, error, and data properties * you can use to render your UI. * * @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options; * * @example * const { data, loading, error } = useGetInterceptedRequestsQuery({ * variables: { * }, * }); */ export function useGetInterceptedRequestsQuery(baseOptions?: Apollo.QueryHookOptions) { const options = {...defaultOptions, ...baseOptions} return Apollo.useQuery(GetInterceptedRequestsDocument, options); } export function useGetInterceptedRequestsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions) { const options = {...defaultOptions, ...baseOptions} return Apollo.useLazyQuery(GetInterceptedRequestsDocument, options); } export type GetInterceptedRequestsQueryHookResult = ReturnType; export type GetInterceptedRequestsLazyQueryHookResult = ReturnType; export type GetInterceptedRequestsQueryResult = Apollo.QueryResult; ================================================ FILE: admin/src/lib/graphql/interceptedRequests.graphql ================================================ query GetInterceptedRequests { interceptedRequests { id url method response { statusCode statusReason } } } ================================================ FILE: admin/src/lib/graphql/omitTypename.ts ================================================ function omitTypename(key: string, value: T): T | undefined { return key === "__typename" ? undefined : value; } export function withoutTypename(input: T): T { return JSON.parse(JSON.stringify(input), omitTypename); } ================================================ FILE: admin/src/lib/graphql/useApollo.ts ================================================ import { ApolloClient, HttpLink, InMemoryCache, NormalizedCacheObject } from "@apollo/client"; let apolloClient: ApolloClient; function createApolloClient() { return new ApolloClient({ ssrMode: typeof window === "undefined", link: new HttpLink({ uri: "/api/graphql/", }), cache: new InMemoryCache({ typePolicies: { Query: { fields: { interceptedRequests: { merge(_, incoming) { return incoming; }, }, }, }, ProjectSettings: { merge: true, }, }, }), }); } export function useApollo() { const _apolloClient = apolloClient ?? createApolloClient(); // For SSG and SSR always create a new Apollo Client if (typeof window === "undefined") return _apolloClient; // Create the Apollo Client once in the client if (!apolloClient) apolloClient = _apolloClient; return _apolloClient; } ================================================ FILE: admin/src/lib/mui/createEmotionCache.ts ================================================ import createCache from "@emotion/cache"; // prepend: true moves MUI styles to the top of the so they're loaded first. // It allows developers to easily override MUI styles with other styling solutions, like CSS modules. export default function createEmotionCache() { return createCache({ key: "css", prepend: true }); } ================================================ FILE: admin/src/lib/mui/theme.ts ================================================ import * as colors from "@mui/material/colors"; import { createTheme } from "@mui/material/styles"; declare module "@mui/material/Paper" { interface PaperPropsVariantOverrides { centered: true; } } const heading = { fontFamily: "'JetBrains Mono', monospace", fontWeight: 600, }; let theme = createTheme({ palette: { mode: "dark", primary: { main: colors.teal["A400"], }, secondary: { main: colors.grey[900], light: "#333", dark: colors.common.black, }, }, typography: { h2: heading, h3: heading, h4: heading, h5: heading, h6: heading, }, }); theme = createTheme(theme, { palette: { background: { default: theme.palette.secondary.main, paper: theme.palette.secondary.light, }, info: { main: theme.palette.primary.main, }, success: { main: theme.palette.primary.main, }, }, components: { MuiTableRow: { styleOverrides: { root: { "&.Mui-selected, &.Mui-selected:hover": { backgroundColor: theme.palette.grey[700], }, }, }, }, MuiPaper: { variants: [ { props: { variant: "centered" }, style: { display: "flex", justifyContent: "center", alignItems: "center", padding: theme.spacing(4), }, }, ], }, }, }); export default theme; ================================================ FILE: admin/src/lib/queryParamsFromURL.tsx ================================================ import { KeyValuePair } from "./components/KeyValuePair"; export function queryParamsFromURL(url: string): KeyValuePair[] { const questionMarkIndex = url.indexOf("?"); if (questionMarkIndex === -1) { return []; } const queryParams: KeyValuePair[] = []; const searchParams = new URLSearchParams(url.slice(questionMarkIndex + 1)); for (const [key, value] of searchParams) { queryParams.push({ key, value }); } return queryParams; } ================================================ FILE: admin/src/lib/updateKeyPairItem.ts ================================================ import { KeyValuePair } from "./components/KeyValuePair"; function updateKeyPairItem(key: string, value: string, idx: number, items: KeyValuePair[]): KeyValuePair[] { const updated = [...items]; updated[idx] = { key, value }; // Append an empty key-value pair if the last item in the array isn't blank // anymore. if (items.length - 1 === idx && items[idx].key === "" && items[idx].value === "") { updated.push({ key: "", value: "" }); } return updated; } export default updateKeyPairItem; ================================================ FILE: admin/src/lib/updateURLQueryParams.ts ================================================ import { KeyValuePair } from "./components/KeyValuePair"; function updateURLQueryParams(url: string, queryParams: KeyValuePair[]) { // Note: We don't use the `URL` interface, because we're potentially dealing // with malformed/incorrect URLs, which would yield TypeErrors when constructed // via `URL`. let newURL = url; const questionMarkIndex = url.indexOf("?"); if (questionMarkIndex !== -1) { newURL = newURL.slice(0, questionMarkIndex); } const searchParams = new URLSearchParams(); for (const { key, value } of queryParams.filter(({ key }) => key !== "")) { searchParams.append(key, value); } const rawQueryParams = decodeURI(searchParams.toString()); if (rawQueryParams == "") { return newURL; } return newURL + "?" + rawQueryParams; } export default updateURLQueryParams; ================================================ FILE: admin/src/pages/_app.tsx ================================================ import { ApolloProvider } from "@apollo/client"; import { CacheProvider, EmotionCache } from "@emotion/react"; import { ThemeProvider } from "@mui/material"; import CssBaseline from "@mui/material/CssBaseline"; import { AppProps } from "next/app"; import Head from "next/head"; import React from "react"; import { ActiveProjectProvider } from "lib/ActiveProjectContext"; import { InterceptedRequestsProvider } from "lib/InterceptedRequestsContext"; import { useApollo } from "lib/graphql/useApollo"; import createEmotionCache from "lib/mui/createEmotionCache"; import theme from "lib/mui/theme"; import "../styles.css"; // Client-side cache, shared for the whole session of the user in the browser. const clientSideEmotionCache = createEmotionCache(); interface MyAppProps extends AppProps { emotionCache?: EmotionCache; } export default function MyApp(props: MyAppProps) { const { Component, emotionCache = clientSideEmotionCache, pageProps } = props; const apolloClient = useApollo(); return ( Hetty:// ); } ================================================ FILE: admin/src/pages/_document.tsx ================================================ import createEmotionServer from "@emotion/server/create-instance"; import Document, { Html, Head, Main, NextScript } from "next/document"; import React from "react"; import createEmotionCache from "lib/mui/createEmotionCache"; import theme from "lib/mui/theme"; export default class MyDocument extends Document { /* eslint-disable */ render() { return ( {(this.props as any).emotionStyleTags}
); } } // `getInitialProps` belongs to `_document` (instead of `_app`), // it's compatible with static-site generation (SSG). MyDocument.getInitialProps = async (ctx) => { // Resolution order // // On the server: // 1. app.getInitialProps // 2. page.getInitialProps // 3. document.getInitialProps // 4. app.render // 5. page.render // 6. document.render // // On the server with error: // 1. document.getInitialProps // 2. app.render // 3. page.render // 4. document.render // // On the client // 1. app.getInitialProps // 2. page.getInitialProps // 3. app.render // 4. page.render const originalRenderPage = ctx.renderPage; // You can consider sharing the same emotion cache between all the SSR requests to speed up performance. // However, be aware that it can have global side effects. const cache = createEmotionCache(); const { extractCriticalToChunks } = createEmotionServer(cache); ctx.renderPage = () => originalRenderPage({ enhanceApp: (App: any) => function EnhanceApp(props) { return ; }, }); const initialProps = await Document.getInitialProps(ctx); // This is important. It prevents emotion to render invalid HTML. // See https://github.com/mui-org/material-ui/issues/26561#issuecomment-855286153 const emotionStyles = extractCriticalToChunks(initialProps.html); const emotionStyleTags = emotionStyles.styles.map((style) => (