├── .dockerignore
├── .editorconfig
├── .eslintrc.js
├── .github
├── dependabot.yml
└── workflows
│ ├── app-deployement.yml
│ └── dockerhub-push.yml
├── .gitignore
├── .husky
├── pre-commit
└── pre-push
├── .log
├── ti-15454.log
├── ti-18421.log
├── ti-19611.log
├── ti-21177.log
├── ti-21349.log
├── ti-22011.log
├── ti-22484.log
├── ti-22812.log
├── ti-78950.log
├── ti-91862.log
├── ti-93985.log
└── tsserver.log
├── .vscode
└── launch.json
├── CNAME
├── Dockerfile
├── LICENSE.md
├── README.md
├── SECURITY.md
├── craco.config.js
├── package.json
├── public
├── favicon.ico
└── index.html
├── src
├── assets
│ └── svg
│ │ ├── arrow_right.svg
│ │ ├── chevron_up.svg
│ │ ├── clear.svg
│ │ ├── close.svg
│ │ ├── copy.svg
│ │ ├── cross.svg
│ │ ├── delete.svg
│ │ ├── download.svg
│ │ ├── filter.svg
│ │ ├── filter_selected.svg
│ │ ├── loader.svg
│ │ ├── logo.svg
│ │ ├── plus.svg
│ │ ├── refresh.svg
│ │ ├── side_by_side.svg
│ │ ├── switch.svg
│ │ ├── theme_blue_button.svg
│ │ ├── theme_dark_button.svg
│ │ ├── theme_synth_button.svg
│ │ └── up_down.svg
├── breakpoints.scss
├── components
│ ├── appLoader
│ │ ├── index.tsx
│ │ └── styles.scss
│ ├── customHost
│ │ ├── index.tsx
│ │ └── styles.scss
│ ├── detailedRequest
│ │ ├── index.tsx
│ │ └── styles.scss
│ ├── header
│ │ ├── index.tsx
│ │ └── styles.scss
│ ├── notificationsPopup
│ │ ├── index.tsx
│ │ └── styles.scss
│ ├── requestsTable
│ │ ├── index.tsx
│ │ └── styles.scss
│ ├── resetPopup
│ │ ├── index.tsx
│ │ └── styles.scss
│ ├── tabSwitcher
│ │ ├── index.tsx
│ │ └── styles.scss
│ └── toggleBtn
│ │ ├── index.tsx
│ │ └── styles.scss
├── globalStyles.js
├── helpers
│ ├── fallback-loaders.js
│ └── styles.scss
├── index.tsx
├── lib
│ ├── index.test.ts
│ ├── index.ts
│ ├── localStorage
│ │ ├── index.test.ts
│ │ └── index.ts
│ ├── notify
│ │ └── index.ts
│ ├── types
│ │ ├── data
│ │ │ └── index.ts
│ │ ├── discord.ts
│ │ ├── filter.ts
│ │ ├── id.ts
│ │ ├── protocol
│ │ │ └── index.ts
│ │ ├── slack.ts
│ │ ├── storedData.ts
│ │ ├── tab.ts
│ │ ├── telegram.ts
│ │ └── view.ts
│ └── utils
│ │ ├── index.test.ts
│ │ └── index.ts
├── pages
│ ├── homePage
│ │ ├── index.tsx
│ │ ├── requestDetailsWrapper.tsx
│ │ ├── requestsTableWrapper.tsx
│ │ └── styles.scss
│ └── termsPage
│ │ └── index.tsx
├── react-app-env.d.ts
├── reportWebVitals.ts
├── styles.scss
├── theme.ts
├── vitals.js
└── xid-js.d.ts
├── tailwind.config.js
├── tsconfig.json
└── yarn.lock
/.dockerignore:
--------------------------------------------------------------------------------
1 | /node_modules
2 | /.pnp
3 | .pnp.js
4 |
5 | /coverage
6 |
7 | /build
8 |
9 | .DS_Store
10 | .env.local
11 | .env.development.local
12 | .env.test.local
13 | .env.production.local
14 |
15 | npm-debug.log*
16 | yarn-debug.log*
17 | yarn-error.log*
18 | .log
19 | .log/
20 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | end_of_line = lf
6 | insert_final_newline = true
7 | indent_style = space
8 | indent_size = 2
9 | trim_trailing_whitespace = true
10 | max_line_length = 100
11 |
12 | [*.md]
13 | trim_trailing_whitespace = falsȩ̧
--------------------------------------------------------------------------------
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: {
4 | browser: true,
5 | es2021: true,
6 | },
7 | settings: {
8 | "import/resolver": {
9 | node: {
10 | paths: ["src"],
11 | extensions: [".js", ".ts", ".tsx", ".d.ts"],
12 | },
13 | },
14 | },
15 | extends: ["plugin:react/recommended", "airbnb", "prettier"],
16 | parser: "@typescript-eslint/parser",
17 | parserOptions: {
18 | ecmaFeatures: {
19 | jsx: true,
20 | },
21 | ecmaVersion: 12,
22 | sourceType: "module",
23 | },
24 | plugins: ["react", "@typescript-eslint", "fp-ts"],
25 | rules: {
26 | "react/jsx-filename-extension": [2, { extensions: [".js", ".jsx", ".ts", ".tsx"] }],
27 | camelcase: ["error", { allow: ["aes_key", "up_and_down", "side_by_side"] }],
28 | "react/jsx-props-no-spreading": "off",
29 | "no-use-before-define": "off",
30 | "@typescript-eslint/no-use-before-define": ["error"],
31 | "import/extensions": "off",
32 | "no-redeclare": "off", // Needed for type declarations
33 | "react/prop-types": "off",
34 | "no-undef": "off",
35 | "no-unused-vars": "off",
36 | "@typescript-eslint/no-unused-vars": "error",
37 | "react/require-default-props": "off",
38 | "no-nested-ternary": "off",
39 | "no-underscore-dangle": "off",
40 | "fp-ts/no-lib-imports": "error",
41 | "import/order": [
42 | "error",
43 | {
44 | groups: ["builtin", "external", "internal"],
45 | pathGroups: [
46 | {
47 | pattern: "react",
48 | group: "external",
49 | position: "before",
50 | },
51 | ],
52 | pathGroupsExcludedImportTypes: ["react"],
53 | "newlines-between": "always",
54 | alphabetize: {
55 | order: "asc",
56 | caseInsensitive: true,
57 | },
58 | },
59 | ],
60 | },
61 | };
62 |
--------------------------------------------------------------------------------
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | # To get started with Dependabot version updates, you'll need to specify which
2 | # package ecosystems to update and where the package manifests are located.
3 | # Please see the documentation for all configuration options:
4 | # https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
5 |
6 | version: 2
7 | updates:
8 |
9 | # Maintain dependencies for GitHub Actions
10 | - package-ecosystem: "github-actions"
11 | directory: "/"
12 | schedule:
13 | interval: "weekly"
14 | target-branch: "dev"
15 | commit-message:
16 | prefix: "chore"
17 | include: "scope"
18 |
19 | # Maintain dependencies for npm modules
20 | - package-ecosystem: "npm"
21 | directory: "/"
22 | schedule:
23 | interval: "weekly"
24 | target-branch: "dev"
25 | commit-message:
26 | prefix: "chore"
27 | include: "scope"
28 |
29 | # Maintain dependencies for docker
30 | - package-ecosystem: "docker"
31 | directory: "/"
32 | schedule:
33 | interval: "weekly"
34 | target-branch: "dev"
35 | commit-message:
36 | prefix: "chore"
37 | include: "scope"
--------------------------------------------------------------------------------
/.github/workflows/app-deployement.yml:
--------------------------------------------------------------------------------
1 | name: React app deployement
2 |
3 | on:
4 | push:
5 | tags:
6 | - '*'
7 | workflow_dispatch:
8 |
9 | jobs:
10 | build:
11 | runs-on: ubuntu-latest
12 | env:
13 | CI: false
14 | steps:
15 | - uses: actions/checkout@v2
16 | with:
17 | fetch-depth: 0
18 | token: ${{ secrets.GITHUB_TOKEN }}
19 |
20 | - run : git config --global user.name github-actions
21 | - run : git config --global user.email github-actions@github.com
22 | - run : yarn install
23 | - run : yarn build
24 | - run : cp CNAME build
25 | - run : git --work-tree build add --all && git commit -m "Automatic Deploy action run by github-actions"
26 | - run : git push origin HEAD:gh-pages --force
--------------------------------------------------------------------------------
/.github/workflows/dockerhub-push.yml:
--------------------------------------------------------------------------------
1 | name: 🌥 Docker Push
2 |
3 | on:
4 | workflow_run:
5 | workflows: ["React app deployement"]
6 | types:
7 | - completed
8 | workflow_dispatch:
9 |
10 | jobs:
11 | docker:
12 | runs-on: ubuntu-latest
13 | steps:
14 | - name: Git Checkout
15 | uses: actions/checkout@v3
16 |
17 | - name: Get Github tag
18 | id: meta
19 | run: |
20 | echo "::set-output name=tag::$(curl --silent "https://api.github.com/repos/projectdiscovery/interactsh-web/releases/latest" | jq -r .tag_name)"
21 | - name: Set up QEMU
22 | uses: docker/setup-qemu-action@v1
23 |
24 | - name: Set up Docker Buildx
25 | uses: docker/setup-buildx-action@v1
26 |
27 | - name: Login to DockerHub
28 | uses: docker/login-action@v1
29 | with:
30 | username: ${{ secrets.DOCKER_USERNAME }}
31 | password: ${{ secrets.DOCKER_TOKEN }}
32 |
33 | - name: Build and push
34 | uses: docker/build-push-action@v2
35 | with:
36 | context: .
37 | platforms: linux/amd64,linux/arm64
38 | push: true
39 | tags: projectdiscovery/interactsh-web:latest,projectdiscovery/interactsh-web:${{ steps.meta.outputs.tag }}
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # production
12 | /build
13 |
14 | # misc
15 | .DS_Store
16 | .env.local
17 | .env.development.local
18 | .env.test.local
19 | .env.production.local
20 |
21 | npm-debug.log*
22 | yarn-debug.log*
23 | yarn-error.log*
24 | .log
25 | .log/
26 |
--------------------------------------------------------------------------------
/.husky/pre-commit:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | yarn test --watchAll=false
5 |
--------------------------------------------------------------------------------
/.husky/pre-push:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | . "$(dirname "$0")/_/husky.sh"
3 |
4 | yarn build
5 |
--------------------------------------------------------------------------------
/.vscode/launch.json:
--------------------------------------------------------------------------------
1 | {
2 | // Use IntelliSense to learn about possible attributes.
3 | // Hover to view descriptions of existing attributes.
4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
5 | "version": "0.2.0",
6 | "configurations": [
7 | {
8 | "type": "pwa-chrome",
9 | "request": "launch",
10 | "name": "Launch Chrome against localhost",
11 | "url": "http://localhost:3000",
12 | "webRoot": "${workspaceFolder}"
13 | }
14 | ]
15 | }
--------------------------------------------------------------------------------
/CNAME:
--------------------------------------------------------------------------------
1 | app.interactsh.com
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM node:16
2 | WORKDIR /app
3 | COPY package.json ./
4 | COPY yarn.lock ./
5 | COPY ./ ./
6 | RUN yarn install
7 | CMD ["yarn", "start"]
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2021 ProjectDiscovery
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # interactsh-web
2 |
3 | [Interactsh-web](https://github.com/projectdiscovery/interactsh-web) is a free and open-source web client that displays [Interactsh](https://github.com/projectdiscovery/interactsh) interactions in a well-managed dashboard in your browser. It uses the **browser's local storage** to store and display interactions. By default, the web client is configured to use - **interachsh.com**, a cloud-hosted interactsh server, and supports other self-hosted public/authencaited interactsh servers as well.
4 |
5 | A hosted instance of **interactsh-web** client is available at https://app.interactsh.com
6 |
7 |
8 |
9 | ## Configuring Self-Hosted Interactsh Server
10 |
11 | - Navigate to hosted interactsh-web client at https://app.interactsh.com
12 | - Click on `oast.fun` link at top bar
13 | - Submit domain name running self-hosted interactsh server, optionally token (for protected server)
14 |
15 | Here is an example configuring self-hosted interactsh server with web-client:
16 |
17 | https://user-images.githubusercontent.com/8293321/163819390-b2677f3b-4c31-4439-b258-33b8bee87bf1.mp4
18 |
19 | ## Build from Source
20 |
21 |
22 |
23 |
24 |
25 |
26 | Note:
27 | ----
28 |
29 | In order to run the local version of the web client, **acao-url** flag should be pointed to **localhost** while running interactsh server to avoid CORS errors. for example,
30 |
31 | ```
32 | interactsh-server -acao-url http://localhost:3000
33 | ```
34 |
35 |
36 |
37 |
38 | ### Using Yarn
39 |
40 | ```
41 | git clone https://github.com/projectdiscovery/interactsh-web
42 | cd interactsh-web
43 | yarn install
44 | yarn start
45 | ```
46 |
47 | ### Using Docker
48 |
49 | ```
50 | docker pull projectdiscovery/interactsh-web
51 | docker run -it -p 3000:3000 projectdiscovery/interactsh-web
52 | ```
53 |
54 | Once successfully started, you can access web dashboard at [localhost:3000](http://localhost:3000)
55 |
56 | -----
57 |
58 | ### Custom configuration
59 |
60 | You can set a custom configuration when deploying this project.
61 | If you want to avoid the registration of your server host and token, you can give the below environnement variable to your docker-compose / server.
62 |
63 | For this, just specify
64 | - `REACT_APP_HOST` for the host (default: "oast.fun")
65 | - `REACT_APP_TOKEN` for the custom token (default: "")
66 | - `REACT_APP_CIDL` for the custom correlation id length (default: 20)
67 | - `REACT_APP_CIDN` for the custom correlation nonce length (default: 13)
68 |
69 |
70 |
71 | **interactsh-web** is made with 🖤 by the [projectdiscovery](https://projectdiscovery.io) team.
72 |
73 |
--------------------------------------------------------------------------------
/SECURITY.md:
--------------------------------------------------------------------------------
1 | # Security Policy
2 |
3 | ## Reporting a Vulnerability
4 |
5 | DO NOT CREATE AN ISSUE to report a security problem. Instead, please send an email to security@projectdiscovery.io, and we will acknowledge it within 3 working days.
6 |
--------------------------------------------------------------------------------
/craco.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | style: {
3 | postcss: {
4 | plugins: [require("tailwindcss"), require("autoprefixer")],
5 | },
6 | },
7 | };
8 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "i",
3 | "version": "0.1.0",
4 | "private": true,
5 | "engines": {
6 | "npm": ">=8.0.0 <9.0.0",
7 | "node": ">=16.0.0 <=18.20.7"
8 | },
9 | "dependencies": {
10 | "@ant-design/icons": "^4.7.0",
11 | "@babakness/exhaustive-type-checking": "^0.1.3",
12 | "@craco/craco": "^6.2.0",
13 | "@headlessui/react": "^1.5.0",
14 | "@morphic-ts/batteries": "^3.0.0",
15 | "@tailwindcss/forms": "^0.3.3",
16 | "@tailwindcss/typography": "^0.4.1",
17 | "@testing-library/jest-dom": "^5.11.4",
18 | "@testing-library/react": "^11.1.0",
19 | "@testing-library/user-event": "^13.5.0",
20 | "@types/jest": "^26.0.15",
21 | "@types/node": "^12.0.0",
22 | "@types/node-rsa": "^1.1.1",
23 | "@types/prismjs": "^1.16.6",
24 | "@types/react": "^17.0.0",
25 | "@types/react-dom": "^17.0.0",
26 | "@types/react-router-dom": "^5.1.8",
27 | "@types/react-transition-group": "^4.4.2",
28 | "@types/validator": "^13.1.4",
29 | "@types/webpack-env": "^1.16.2",
30 | "@vercel/analytics": "^0.1.11",
31 | "antd": "3.3.0",
32 | "autoprefixer": "^9",
33 | "date-fns": "^2.23.0",
34 | "dayjs": "^1.10.6",
35 | "eslint-plugin-fp-ts": "^0.2.1",
36 | "fp-ts": "^2.11.1",
37 | "fp-ts-contrib": "^0.1.26",
38 | "fp-ts-local-storage": "^1.0.3",
39 | "fp-ts-std": "^0.11.0",
40 | "headlessui": "^0.0.0",
41 | "husky": "^7.0.4",
42 | "io-ts": "^2.2.16",
43 | "io-ts-numbers": "^1.0.3",
44 | "io-ts-types": "^0.5.16",
45 | "is-xid": "^1.0.3",
46 | "jest-fast-check": "^1.0.2",
47 | "js-file-download": "^0.4.12",
48 | "monocle-ts": "^2.3.10",
49 | "morphic-ts": "^0.8.0-rc.2",
50 | "newtype-ts": "^0.3.4",
51 | "node-rsa": "^1.1.1",
52 | "postcss": "^7",
53 | "prettier": "^2.3.2",
54 | "prismjs": "^1.24.1",
55 | "react": "^17.0.2",
56 | "react-content-loader": "^6.0.3",
57 | "react-dom": "^17.0.2",
58 | "react-error-boundary": "^3.1.3",
59 | "react-ga": "^3.3.0",
60 | "react-router-dom": "^5.2.0",
61 | "react-scripts": "4.0.3",
62 | "react-transition-group": "^4.4.2",
63 | "sass": "^1.35.2",
64 | "styled-components": "5.2.1",
65 | "tailwindcss": "npm:@tailwindcss/postcss7-compat",
66 | "ts-pattern": "^3.2.4",
67 | "typescript": "^4.3.5",
68 | "uuid": "^8.3.2",
69 | "validator": "^13.7.0",
70 | "web-vitals": "^2.1.0",
71 | "xid-js": "^1.0.1",
72 | "zbase32": "github:projectdiscovery/zbase32"
73 | },
74 | "scripts": {
75 | "start": "PORT=3000 craco start",
76 | "build": "NODE_OPTIONS=--openssl-legacy-provider craco build",
77 | "test": "craco test",
78 | "eject": "craco eject",
79 | "lint:fix": "eslint --fix src"
80 | },
81 | "eslintConfig": {
82 | "extends": [
83 | "react-app",
84 | "react-app/jest"
85 | ]
86 | },
87 | "browserslist": {
88 | "production": [
89 | ">0.2%",
90 | "not dead",
91 | "not op_mini all"
92 | ],
93 | "development": [
94 | "last 1 chrome version",
95 | "last 1 firefox version",
96 | "last 1 safari version"
97 | ]
98 | },
99 | "devDependencies": {
100 | "@types/jest-axe": "^3.5.2",
101 | "@types/styled-components": "^5.1.12",
102 | "@types/uuid": "^8.3.3",
103 | "@typescript-eslint/eslint-plugin": "^4.25.0",
104 | "@typescript-eslint/parser": "^4.25.0",
105 | "eslint-config-airbnb": "^18.2.1",
106 | "eslint-config-prettier": "^8.3.0",
107 | "eslint-plugin-prettier": "^3.4.0",
108 | "eslint-plugin-react": "^7.24.0",
109 | "fast-check": "^2.17.0"
110 | }
111 | }
112 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/projectdiscovery/interactsh-web/3a8c85a9c9d512e85b394c4a897450d09bb6847a/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Interactsh | Web Client
9 |
10 |
14 |
18 |
33 |
50 |
56 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
--------------------------------------------------------------------------------
/src/assets/svg/arrow_right.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 | icon-arrow-right
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/assets/svg/chevron_up.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 | keyboard-arrow-up-sharp-24px
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/src/assets/svg/clear.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 | auto-delete-outlined-20px
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/assets/svg/close.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/src/assets/svg/copy.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 | copy.1
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/assets/svg/cross.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 | cross
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/assets/svg/delete.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 | delete.1
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/assets/svg/download.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 | internet-download
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/src/assets/svg/filter.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 | Shape Copy
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/assets/svg/filter_selected.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 | Shape Copy
4 |
5 |
6 |
7 |
8 |
9 |
--------------------------------------------------------------------------------
/src/assets/svg/loader.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
14 |
15 |
16 |
17 |
--------------------------------------------------------------------------------
/src/assets/svg/logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 | Httpx-Blk
4 |
5 |
6 |
7 |
--------------------------------------------------------------------------------
/src/assets/svg/plus.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 | plus.2
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/src/assets/svg/refresh.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 | Path
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/src/assets/svg/side_by_side.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 | Group 24
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 | ̧
--------------------------------------------------------------------------------
/src/assets/svg/switch.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 | settings-power-sharp-24px
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/assets/svg/theme_blue_button.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 | Group 28
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/assets/svg/theme_dark_button.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 | Group 25
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/assets/svg/theme_synth_button.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 | Group 26
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
--------------------------------------------------------------------------------
/src/assets/svg/up_down.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 | horizontal-split-24px
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
--------------------------------------------------------------------------------
/src/breakpoints.scss:
--------------------------------------------------------------------------------
1 | @mixin for-size($range) {
2 | $small: 600px;
3 | $medium: 768px;
4 | $large: 992px;
5 | $extra-large: 1200px;
6 | @if $range == xsmall {
7 | @media only screen and (max-width: #{$small }) {
8 | @content;
9 | }
10 | } @else if $range == small {
11 | @media only screen and (min-width: $small) {
12 | @content;
13 | }
14 | } @else if $range == medium {
15 | @media only screen and (min-width: $medium) {
16 | @content;
17 | }
18 | } @else if $range == large {
19 | @media only screen and (min-width: $large) {
20 | @content;
21 | }
22 | } @else if $range == xlarge {
23 | @media only screen and (min-width: $extra-large) {
24 | @content;
25 | }
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/components/appLoader/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { ReactComponent as Logo } from "../../assets/svg/logo.svg";
4 |
5 | import "./styles.scss";
6 |
7 | interface AppLoaderP {
8 | isRegistered: boolean;
9 | mode: string;
10 | }
11 |
12 | const AppLoader = ({ isRegistered, mode }: AppLoaderP) => (
13 |
17 |
18 | {mode === "loading" ? (
19 | <>
20 | {" "}
21 |
22 | interact .sh
23 |
24 | >
25 | ) : (
26 | "Server Unavailable..."
27 | )}
28 |
29 |
30 | );
31 | export default AppLoader;
32 |
--------------------------------------------------------------------------------
/src/components/appLoader/styles.scss:
--------------------------------------------------------------------------------
1 | @import "../../breakpoints.scss";
2 | @include for-size(xsmall) {
3 | }
4 |
5 | @include for-size(small) {
6 | }
7 |
8 | @include for-size(medium) {
9 | }
10 |
11 | @include for-size(large) {
12 | }
13 |
14 | @include for-size(xlarge) {
15 | }
16 |
17 | .loader_container {
18 | position: fixed;
19 | inset: 0;
20 | height: 100vh;
21 | width: 100vw;
22 | background: #000 !important;
23 | display: flex;
24 | visibility: hidden;
25 | align-items: center;
26 | justify-content: center;
27 | opacity: 0;
28 | transition: opacity 0.2s ease-out, visibility 0.2s ease-in;
29 |
30 | &:before {
31 | content: "";
32 | position: absolute;
33 | opacity: 0.7;
34 | top: 0;
35 | left: 0;
36 | height: 100%;
37 | width: 100%;
38 | --s: 5rem;
39 |
40 | --m: 0.2rem;
41 |
42 | --v1: #000 119.5deg, #0000 120.5deg;
43 | --v2: #4d0343 119.5deg, #0000 120.5deg;
44 | background: conic-gradient(
45 | at var(--m) calc(var(--s) * 0.5777),
46 | transparent 270deg,
47 | #4d0343 0deg
48 | ),
49 | conic-gradient(
50 | at calc(100% - var(--m)) calc(var(--s) * 0.5777),
51 | #4d0343 90deg,
52 | transparent 0deg
53 | ),
54 | conic-gradient(from -60deg at 50% calc(var(--s) * 0.8662), var(--v1)),
55 | conic-gradient(from -60deg at 50% calc(var(--s) * 0.8662 + 2 * var(--m)), var(--v2)),
56 | conic-gradient(from 120deg at 50% calc(var(--s) * 1.4435 + 3 * var(--m)), var(--v1)),
57 | conic-gradient(from 120deg at 50% calc(var(--s) * 1.4435 + var(--m)), var(--v2)),
58 | linear-gradient(90deg, #000 calc(50% - var(--m)), #4d0343 0 calc(50% + var(--m)), #000 0);
59 | background-size: calc(var(--s) + 2 * var(--m)) calc(var(--s) * 1.732 + 3 * var(--m));
60 | -webkit-mask: linear-gradient(
61 | -60deg,
62 | #0000 calc(20% - 4 * var(--s)),
63 | #000,
64 | #0000 calc(80% + 4 * var(--s))
65 | )
66 | (right / 300%) 100% no-repeat;
67 | animation: f 4s linear infinite alternate;
68 | }
69 |
70 | .loader_content {
71 | font-family: "Nunito Sans", sans-serif;
72 | font-size: 5.2rem;
73 | font-family: "Nunito Sans", sans-serif;
74 | color: #fff;
75 | font-weight: 100;
76 | display: flex;
77 | align-items: center;
78 | span {
79 | margin-left: 1rem;
80 | span {
81 | font-weight: 600;
82 | }
83 | }
84 | svg {
85 | height: 10rem;
86 | width: 10rem;
87 | }
88 | }
89 | }
90 |
91 | @keyframes f {
92 | 100% {
93 | -webkit-mask-position: left;
94 | }
95 | }
96 |
--------------------------------------------------------------------------------
/src/components/customHost/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 |
3 | import { ReactComponent as ArrowRightIcon } from "assets/svg/arrow_right.svg";
4 | import { ReactComponent as CloseIcon } from "assets/svg/close.svg";
5 | import { ReactComponent as LoadingIcon } from "assets/svg/loader.svg";
6 | import "./styles.scss";
7 | import { register } from "lib";
8 |
9 | import { defaultStoredData, getStoredData, writeStoredData } from "../../lib/localStorage";
10 |
11 | interface CustomHostP {
12 | handleCloseDialog: () => void;
13 | }
14 |
15 | const CustomHost = ({ handleCloseDialog }: CustomHostP) => {
16 | const data = getStoredData();
17 | const { host, token, correlationIdLength, correlationIdNonceLength } = data;
18 | const [isDeleteConfirmationVisible, setIsDeleteConfirmationVisible] = useState(false);
19 | const [isLoading, setIsLoading] = useState(false);
20 | const [errorText, setErrorText] = useState("");
21 | const [inputValue, setInputValue] = useState(host === "oast.fun" ? "" : host);
22 | const [tokenInputValue, setTokenInputValue] = useState(token === "" ? "" : token);
23 | const [correlationIdLengthInputValue, setCorrelationIdLengthValue] = useState(correlationIdLength === 20 ? 20 : correlationIdLength);
24 | const [correlationIdNonceLengthInputValue, setCorrelationIdNonceLengthValue] = useState(correlationIdNonceLength === 13 ? 13 : correlationIdNonceLength);
25 |
26 | const handleDeleteConfirmationVisibility = () => {
27 | setIsDeleteConfirmationVisible(!isDeleteConfirmationVisible);
28 | };
29 |
30 | const handleInput: React.ChangeEventHandler = (e) => {
31 | switch (e.target.name) {
32 | case "custom_host":
33 | setInputValue(e.target.value);
34 | break;
35 | case "token":
36 | setTokenInputValue(e.target.value);
37 | break;
38 | case "cidl":
39 | setCorrelationIdLengthValue(parseInt(e.target.value, 10));
40 | break;
41 | case "cidn":
42 | setCorrelationIdNonceLengthValue(parseInt(e.target.value, 10));
43 | break;
44 | default:
45 | break;
46 | }
47 | };
48 |
49 | const handleConfirm = () => {
50 | if (
51 | (inputValue !== "" && inputValue !== "oast.fun" && host !== inputValue) ||
52 | (inputValue !== "" && inputValue !== "oast.fun" && tokenInputValue !== token)
53 | ) {
54 | setIsLoading(true);
55 | const oldData = getStoredData();
56 | setTimeout(() => {
57 | writeStoredData({...getStoredData(),
58 | correlationIdLength: correlationIdLengthInputValue,
59 | correlationIdNonceLength: correlationIdNonceLengthInputValue
60 | });
61 | register(
62 | inputValue.replace(/(^\w+:|^)\/\//, ""),
63 | tokenInputValue,
64 | inputValue !== host && tokenInputValue === token,
65 | inputValue === host && tokenInputValue !== token
66 | )
67 | .then((d) => {
68 | localStorage.clear();
69 | writeStoredData(d);
70 | setIsLoading(false);
71 | handleCloseDialog();
72 | setErrorText("");
73 | window.location.reload();
74 | })
75 | .catch((err) => {
76 | if (err.message === "auth failed") {
77 | setIsLoading(false);
78 | setErrorText("Authentication failed, token not valid.");
79 | } else {
80 | setIsLoading(false);
81 | setErrorText(
82 | "We were unable to establish a connection with your server; please try again by clicking on confirm."
83 | );
84 | }
85 | writeStoredData({...oldData});
86 | });
87 | }, 30);
88 | }
89 | };
90 |
91 | const handleDelete = () => {
92 | setIsLoading(true);
93 | const oldData = getStoredData();
94 | setTimeout(() => {
95 | writeStoredData({...getStoredData(),
96 | correlationIdLength: defaultStoredData.correlationIdLength,
97 | correlationIdNonceLength: defaultStoredData.correlationIdNonceLength
98 | });
99 | register(defaultStoredData.host, defaultStoredData.token, true, false)
100 | .then((d) => {
101 | localStorage.clear();
102 | writeStoredData(d);
103 | setIsLoading(false);
104 | handleCloseDialog();
105 | setErrorText("");
106 | window.location.reload();
107 | })
108 | .catch((err) => {
109 | if (err.message === "auth failed") {
110 | setIsLoading(false);
111 | setErrorText("Authentication failed, token not valid.");
112 | } else {
113 | setIsLoading(false);
114 | setErrorText(
115 | "We were unable to establish a connection with your server; please try again by clicking on confirm."
116 | );
117 | }
118 | writeStoredData({...oldData});
119 | });
120 | }, 30);
121 | };
122 |
123 | return (
124 |
125 | {isDeleteConfirmationVisible ? (
126 |
127 |
128 | Remove Custom Host
129 |
130 |
131 |
132 | Please confirm the action, this action can’t be undone and all the client data will be
133 | delete immediately.
134 |
135 |
136 |
142 | Delete {isLoading && }
143 |
144 |
145 |
146 | ) : (
147 |
148 |
149 | Custom Host
150 |
151 |
152 |
153 | You can point your self hosted oast.fun server below to connect with this web client.
154 |
155 |
162 |
170 |
171 | Correlation Id Length (cidl)
172 |
182 |
183 |
184 | Correlation Id Nonce Length (cidn)
185 |
195 |
196 | {errorText !== "" &&
{errorText}
}
197 |
198 | {host !== "oast.fun" && (
199 |
204 | Remove Custom Host
205 |
206 | )}
207 |
213 | Confirm
214 | {isLoading ? : }
215 |
216 |
217 |
218 | )}
219 |
220 | );
221 | };
222 |
223 | export default CustomHost;
224 |
--------------------------------------------------------------------------------
/src/components/customHost/styles.scss:
--------------------------------------------------------------------------------
1 | @import "../../breakpoints.scss";
2 | @include for-size(xsmall) {
3 | }
4 |
5 | @include for-size(small) {
6 | }
7 |
8 | @include for-size(medium) {
9 | }
10 |
11 | @include for-size(large) {
12 | }
13 |
14 | @include for-size(xlarge) {
15 | }
16 |
17 | .backdrop_container {
18 | width: 100vw;
19 | height: 100vh;
20 | position: absolute;
21 | top: 0;
22 | left: 0;
23 | z-index: 1000;
24 | background: rgba(36, 13, 44, 0.2);
25 | display: flex;
26 | align-items: center;
27 | justify-content: center;
28 |
29 | .dialog_box {
30 | background: #000000;
31 | border-radius: 0.6rem;
32 | width: 45%;
33 | padding: 3.5rem 4rem;
34 | display: flex;
35 | flex-direction: column;
36 |
37 | .header {
38 | display: flex;
39 | margin-bottom: 2rem;
40 | padding: 0;
41 | span {
42 | font-size: 2.2rem;
43 | color: #ffffff;
44 | letter-spacing: 0;
45 | }
46 | svg {
47 | margin-left: auto;
48 | height: 3rem;
49 | cursor: pointer;
50 | }
51 | }
52 | & > span {
53 | width: 100%;
54 | font-size: 1.6rem;
55 | color: #ffffff;
56 | letter-spacing: 0;
57 | margin-bottom: 3.5rem;
58 | }
59 | input {
60 | font-size: 1.5rem;
61 | color: #6588ff;
62 | letter-spacing: 0;
63 | text-align: left;
64 | height: 4rem;
65 | padding: 1rem;
66 | border: 1px solid #1c1c1c;
67 | background: transparent;
68 | border-radius: 0.6rem;
69 | margin-bottom: 2rem;
70 | &::placeholder {
71 | color: #3c3c3c;
72 | }
73 | &:focus {
74 | outline: none;
75 | box-shadow: none;
76 | border: 1px solid #6588ff;
77 | }
78 | &:disabled {
79 | background: #101010;
80 | border: 1px solid #101010;
81 | }
82 | }
83 | .error {
84 | font-size: 1.5rem;
85 | color: #ff5151;
86 | letter-spacing: 0;
87 | text-align: left;
88 | margin-bottom: 2rem;
89 | }
90 | .buttons {
91 | display: flex;
92 | font-weight: 600;
93 | margin-top: 1rem;
94 | button {
95 | &:disabled {
96 | opacity: 0.5;
97 | }
98 | }
99 | .delete_button {
100 | display: flex;
101 | align-items: center;
102 | justify-content: center;
103 | background: #ff5151;
104 | border-radius: 0.6rem;
105 | font-size: 1.4rem;
106 | color: #ffffff;
107 | letter-spacing: 0;
108 | text-align: left;
109 | height: 3.5rem;
110 | margin-right: 2rem;
111 | flex: 1;
112 | transition: all 0.1s linear;
113 | svg {
114 | margin-left: 1.5rem;
115 | }
116 | }
117 | .remove_button {
118 | display: flex;
119 | align-items: center;
120 | justify-content: center;
121 | background: rgba(216, 216, 216, 0.19);
122 | border-radius: 0.6rem;
123 | font-size: 1.4rem;
124 | color: #ffffff;
125 | letter-spacing: 0;
126 | text-align: left;
127 | height: 3.5rem;
128 | margin-right: 2rem;
129 | flex: 1;
130 | transition: all 0.1s linear;
131 | svg {
132 | margin-left: 1.5rem;
133 | }
134 | }
135 | .submit_button {
136 | flex: 2;
137 | display: flex;
138 | align-items: center;
139 | justify-content: center;
140 | background: #3254c5;
141 | border-radius: 0.6rem;
142 | font-size: 1.4rem;
143 | color: #ffffff;
144 | letter-spacing: 0;
145 | text-align: left;
146 | height: 3.5rem;
147 | transition: all 0.1s linear;
148 | svg {
149 | margin-left: 1.5rem;
150 | }
151 | }
152 | }
153 | .advanced_options {
154 | display: flex;
155 | align-items: left;
156 | justify-content: left;
157 | span {
158 | width: 250px;
159 | font-size: 1.6rem;
160 | color: #ffffff;
161 | letter-spacing: 0;
162 | margin-top: 0.7rem;
163 | }
164 | input {
165 | margin-left: 1rem;
166 | }
167 | }
168 | }
169 | }
170 |
--------------------------------------------------------------------------------
/src/components/detailedRequest/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect } from "react";
2 |
3 | import Prism from "prismjs";
4 | import "prismjs/themes/prism-dark.css";
5 | import "prismjs/components/prism-http";
6 | import "prismjs/components/prism-dns-zone-file";
7 |
8 | import { copyDataToClipboard } from "lib";
9 | import Protocol from "lib/types/protocol";
10 | import "./styles.scss";
11 | import View from "lib/types/view";
12 |
13 | import { ReactComponent as CopyIcon } from "../../assets/svg/copy.svg";
14 |
15 | interface DetailedRequestP {
16 | title: string;
17 | data: string;
18 | view: View;
19 | protocol: Protocol;
20 | }
21 |
22 | const DetailedRequest = ({ title, data, view, protocol }: DetailedRequestP) => {
23 | useEffect(() => {
24 | Prism.highlightAll();
25 | }, [data]);
26 |
27 | return (
28 |
35 |
{title}
36 |
37 |
copyDataToClipboard(data)}>
38 | Copy
39 |
40 |
41 |
42 | {`${data}`}
51 |
52 |
53 |
54 |
55 | );
56 | };
57 |
58 | export default DetailedRequest;
59 |
--------------------------------------------------------------------------------
/src/components/detailedRequest/styles.scss:
--------------------------------------------------------------------------------
1 | @import "../../breakpoints.scss";
2 | @include for-size(xsmall) {
3 | }
4 |
5 | @include for-size(small) {
6 | }
7 |
8 | @include for-size(medium) {
9 | }
10 |
11 | @include for-size(large) {
12 | }
13 |
14 | @include for-size(xlarge) {
15 | }
16 |
17 | .detailed_request_container {
18 | display: flex;
19 | flex-direction: column;
20 | margin-bottom: 3rem;
21 | height: 100%;
22 | & > span {
23 | font-size: 1.4rem;
24 | color: #ffffff;
25 | margin-bottom: 1rem;
26 | }
27 | .body {
28 | border: 1px solid rgba(151, 151, 151, 0.25);
29 | border-radius: 0.6rem;
30 | padding: 2rem 1.5rem;
31 | font-family: Menlo-Regular;
32 | font-size: 1.2rem;
33 | color: #fcc28c;
34 | line-height: 2.8rem;
35 | position: relative;
36 | height: calc(100% - 3rem);
37 | & > .pre_wrapper {
38 | height: 100%;
39 | width: 100%;
40 | overflow-y: scroll;
41 | & > .default {
42 | color: #fcc28c;
43 | }
44 | & > pre {
45 | text-shadow: none !important;
46 | box-shadow: none !important;
47 | border: none !important;
48 | background: transparent !important;
49 | & > code {
50 | text-shadow: none !important;
51 | }
52 | }
53 | }
54 | .copy_button {
55 | position: absolute;
56 | top: -1.4rem;
57 | right: 2.8rem;
58 | background: rgba(216, 216, 216, 0.14);
59 | font-size: 1.5rem;
60 | cursor: pointer;
61 | color: #ffffff;
62 | padding: 0 0.6rem;
63 | border-radius: 0.6rem;
64 | display: flex;
65 | align-items: center;
66 | user-select: none;
67 | &:active {
68 | transition: all 0.1s linear;
69 | background: rgba(216, 216, 216, 0.25);
70 | }
71 | & > svg {
72 | margin-left: 1rem;
73 | height: 1.7rem;
74 | }
75 | }
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/src/components/header/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 |
3 | import { NotificationOutlined } from "@ant-design/icons";
4 | import { matchConfig } from "@babakness/exhaustive-type-checking";
5 |
6 | import { ReactComponent as DeleteIcon } from "assets/svg/delete.svg";
7 | import { ReactComponent as DownloadIcon } from "assets/svg/download.svg";
8 | import { ReactComponent as SwitchIcon } from "assets/svg/switch.svg";
9 | import { ReactComponent as ThemeBlueButtonIcon } from "assets/svg/theme_blue_button.svg";
10 | import { ReactComponent as ThemeDarkButtonIcon } from "assets/svg/theme_dark_button.svg";
11 | import { ReactComponent as ThemeSynthButtonIcon } from "assets/svg/theme_synth_button.svg";
12 | import NotificationsPopup from "components/notificationsPopup";
13 | import ResetPopup from "components/resetPopup";
14 | import ToggleBtn from "components/toggleBtn";
15 | import { handleDataExport } from "lib";
16 | import { getStoredData, writeStoredData } from "lib/localStorage";
17 | import { ThemeName, showThemeName } from "theme";
18 | import "./styles.scss";
19 |
20 | import CustomHost from "../customHost";
21 |
22 | const themeIcon = matchConfig()({
23 | dark: () => ,
24 | synth: () => ,
25 | blue: () => ,
26 | });
27 |
28 | interface HeaderP {
29 | handleThemeSelection: (t: ThemeName) => void;
30 | theme: ThemeName;
31 | host: string;
32 | handleAboutPopupVisibility: () => void;
33 | isResetPopupDialogVisible: boolean;
34 | isNotificationsDialogVisible: boolean;
35 | isCustomHostDialogVisible: boolean;
36 | handleResetPopupDialogVisibility: () => void;
37 | handleNotificationsDialogVisibility: () => void;
38 | handleCustomHostDialogVisibility: () => void;
39 | }
40 |
41 | const Header = ({
42 | handleThemeSelection,
43 | theme,
44 | host,
45 | handleAboutPopupVisibility,
46 | isResetPopupDialogVisible,
47 | isNotificationsDialogVisible,
48 | handleResetPopupDialogVisibility,
49 | handleNotificationsDialogVisibility,
50 | isCustomHostDialogVisible,
51 | handleCustomHostDialogVisibility,
52 | }: HeaderP) => {
53 | const [isSelectorVisible, setIsSelectorVisible] = useState(false);
54 |
55 | const handleThemeSwitchesVisibility = () => {
56 | setIsSelectorVisible(!isSelectorVisible);
57 | };
58 |
59 | const setTheme = (t: ThemeName) => () => handleThemeSelection(t);
60 |
61 | const isThemeSelected = (t: ThemeName) => ThemeName.eq.equals(t, theme);
62 | const themeButtonStyle = (t: ThemeName) =>
63 | `${isSelectorVisible && "__selector_visible"} ${isThemeSelected(t) && "__selected"} ${
64 | !isSelectorVisible && "__without_bg"
65 | }`;
66 |
67 | const ThemeButton = ({ theme: t }: { theme: ThemeName }) => (
68 |
69 | {themeIcon(t)}
70 | {showThemeName.show(t)}
71 |
72 | );
73 |
74 | const data = getStoredData();
75 | const [inputData, setInputData] = useState({
76 | responseExport: data.responseExport
77 | });
78 |
79 | const handleToggleBtn = (e: any) => {
80 |
81 | const currentStoredData = getStoredData();
82 |
83 | setInputData({ ...inputData, responseExport: e.target.checked});
84 | writeStoredData({ ...currentStoredData, responseExport: e.target.checked});
85 | };
86 |
87 | return (
88 |
143 | );
144 | };
145 |
146 | export default Header;
147 |
--------------------------------------------------------------------------------
/src/components/header/styles.scss:
--------------------------------------------------------------------------------
1 | @import "../../breakpoints.scss";
2 | @include for-size(xsmall) {
3 | }
4 |
5 | @include for-size(small) {
6 | }
7 |
8 | @include for-size(medium) {
9 | }
10 |
11 | @include for-size(large) {
12 | }
13 |
14 | @include for-size(xlarge) {
15 | }
16 |
17 | .header {
18 | z-index: 10;
19 | width: 100%;
20 | height: 5.3rem;
21 | padding: 0 1.5rem;
22 | display: flex;
23 | align-items: center;
24 |
25 | & > div:nth-of-type(1) {
26 | margin-left: 0;
27 | font-size: 1.5rem;
28 | color: #ffffff;
29 | letter-spacing: -0.02rem;
30 | text-align: left;
31 | padding-right: 1rem;
32 | margin-right: 0.8rem;
33 | border-right: 1px solid rgba(255, 255, 255, 0.25);
34 | }
35 | & > div:nth-of-type(2) {
36 | letter-spacing: -0.02rem;
37 | text-align: left;
38 | padding-right: 1rem;
39 | color: #ffffff;
40 | margin-right: 0.8rem;
41 | border-right: 1px solid rgba(255, 255, 255, 0.25);
42 | }
43 | & > div:nth-of-type(3) {
44 | text-align: left;
45 | color: #ffffff;
46 | font-size: 1.4rem;
47 | padding-right: 1rem;
48 | }
49 | & > button:nth-of-type(1) {
50 | margin-left: 0;
51 | border-radius: 0.6rem;
52 | overflow: hidden;
53 | height: 2.7rem;
54 | display: flex;
55 | justify-content: stretch;
56 | .__selector_visible {
57 | display: flex;
58 | background: rgba(216, 216, 216, 0.11);
59 | }
60 | .__selected {
61 | display: flex !important;
62 | background: rgba(216, 216, 216, 0.2);
63 | }
64 | .__without_bg {
65 | background: none !important;
66 | }
67 | & > button {
68 | cursor: pointer;
69 | height: 100%;
70 | display: none;
71 | align-items: center;
72 | padding: 0 0.6rem;
73 | color: #ffffff;
74 | font-size: 1.4rem;
75 | & > svg {
76 | margin-right: 0.4rem;
77 | }
78 | }
79 | }
80 | .links {
81 | display: flex;
82 | margin-left: auto;
83 | padding: 1.5rem 0;
84 | height: 100%;
85 | align-items: center;
86 | .custom_host_active {
87 | color: #6588ff;
88 | svg path {
89 | fill: #6588ff;
90 | }
91 | }
92 | & > button {
93 | font-size: 1.5rem;
94 | color: #ffffff;
95 | letter-spacing: -0.02rem;
96 | cursor: pointer;
97 | margin-left: 2rem;
98 | display: flex;
99 | align-items: center;
100 | &:nth-of-type(4) {
101 | &:active {
102 | svg {
103 | path {
104 | fill: transparent;
105 | }
106 | g {
107 | stroke: #979797;
108 | }
109 | }
110 | }
111 | }
112 | &:active {
113 | color: #979797;
114 | svg {
115 | path {
116 | fill: #979797;
117 | }
118 | }
119 | }
120 | & > svg {
121 | margin-right: 1rem;
122 | }
123 | }
124 | & > a {
125 | margin-left: auto;
126 | font-size: 1.5rem;
127 | color: #ffffff;
128 | letter-spacing: -0.02rem;
129 | cursor: pointer;
130 | }
131 | .vertical_bar {
132 | border-right: 1px solid rgba(151, 151, 151, 0.5);
133 | height: 100%;
134 | width: 1px;
135 | margin: 0 1.5rem;
136 | }
137 | }
138 | }
139 |
--------------------------------------------------------------------------------
/src/components/notificationsPopup/index.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable jsx-a11y/no-static-element-interactions */
2 | /* eslint-disable jsx-a11y/click-events-have-key-events */
3 | import React, { useState } from "react";
4 |
5 | import { Tab } from "@headlessui/react";
6 |
7 | import { ReactComponent as ArrowRightIcon } from "assets/svg/arrow_right.svg";
8 | import { ReactComponent as CloseIcon } from "assets/svg/close.svg";
9 | import { ReactComponent as LoadingIcon } from "assets/svg/loader.svg";
10 | import "./styles.scss";
11 | import ToggleBtn from "components/toggleBtn";
12 | import { getStoredData, writeStoredData } from "lib/localStorage";
13 |
14 | interface NotificationsPopupP {
15 | handleCloseDialog: () => void;
16 | }
17 |
18 | const NotificationsPopup = ({ handleCloseDialog }: NotificationsPopupP) => {
19 | const data = getStoredData();
20 | const [isLoading, setIsLoading] = useState(false);
21 | const [inputData, setInputData] = useState({
22 | telegram: data.telegram,
23 | slack: data.slack,
24 | discord: data.discord,
25 | });
26 |
27 | const handleTelegramConfirm = () => {
28 | setIsLoading(true);
29 | const currentStoredData = getStoredData();
30 | setTimeout(() => {
31 | localStorage.clear();
32 | writeStoredData({ ...currentStoredData, telegram: inputData.telegram });
33 | setInputData({ ...inputData });
34 | setIsLoading(false);
35 | }, 500);
36 | };
37 |
38 | const handleDiscordConfirm = () => {
39 | setIsLoading(true);
40 | const currentStoredData = getStoredData();
41 | setTimeout(() => {
42 | localStorage.clear();
43 | writeStoredData({ ...currentStoredData, discord: inputData.discord });
44 | setInputData({ ...inputData });
45 | setIsLoading(false);
46 | }, 500);
47 | };
48 |
49 | const handleSlackConfirm = () => {
50 | setIsLoading(true);
51 | const currentStoredData = getStoredData();
52 | setTimeout(() => {
53 | localStorage.clear();
54 | writeStoredData({ ...currentStoredData, slack: inputData.slack });
55 | setInputData({ ...inputData });
56 | setIsLoading(false);
57 | }, 500);
58 | };
59 |
60 | const handleInput = (e: any) => {
61 | if (e.target.id === "telegram_bot_token") {
62 | setInputData({ ...inputData, telegram: { ...inputData.telegram, botToken: e.target.value } });
63 | } else if (e.target.id === "telegram_chat_id") {
64 | setInputData({ ...inputData, telegram: { ...inputData.telegram, chatId: e.target.value } });
65 | } else if (e.target.id === "slack_hook_key") {
66 | setInputData({ ...inputData, slack: { ...inputData.slack, hookKey: e.target.value } });
67 | } else if (e.target.id === "slack_channel") {
68 | setInputData({ ...inputData, slack: { ...inputData.slack, channel: e.target.value } });
69 | } else if (e.target.id === "discord_webhook") {
70 | setInputData({ ...inputData, discord: { ...inputData.discord, webhook: e.target.value } });
71 | } else if (e.target.id === "discord_channel") {
72 | setInputData({ ...inputData, discord: { ...inputData.discord, channel: e.target.value } });
73 | }
74 | };
75 |
76 | const handleToggleBtn = (e: any) => {
77 | if (e.target.name === "telegram") {
78 | setInputData({ ...inputData, telegram: { ...inputData.telegram, enabled: e.target.checked } });
79 | writeStoredData({ ...data, telegram: { ...data.telegram, enabled: e.target.checked } });
80 | } else if (e.target.name === "slack") {
81 | setInputData({ ...inputData, slack: { ...inputData.slack, enabled: e.target.checked } });
82 | writeStoredData({ ...data, slack: { ...data.slack, enabled: e.target.checked } });
83 | } else if (e.target.name === "discord") {
84 | setInputData({ ...inputData, discord: { ...inputData.discord, enabled: e.target.checked } });
85 | writeStoredData({ ...data, discord: { ...data.discord, enabled: e.target.checked } });
86 | }
87 | };
88 |
89 | return (
90 |
91 |
92 |
93 | Notifications
94 |
95 |
96 |
97 | {/*
98 |
99 | Telegram:
100 |
105 |
106 |
107 | Slack:
108 |
113 |
114 |
115 | Discord:
116 |
121 |
122 |
*/}
123 |
124 |
125 | {({ selectedIndex }) => (
126 | <>
127 |
134 |
140 | Telegram
141 |
142 |
147 |
148 |
155 |
161 | Slack
162 |
163 |
168 |
169 |
176 |
182 | Discord
183 |
184 |
189 |
190 | >
191 | )}
192 |
193 |
194 |
195 |
202 |
209 |
210 |
221 | Confirm
222 | {isLoading ? : }
223 |
224 |
225 |
226 |
227 |
234 |
241 |
242 |
251 | Confirm
252 | {isLoading ? : }
253 |
254 |
255 |
256 |
257 |
264 |
271 |
272 |
281 | Confirm
282 | {isLoading ? : }
283 |
284 |
285 |
286 |
287 |
288 |
289 | {/*
290 | Please confirm the action, this action can’t be undone and all the client data will be
291 | deleted immediately. You can download a copy of your data in JSON format by clicking the
292 | Export button below or in top right.
293 |
294 |
295 |
296 | Export
297 |
298 |
299 |
300 |
306 | Confirm {isLoading ? : }
307 |
308 |
*/}
309 |
310 |
311 | );
312 | };
313 |
314 | export default NotificationsPopup;
315 |
--------------------------------------------------------------------------------
/src/components/notificationsPopup/styles.scss:
--------------------------------------------------------------------------------
1 | @import "../../breakpoints.scss";
2 | @include for-size(xsmall) {
3 | }
4 |
5 | @include for-size(small) {
6 | }
7 |
8 | @include for-size(medium) {
9 | }
10 |
11 | @include for-size(large) {
12 | }
13 |
14 | @include for-size(xlarge) {
15 | }
16 |
17 | .backdrop_container {
18 | width: 100vw;
19 | height: 100vh;
20 | position: absolute;
21 | top: 0;
22 | left: 0;
23 | z-index: 1000;
24 | background: rgba(36, 13, 44, 0.2);
25 | display: flex;
26 | align-items: center;
27 | justify-content: center;
28 |
29 | .dialog_box {
30 | background: #000000;
31 | border-radius: 0.6rem;
32 | width: 45%;
33 | padding: 3.5rem 4rem;
34 | display: flex;
35 | flex-direction: column;
36 |
37 | .header {
38 | display: flex;
39 | margin-bottom: 2rem;
40 | padding: 0;
41 | span {
42 | font-size: 2.2rem;
43 | color: #ffffff;
44 | letter-spacing: 0;
45 | }
46 | svg {
47 | margin-left: auto;
48 | height: 3rem;
49 | cursor: pointer;
50 | }
51 | }
52 | .body {
53 | color: #ffffff;
54 | display: flex;
55 | flex-direction: column;
56 | gap: 30px;
57 |
58 | .toggle_btns {
59 | display: flex;
60 | justify-content: space-between;
61 | gap: 20px;
62 | font-size: 16px;
63 | .toggle_btn {
64 | gap: 15px;
65 | display: flex;
66 | align-items: center;
67 | }
68 | }
69 | .tab_list {
70 | font-size: 16px;
71 | height: 40px;
72 | display: flex;
73 | border-right: none;
74 |
75 | .tab {
76 | flex: 1;
77 | display: flex;
78 | align-items: center;
79 | gap: 15px;
80 | justify-content: center;
81 | border-bottom: 1px solid white;
82 | transform: translateX(1px);
83 | padding: 0 5px;
84 | z-index: 10;
85 | position: relative;
86 | transition: 0.15s all;
87 |
88 | &:focus {
89 | outline: none;
90 | }
91 |
92 | & > div {
93 | transition: 0.15s all;
94 | }
95 | }
96 | }
97 | .panel {
98 | display: flex;
99 | flex-direction: column;
100 | padding: 0 20px;
101 | div {
102 | display: flex;
103 | button {
104 | &:disabled {
105 | opacity: 0.5;
106 | cursor: default;
107 | }
108 | }
109 | .remove_button {
110 | display: flex;
111 | align-items: center;
112 | justify-content: center;
113 | background: rgba(216, 216, 216, 0.19);
114 | border-radius: 0.6rem;
115 | font-size: 1.4rem;
116 | color: #ffffff;
117 | letter-spacing: 0;
118 | text-align: left;
119 | height: 3.5rem;
120 | margin-right: 2rem;
121 | flex: 1;
122 | transition: all 0.1s linear;
123 | svg {
124 | margin-left: 1.5rem;
125 | }
126 | }
127 | .submit_button {
128 | flex: 2;
129 | display: flex;
130 | align-items: center;
131 | justify-content: center;
132 | background: #3254c5;
133 | border-radius: 0.6rem;
134 | font-size: 1.4rem;
135 | color: #ffffff;
136 | letter-spacing: 0;
137 | text-align: left;
138 | height: 3.5rem;
139 | transition: all 0.1s linear;
140 | svg {
141 | margin-left: 1.5rem;
142 | }
143 | }
144 | }
145 | }
146 | }
147 | & > span {
148 | width: 100%;
149 | font-size: 1.6rem;
150 | color: #ffffff;
151 | letter-spacing: 0;
152 | margin-bottom: 3.5rem;
153 | }
154 | .buttons {
155 | display: flex;
156 | font-weight: 600;
157 | margin-top: 1rem;
158 | button {
159 | &:disabled {
160 | opacity: 0.5;
161 | }
162 | }
163 | .button {
164 | flex: 2;
165 | display: flex;
166 | align-items: center;
167 | justify-content: center;
168 | background: #3254c5;
169 | border-radius: 0.6rem;
170 | font-size: 1.4rem;
171 | color: #ffffff;
172 | letter-spacing: 0;
173 | text-align: left;
174 | height: 3.5rem;
175 | transition: all 0.1s linear;
176 | svg {
177 | margin-left: 1rem;
178 | margin-top: -0.3rem;
179 | }
180 | }
181 | .confirm_button {
182 | flex: 2;
183 | display: flex;
184 | align-items: center;
185 | justify-content: center;
186 | background: #ff5151;
187 | border-radius: 0.6rem;
188 | font-size: 1.4rem;
189 | color: #ffffff;
190 | letter-spacing: 0;
191 | text-align: left;
192 | height: 3.5rem;
193 | transition: all 0.1s linear;
194 | svg {
195 | margin-left: 1rem;
196 | margin-top: -0.3rem;
197 | }
198 | }
199 | }
200 | }
201 | }
202 |
--------------------------------------------------------------------------------
/src/components/requestsTable/index.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable jsx-a11y/no-static-element-interactions */
2 | /* eslint-disable jsx-a11y/click-events-have-key-events */
3 | import React, { useEffect, useState } from "react";
4 |
5 | import formatDistance from "date-fns/formatDistance";
6 | import { now } from "fp-ts/Date";
7 | import { pipe } from "fp-ts/function";
8 |
9 | import { ReactComponent as FilterIcon } from "assets/svg/filter.svg";
10 | import { ReactComponent as FilterSelectedIcon } from "assets/svg/filter_selected.svg";
11 | import { getStoredData, writeStoredData } from "lib/localStorage";
12 | import Data, { filterByProtocols } from "lib/types/data";
13 | import Filter from "lib/types/filter";
14 | import Protocol, { protocols } from "lib/types/protocol";
15 | import { trueKeys } from "lib/utils";
16 |
17 | import "./styles.scss";
18 |
19 | interface RequestsTableP {
20 | data: Data[];
21 | handleRowClick: (id: string) => void;
22 | selectedInteraction: string;
23 | filter: Filter;
24 | }
25 |
26 | const RequestsTable = ({ data, handleRowClick, selectedInteraction, filter }: RequestsTableP) => {
27 | const [filteredData, setFilteredData] = useState(data);
28 | const [filterDropdownVisibility, setFilterDropdownVisibility] = useState(false);
29 | const [filterValue, setFilterValue] = useState(filter);
30 |
31 | const isFiltered = pipe(filterValue, trueKeys).length !== protocols.length;
32 |
33 | const filterData = (f: Filter) => filterByProtocols(trueKeys(f))(data);
34 |
35 | useEffect(() => {
36 | setFilteredData(filterData(filterValue));
37 | }, [data]);
38 |
39 | const handleFilterDropdownVisibility = () => {
40 | const dropdownElement = document.getElementById("filter_dropdown");
41 | setFilterDropdownVisibility(!filterDropdownVisibility);
42 | document.addEventListener("click", (e: any) => {
43 | const isClickInsideElement = dropdownElement?.contains(e.target);
44 | if (!isClickInsideElement) {
45 | setFilterDropdownVisibility(false);
46 | }
47 | });
48 | };
49 |
50 | useEffect(() => {
51 | window.addEventListener("storage", () => {
52 | setFilterValue(getStoredData().filter);
53 | });
54 | }, []);
55 |
56 | const handleFilterSelection = (e: any) => {
57 | const newFilterValue: typeof filterValue = {
58 | ...filterValue,
59 | [e.target.value]: e.target.checked,
60 | };
61 |
62 | setFilterValue(newFilterValue);
63 | writeStoredData({ ...getStoredData(), filter: newFilterValue });
64 |
65 | setFilteredData(filterData(newFilterValue));
66 | };
67 |
68 | return (
69 |
70 |
71 |
72 | #
73 | TIME
74 |
75 |
76 |
80 | TYPE
81 | {isFiltered ? : }
82 |
83 | {filterDropdownVisibility && (
84 |
104 | )}
105 |
106 |
107 |
108 |
109 |
110 | {filteredData.map((item, i) => (
111 | handleRowClick(item.id)}
114 | className={item.id === selectedInteraction ? "selected_row" : ""}
115 | >
116 | {filteredData.length - i}
117 |
118 | {formatDistance(new Date(item.timestamp), now(), {
119 | addSuffix: true,
120 | })}
121 |
122 | {item.protocol}
123 |
124 | ))}
125 |
126 |
127 | );
128 | };
129 |
130 | export default RequestsTable;
131 |
--------------------------------------------------------------------------------
/src/components/requestsTable/styles.scss:
--------------------------------------------------------------------------------
1 | @import "../../breakpoints.scss";
2 | @include for-size(xsmall) {
3 | }
4 |
5 | @include for-size(small) {
6 | }
7 |
8 | @include for-size(medium) {
9 | }
10 |
11 | @include for-size(large) {
12 | }
13 |
14 | @include for-size(xlarge) {
15 | }
16 |
17 | .requests_table {
18 | width: 100%;
19 | color: #fff;
20 | text-align: left;
21 | height: 100%;
22 | border-spacing: 0;
23 |
24 | & > tbody {
25 | display: block;
26 | height: calc(100% - 5.5rem);
27 | overflow: auto;
28 | font-size: 1.5rem;
29 | color: #ffffff;
30 | tr {
31 | cursor: pointer;
32 | display: flex;
33 | td {
34 | border-bottom: 0.1rem solid rgba(151, 151, 151, 0.25);
35 | padding: 0 1.5rem;
36 | height: 6rem;
37 | &:nth-of-type(1) {
38 | width: 15%;
39 | }
40 | &:nth-of-type(3) {
41 | width: 20%;
42 | }
43 | }
44 | }
45 | }
46 | & > thead,
47 | & > tbody tr {
48 | display: table;
49 | width: 100%;
50 | table-layout: fixed;
51 | }
52 | & > thead {
53 | font-size: 1.1rem;
54 | color: #ffffff;
55 | th {
56 | height: 5.5rem;
57 | padding: 0 1.5rem;
58 | &:nth-of-type(1) {
59 | width: 15%;
60 | }
61 | &:nth-of-type(3) {
62 | width: 20%;
63 | cursor: pointer;
64 | }
65 | & > div {
66 | position: relative;
67 | .__filtered {
68 | color: #3254c5;
69 | }
70 | & > div {
71 | display: flex;
72 | align-items: center;
73 | svg {
74 | margin-left: 1rem;
75 | margin-top: -0.2rem;
76 | }
77 | }
78 |
79 | .filter_dropdown {
80 | position: absolute;
81 | top: 2rem;
82 | right: -0.5rem;
83 | padding: 1.5rem 1.5rem 0 1.5rem;
84 | font-size: 1.4rem;
85 | color: #ffffff;
86 | border-radius: 0.6rem;
87 | li {
88 | margin-bottom: 1.5rem;
89 | width: 100%;
90 | display: block;
91 | position: relative;
92 | padding-left: 3.5rem;
93 | cursor: pointer;
94 | font-size: 1.4rem;
95 | label:hover input ~ .checkmark {
96 | background-color: #2f2f2f;
97 | }
98 |
99 | /* When the checkbox is checked, add a blue background */
100 |
101 | label {
102 | user-select: none;
103 | cursor: pointer;
104 | align-items: center;
105 | display: flex;
106 |
107 | input {
108 | position: absolute;
109 | opacity: 0;
110 | cursor: pointer;
111 | height: 0;
112 | width: 0;
113 | margin-right: 1.5rem;
114 | &:checked ~ .checkmark {
115 | border: 0.1rem solid #3254c5;
116 | }
117 | }
118 | .checkmark {
119 | position: absolute;
120 | transition: background 0.1s linear;
121 | top: 0;
122 | left: 0;
123 | height: 2rem;
124 | width: 2.2rem;
125 | border-radius: 0.6rem;
126 | background-color: #1e1e1e;
127 | &::after {
128 | content: "";
129 | position: absolute;
130 | display: none;
131 | }
132 | &::after {
133 | left: 0.8rem;
134 | top: 0.2rem;
135 | width: 0.5rem;
136 | height: 1rem;
137 | border: solid white;
138 | border-width: 0 0.1rem 0.1rem 0;
139 | -webkit-transform: rotate(45deg);
140 | -ms-transform: rotate(45deg);
141 | transform: rotate(45deg);
142 | }
143 | }
144 | :checked ~ .checkmark:after {
145 | display: block;
146 | }
147 | }
148 | }
149 | }
150 | }
151 | }
152 | }
153 | }
154 | .placeholder_row {
155 | height: 100%;
156 | width: 100%;
157 | display: flex;
158 | align-items: center;
159 | justify-content: center;
160 | color: #6a6a6a;
161 | font-size: 1.4rem;
162 | }
163 | .selected_row {
164 | background: rgba(216, 216, 216, 0.07);
165 | }
166 |
--------------------------------------------------------------------------------
/src/components/resetPopup/index.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable jsx-a11y/no-static-element-interactions */
2 | /* eslint-disable jsx-a11y/click-events-have-key-events */
3 | import React, { useState } from "react";
4 |
5 | import { ReactComponent as CloseIcon } from "assets/svg/close.svg";
6 | import { ReactComponent as DeleteIcon } from "assets/svg/delete.svg";
7 | import { ReactComponent as DownloadIcon } from "assets/svg/download.svg";
8 | import { ReactComponent as LoadingIcon } from "assets/svg/loader.svg";
9 | import "./styles.scss";
10 | import { handleDataExport, register } from "lib";
11 | import { getStoredData, writeStoredData } from "lib/localStorage";
12 |
13 | interface CustomHostP {
14 | handleCloseDialog: () => void;
15 | }
16 |
17 | const ResetPopup = ({ handleCloseDialog }: CustomHostP) => {
18 | const [isLoading, setIsLoading] = useState(false);
19 |
20 | const handleConfirm = () => {
21 | setIsLoading(true);
22 | const currentStoredData = getStoredData();
23 | setTimeout(() => {
24 | register(currentStoredData.host, currentStoredData.token, true, false)
25 | .then((d) => {
26 | setIsLoading(false);
27 | localStorage.clear();
28 | writeStoredData(d);
29 | handleCloseDialog();
30 | window.location.reload();
31 | })
32 | .catch(() => {
33 | setIsLoading(false);
34 | });
35 | }, 50);
36 | };
37 |
38 | return (
39 |
40 |
41 |
42 | Reset interactsh.com
43 |
44 |
45 |
46 | Please confirm the action, this action can’t be undone and all the client data will be
47 | deleted immediately. You can download a copy of your data in JSON format by clicking the
48 | Export button below or in top right.
49 |
50 |
51 |
52 | Export
53 |
54 |
55 |
56 |
62 | Confirm {isLoading ? : }
63 |
64 |
65 |
66 |
67 | );
68 | };
69 |
70 | export default ResetPopup;
71 |
--------------------------------------------------------------------------------
/src/components/resetPopup/styles.scss:
--------------------------------------------------------------------------------
1 | @import "../../breakpoints.scss";
2 | @include for-size(xsmall) {
3 | }
4 |
5 | @include for-size(small) {
6 | }
7 |
8 | @include for-size(medium) {
9 | }
10 |
11 | @include for-size(large) {
12 | }
13 |
14 | @include for-size(xlarge) {
15 | }
16 |
17 | .backdrop_container {
18 | width: 100vw;
19 | height: 100vh;
20 | position: absolute;
21 | top: 0;
22 | left: 0;
23 | z-index: 1000;
24 | background: rgba(36, 13, 44, 0.2);
25 | display: flex;
26 | align-items: center;
27 | justify-content: center;
28 |
29 | .dialog_box {
30 | background: #000000;
31 | border-radius: 0.6rem;
32 | width: 45%;
33 | padding: 3.5rem 4rem;
34 | display: flex;
35 | flex-direction: column;
36 |
37 | .header {
38 | display: flex;
39 | margin-bottom: 2rem;
40 | padding: 0;
41 | span {
42 | font-size: 2.2rem;
43 | color: #ffffff;
44 | letter-spacing: 0;
45 | }
46 | svg {
47 | margin-left: auto;
48 | height: 3rem;
49 | cursor: pointer;
50 | }
51 | }
52 | & > span {
53 | width: 100%;
54 | font-size: 1.6rem;
55 | color: #ffffff;
56 | letter-spacing: 0;
57 | margin-bottom: 3.5rem;
58 | }
59 | .buttons {
60 | display: flex;
61 | font-weight: 600;
62 | margin-top: 1rem;
63 | button {
64 | &:disabled {
65 | opacity: 0.5;
66 | }
67 | }
68 | .button {
69 | flex: 2;
70 | display: flex;
71 | align-items: center;
72 | justify-content: center;
73 | background: #3254c5;
74 | border-radius: 0.6rem;
75 | font-size: 1.4rem;
76 | color: #ffffff;
77 | letter-spacing: 0;
78 | text-align: left;
79 | height: 3.5rem;
80 | transition: all 0.1s linear;
81 | svg {
82 | margin-left: 1rem;
83 | margin-top: -0.3rem;
84 | }
85 | }
86 | .confirm_button {
87 | flex: 2;
88 | display: flex;
89 | align-items: center;
90 | justify-content: center;
91 | background: #ff5151;
92 | border-radius: 0.6rem;
93 | font-size: 1.4rem;
94 | color: #ffffff;
95 | letter-spacing: 0;
96 | text-align: left;
97 | height: 3.5rem;
98 | transition: all 0.1s linear;
99 | svg {
100 | margin-left: 1rem;
101 | margin-top: -0.3rem;
102 | }
103 | }
104 | }
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/src/components/tabSwitcher/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 |
3 | import "./styles.scss";
4 | import { ReactComponent as CrossIcon } from "assets/svg/cross.svg";
5 | import { ReactComponent as PlusIcon } from "assets/svg/plus.svg";
6 | import { ReactComponent as RefreshIcon } from "assets/svg/refresh.svg";
7 | import Tab from "lib/types/tab";
8 |
9 | interface TabSwitcherP {
10 | handleTabButtonClick: (tab: Tab) => void;
11 | selectedTab: Tab;
12 | handleAddNewTab: () => void;
13 | data: Tab[];
14 | handleDeleteTab: (tab: Tab) => void;
15 | handleTabRename: React.ChangeEventHandler;
16 | processPolledData: () => void;
17 | }
18 | const TabSwitcher = ({
19 | handleTabButtonClick,
20 | selectedTab,
21 | handleAddNewTab,
22 | data,
23 | handleDeleteTab,
24 | handleTabRename,
25 | processPolledData,
26 | }: TabSwitcherP) => {
27 | const [isInputVisible, setIsInputVisible] = useState(false);
28 |
29 | const handleTabButtonClickTemp = (item: Tab) => {
30 | handleTabButtonClick(item);
31 | setIsInputVisible(false);
32 | };
33 | const handleTabRanameDone = (e: React.KeyboardEvent) => {
34 | if (e.keyCode === 13) {
35 | setIsInputVisible(false);
36 | }
37 | };
38 | const handleTabButtonDoubleClick = (id: string) => {
39 | if (!isInputVisible) {
40 | setIsInputVisible(true);
41 | setTimeout(() => {
42 | document.getElementById(id.toString())?.focus();
43 | }, 200);
44 | }
45 | };
46 |
47 | return (
48 | <>
49 |
50 | {data.length !== 0 &&
51 | data.map((item) => (
52 |
57 | !Tab.eq.equals(selectedTab, item) ? handleTabButtonClickTemp(item) : ""
58 | }
59 | onDoubleClick={() => handleTabButtonDoubleClick(item["unique-id"])}
60 | className={`tab_button ${
61 | Tab.eq.equals(selectedTab, item) && "__selected_tab_button"
62 | }`}
63 | >
64 | {isInputVisible && Tab.eq.equals(selectedTab, item) ? (
65 |
70 | ) : (
71 | {item.name}
72 | )}
73 | handleDeleteTab(item)} />
74 |
75 | ))}
76 |
77 |
78 |
79 |
80 |
81 | Refresh
82 |
83 |
84 | >
85 | );
86 | };
87 |
88 | export default TabSwitcher;
89 |
--------------------------------------------------------------------------------
/src/components/tabSwitcher/styles.scss:
--------------------------------------------------------------------------------
1 | @import "../../breakpoints.scss";
2 | @include for-size(xsmall) {
3 | }
4 |
5 | @include for-size(small) {
6 | }
7 |
8 | @include for-size(medium) {
9 | }
10 |
11 | @include for-size(large) {
12 | }
13 |
14 | @include for-size(xlarge) {
15 | }
16 |
17 | .tab_switcher {
18 | width: 100%;
19 | height: 3.7rem;
20 | display: flex;
21 | background: #192030;
22 | justify-content: stretch;
23 |
24 | .__selected_tab_button {
25 | background: rgba(216, 216, 216, 0.12);
26 | margin: 0 !important;
27 | height: 100% !important;
28 | border-right: 1px solid transparent !important;
29 | }
30 | .tab_button {
31 | height: calc(100% - 1rem);
32 | cursor: pointer;
33 | margin: 0.5rem 0;
34 | padding: 0 1rem;
35 | display: flex;
36 | align-items: center;
37 | border-right: 1px solid rgba(151, 151, 151, 0.5);
38 | font-size: 1.2rem;
39 | & > input {
40 | border: none;
41 | outline: none;
42 | font-size: inherit;
43 | background: transparent;
44 | color: #ffffff;
45 | }
46 | & > div {
47 | margin-right: 1rem;
48 | color: #ffffff;
49 | text-align: left;
50 | max-width: 11rem;
51 | white-space: nowrap;
52 | text-overflow: ellipsis;
53 | overflow: hidden;
54 | }
55 | & > svg {
56 | margin-left: auto;
57 | cursor: pointer;
58 | transition: all 0.1s linear;
59 | &:active {
60 | transform: scale(0.9);
61 | }
62 | }
63 | }
64 | .add_new_tab_button {
65 | height: 100%;
66 | width: auto;
67 | display: flex;
68 | padding: 0 1.5rem;
69 | align-items: center;
70 | cursor: pointer;
71 | }
72 | .refresh_button {
73 | display: flex;
74 | align-items: center;
75 | height: 100%;
76 | padding: 0 1.5rem;
77 | background: transparent;
78 | margin-left: auto;
79 | cursor: pointer;
80 | &:active {
81 | & > svg {
82 | transform: rotate(90deg);
83 | path {
84 | fill: #979797;
85 | }
86 | }
87 | & > span {
88 | color: #979797;
89 | }
90 | }
91 | & > span {
92 | font-size: 1.5rem;
93 | color: #ffffff;
94 | transition: all 0.1s linear;
95 | user-select: none;
96 | }
97 | & > svg {
98 | transition: all 0.1s linear;
99 | margin-right: 1rem;
100 | }
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/src/components/toggleBtn/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import './styles.scss'
3 |
4 | interface ToggleBtnP { name: string, onChangeHandler: any, value: boolean }
5 | const ToggleBtn = ({ name, onChangeHandler, value }: ToggleBtnP) => (
6 | // eslint-disable-next-line jsx-a11y/label-has-associated-control
7 |
8 |
9 |
10 |
11 | )
12 |
13 | export default ToggleBtn;
--------------------------------------------------------------------------------
/src/components/toggleBtn/styles.scss:
--------------------------------------------------------------------------------
1 | .switch {
2 | position: relative;
3 | display: inline-block;
4 | width: 45px;
5 | height: 24px;
6 | input {
7 | opacity: 0;
8 | width: 0;
9 | height: 0!important;
10 | &:checked + .slider {
11 | background-color: rgba(58, 57, 57, 1);
12 | }
13 | &:focus + .slider {
14 | box-shadow: 0 0 1px #2196f3;
15 | }
16 | &:checked + .slider:before {
17 | background-color: #36AE7C;
18 | -webkit-transform: translateX(17px);
19 | -ms-transform: translateX(17px);
20 | transform: translateX(17px);
21 | }
22 | }
23 | .slider {
24 | position: absolute;
25 | cursor: pointer;
26 | top: 0;
27 | left: 0;
28 | right: 0;
29 | bottom: 0;
30 | background-color: rgba(58, 57, 57, 0.7);
31 | -webkit-transition: 0.4s;
32 | transition: 0.4s;
33 | }
34 |
35 | .slider:before {
36 | position: absolute;
37 | content: "";
38 | height: 18px;
39 | width: 18px;
40 | left: 5px;
41 | bottom: 3px;
42 | background-color: rgb(83, 81, 81);
43 | -webkit-transition: 0.4s;
44 | transition: 0.4s;
45 | }
46 |
47 | .slider.round {
48 | border-radius: 34px;
49 | }
50 |
51 | .slider.round:before {
52 | border-radius: 50%;
53 | }
54 | }
55 |
--------------------------------------------------------------------------------
/src/globalStyles.js:
--------------------------------------------------------------------------------
1 | import { createGlobalStyle } from "styled-components";
2 |
3 | export const GlobalStyles = createGlobalStyle`
4 | body {
5 | background: ${({ theme }) => theme.background};
6 | }
7 | .secondary_bg {
8 | background: ${({ theme }) => theme.secondaryBackground}!important;
9 | }
10 | .light_bg {
11 | background: ${({ theme }) => theme.lightBackground}!important;
12 | }
13 | `;
14 |
15 | export default {};
16 |
--------------------------------------------------------------------------------
/src/helpers/fallback-loaders.js:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import { RedoOutlined } from '@ant-design/icons';
4 | import ContentLoader from "react-content-loader";
5 | import "./styles.scss";
6 |
7 | const OverviewStatsErrorFallback = ({ retry }) => (
8 |
15 |
16 |
17 |
18 | Request failed!
19 |
20 | );
21 | const StarsOvertimeErrorFallback = ({ retry }) => (
22 |
29 |
30 |
31 |
32 | Request failed!
33 |
34 | );
35 | const DailyStarsErrorFallback = ({ retry }) => (
36 |
43 |
44 |
45 |
46 | Request failed!
47 |
48 | );
49 | const FiltersErrorFallback = ({ retry }) => (
50 |
57 |
58 |
59 |
60 | Request failed!
61 |
62 | );
63 | const StargazersListErrorFallback = ({ retry }) => (
64 |
71 |
72 |
73 |
74 | Request failed!
75 |
76 | );
77 | const ContributorsListErrorFallback = ({ retry }) => (
78 |
85 |
86 |
87 |
88 | Request failed!
89 |
90 | );
91 | const RepositoriesListErrorFallback = ({ retry }) => (
92 |
99 |
100 |
101 |
102 | Request failed!
103 |
104 | );
105 | const EfficiencyStatsErrorFallback = ({ retry }) => (
106 |
113 |
114 |
115 |
116 | Request failed!
117 |
118 | );
119 | const RepoSidebarListErrorFallback = ({ retry }) => (
120 |
127 |
128 |
129 |
130 | Request failed!
131 |
132 | );
133 | const IssuesListErrorFallback = ({ retry }) => (
134 |
141 |
142 |
143 |
144 | Request failed!
145 |
146 | );
147 | const RepoStatsErrorFallback = ({ retry }) => (
148 |
155 |
156 |
157 |
158 | Request failed!
159 |
160 | );
161 |
162 | const OverviewStatsFallback = () => (
163 |
171 |
172 |
173 | );
174 | const StarsOvertimeFallback = () => (
175 |
183 |
184 |
185 | );
186 | const DailyStarsFallback = () => (
187 |
195 |
196 |
197 | );
198 | const FiltersFallback = () => (
199 |
207 |
208 |
209 | );
210 | const StargazersListFallback = () => (
211 |
219 |
220 |
221 |
222 |
223 |
224 |
225 |
226 |
227 |
228 |
229 | );
230 | const ContributorsListFallback = () => (
231 |
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
247 |
248 |
249 | );
250 | const RepositoriesListFallback = () => (
251 |
259 |
260 |
261 |
262 |
263 |
264 |
265 |
266 |
267 |
268 |
269 | );
270 | const EfficiencyStatsFallback = () => (
271 |
279 |
287 |
288 | );
289 | const RepoSidebarListFallback = () => (
290 |
298 |
299 |
300 |
301 |
302 |
303 |
304 |
305 |
306 |
307 |
308 | );
309 | const IssuesListFallback = () => (
310 |
318 |
319 |
320 |
321 |
322 |
323 |
324 |
325 |
326 |
327 |
328 | );
329 | const RepoStatsFallback = () => (
330 |
338 |
339 |
340 |
341 |
342 | );
343 |
344 | export {
345 | OverviewStatsErrorFallback,
346 | StarsOvertimeErrorFallback,
347 | DailyStarsErrorFallback,
348 | FiltersErrorFallback,
349 | StargazersListErrorFallback,
350 | ContributorsListErrorFallback,
351 | RepositoriesListErrorFallback,
352 | EfficiencyStatsErrorFallback,
353 | RepoSidebarListErrorFallback,
354 | IssuesListErrorFallback,
355 | RepoStatsErrorFallback,
356 | OverviewStatsFallback,
357 | StarsOvertimeFallback,
358 | DailyStarsFallback,
359 | FiltersFallback,
360 | StargazersListFallback,
361 | ContributorsListFallback,
362 | RepositoriesListFallback,
363 | EfficiencyStatsFallback,
364 | RepoSidebarListFallback,
365 | IssuesListFallback,
366 | RepoStatsFallback,
367 | };
368 |
--------------------------------------------------------------------------------
/src/helpers/styles.scss:
--------------------------------------------------------------------------------
1 | .retry_error_fallback {
2 | background: rgba(216, 216, 216, 0.03);
3 | display: flex;
4 | justify-content: center;
5 | border-radius: 0.2rem;
6 | align-items: center;
7 | flex-direction: column;
8 | .error_box {
9 | background: #ffffff;
10 | height: 5rem;
11 | width: 5rem;
12 | border-radius: 50%;
13 | display: flex;
14 | align-items: center;
15 | justify-content: center;
16 | padding: 1rem;
17 | cursor: pointer;
18 | box-shadow: 0 0 1rem 0.2rem rgba(0, 0, 0, 0.2);
19 | margin-bottom: 1.5rem;
20 | transition: 0.1s all ease-out;
21 | &:active {
22 | box-shadow: 0 0 0.5rem 0.15rem rgba(0, 0, 0, 0.2);
23 | }
24 | i {
25 | font-size: 2rem;
26 | font-weight: 600;
27 | color: #1f2239;
28 | cursor: pointer;
29 | transform: rotateX(180deg);
30 | display: flex;
31 | align-items: center;
32 | align-items: center;
33 | }
34 | }
35 | .error_msg {
36 | width: 100%;
37 | font-size: 1.8rem;
38 | text-align: center;
39 | color: #000000;
40 | line-height: 2.4rem;
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/src/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | import {inject} from "@vercel/analytics";
4 | import ReactDOM from "react-dom";
5 | import ReactGA from "react-ga";
6 | import { HashRouter as Router, Route, Switch, withRouter } from "react-router-dom";
7 | import "./styles.scss";
8 | import { CSSTransition, TransitionGroup } from "react-transition-group";
9 |
10 | import HomePage from "./pages/homePage";
11 | import TermsPage from "./pages/termsPage";
12 | import reportWebVitals from './reportWebVitals';
13 | import { sendToVercelAnalytics } from './vitals';
14 |
15 | const trackingId = "UA-165996103-1";
16 | ReactGA.initialize(trackingId);
17 | ReactGA.pageview(window.location.pathname + window.location.search);
18 | ReactGA.set({
19 | config: trackingId,
20 | js: new Date(),
21 | });
22 | inject()
23 |
24 | const AnimatedSwitch = withRouter(({ location }) => {
25 | window.scrollTo(0, 0);
26 | document.getElementsByTagName("html")[0].style.overflow = "visible";
27 | return (
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 | );
37 | });
38 | ReactDOM.render(
39 |
40 |
41 | ,
42 | document.getElementById("root")
43 | );
44 |
45 | reportWebVitals(sendToVercelAnalytics);
46 |
47 | if (module.hot) {
48 | module.hot.accept();
49 | }
50 |
--------------------------------------------------------------------------------
/src/lib/index.test.ts:
--------------------------------------------------------------------------------
1 | import isURL from 'validator/lib/isURL';
2 |
3 | import { generateUrl } from '.'
4 |
5 | describe("utils", () => {
6 | test("generateUrl", () => {
7 | const ret = generateUrl("id", 1, "google.com").url;
8 | expect(isURL(ret)).toBeTruthy()
9 | })
10 | })
11 |
--------------------------------------------------------------------------------
/src/lib/index.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable prefer-destructuring */
2 | import crypto from "crypto";
3 |
4 | import format from "date-fns/format";
5 | import * as E from "fp-ts/Either";
6 | import { pipe } from "fp-ts/function";
7 | import * as J from "fp-ts/Json";
8 | import * as O from "fp-ts/Option";
9 | import * as R from "fp-ts/Record";
10 | import downloadData from "js-file-download";
11 | import NodeRSA from "node-rsa";
12 | import { v4 as uuidv4 } from "uuid";
13 |
14 | import { generateRandomString } from "lib/utils";
15 |
16 | import { getStoredData, writeStoredData } from "./localStorage";
17 | import Data from "./types/data";
18 | import { defaultFilter } from "./types/filter";
19 | import { StoredData } from "./types/storedData";
20 | import Tab from "./types/tab";
21 |
22 | export const copyDataToClipboard = (data: string) => navigator.clipboard.writeText(data);
23 |
24 | export const generateUrl = (correlationId: string, correlationIdNonceLength: number, incrementNumber: number, host: string) => {
25 | const timestamp = Math.floor(Date.now() / 1000);
26 | const increment = incrementNumber;
27 | const arr = new ArrayBuffer(8);
28 | const view = new DataView(arr);
29 | view.setUint32(0, timestamp, false);
30 | view.setUint32(4, increment, false);
31 | const randomId = generateRandomString(correlationIdNonceLength);
32 | const url = `${correlationId}${randomId}.${host}`;
33 | const uniqueId = `${correlationId}${randomId}`;
34 | return { url, uniqueId };
35 | };
36 |
37 | export const clearIntervals = () => {
38 | const currentTimeoutId = setTimeout(() => {
39 | let id = Number(currentTimeoutId);
40 | for (id; id > 0; id -= 1) {
41 | window.clearInterval(id);
42 | }
43 | }, 11);
44 | };
45 |
46 | interface PolledData {
47 | error?: any;
48 | aes_key: string;
49 | data: string[];
50 | }
51 |
52 | export const decryptAESKey = (privateKey: string, aesKey: string) => {
53 | const key = new NodeRSA({ b: 2048 });
54 | key.setOptions({
55 | environment: "browser",
56 | encryptionScheme: {
57 | hash: "sha256",
58 | scheme: "pkcs1_oaep", // TODO: Ensure that this is correct.
59 | },
60 | });
61 | key.importKey(privateKey, "pkcs8-private");
62 | return key.decrypt(aesKey, "base64");
63 | };
64 |
65 | export const processData = (aesKey: string, polledData: PolledData) => {
66 | const { data } = polledData;
67 | let parsedData: Data[] = [];
68 | if (data && data.length > 0) {
69 | const decryptedData: string[] = data.map((item) => {
70 | const iv = Buffer.from(item, "base64").slice(0, 16);
71 | const key = Buffer.from(aesKey, "base64");
72 | const decipher = crypto.createDecipheriv("aes-256-cfb", key, iv);
73 | let mystr: any = decipher.update(Buffer.from(item, "base64").slice(16));
74 | mystr += decipher.final("utf8");
75 | const test: string = mystr;
76 | return test;
77 | });
78 | parsedData = decryptedData.map((item) => ({
79 | ...JSON.parse(item),
80 | id: uuidv4(),
81 | }));
82 | }
83 |
84 | return parsedData;
85 | };
86 |
87 | const getData = (key: string) =>
88 | pipe(
89 | O.tryCatch(() => localStorage.getItem(key)),
90 | O.chain(O.fromNullable)
91 | );
92 |
93 | export const handleResponseExport = (item : any) => {
94 | const fileName = `${format(Date.now(), "yyyy-MM-dd_hh:mm")}_${item.protocol}_${item['remote-address']}_${item['full-id']}_${item.id}.txt`;
95 | downloadData(item['raw-request'], fileName);
96 | }
97 |
98 | export const handleDataExport = () => {
99 | const values = pipe(
100 | R.mapWithIndex((key) => ({ key, data: getData(key) }))(localStorage),
101 | R.filterMap((x) => x.data),
102 | J.stringify,
103 | E.getOrElse(() => "An error occured") // TODO: Handle error case.
104 | );
105 |
106 | const fileName = `${format(Date.now(), "yyyy-mm-dd_hh:mm")}.json`;
107 | downloadData(values, fileName);
108 | };
109 |
110 | export const generateRegistrationParams = (correlationIdLength: number) => {
111 | const key = new NodeRSA({ b: 2048 });
112 | const pub = key.exportKey("pkcs8-public-pem");
113 | const priv = key.exportKey("pkcs8-private-pem");
114 | const correlation = generateRandomString(correlationIdLength, true);
115 | const secret = uuidv4().toString();
116 |
117 | return { pub, priv, correlation, secret };
118 | };
119 |
120 | export const deregister = (
121 | secretKey: string,
122 | correlationId: string,
123 | host: string,
124 | token?: string
125 | ) => {
126 | const registerFetcherOptions = {
127 | "secret-key": secretKey,
128 | "correlation-id": correlationId,
129 | };
130 |
131 | const headers = [
132 | { "Content-Type": "application/json" },
133 | {
134 | "Content-Type": "application/json",
135 | Authorization: token,
136 | },
137 | ] as const;
138 |
139 | return fetch(`https://${host}/deregister`, {
140 | method: "POST",
141 | cache: "no-cache",
142 | headers: token && token !== "" ? headers[1] : headers[0],
143 | referrerPolicy: "no-referrer",
144 | body: JSON.stringify(registerFetcherOptions),
145 | }).catch(() => {});
146 | };
147 |
148 | export const register = (
149 | host: string,
150 | token: string,
151 | deregisterCurrentInstance: boolean,
152 | reregister: boolean
153 | ) => {
154 | const currentData = getStoredData();
155 | const { pub, priv, correlation, secret } = generateRegistrationParams(currentData.correlationIdLength);
156 | const registerFetcherOptions = reregister
157 | ? {
158 | "public-key": btoa(currentData.publicKey),
159 | "secret-key": currentData.secretKey,
160 | "correlation-id": currentData.correlationId,
161 | }
162 | : {
163 | "public-key": btoa(pub),
164 | "secret-key": secret,
165 | "correlation-id": correlation,
166 | };
167 | const contentType = { "Content-Type": "application/json" };
168 | const authorizationHeader = { Authorization: token };
169 |
170 | return fetch(`https://${host}/register`, {
171 | method: "POST",
172 | cache: "no-cache",
173 | headers: token && token !== "" ? { ...contentType, ...authorizationHeader } : contentType,
174 | referrerPolicy: "no-referrer",
175 | body: JSON.stringify(registerFetcherOptions),
176 | }).then(async (res) => {
177 | if (res.status === 401) {
178 | throw new Error("auth failed");
179 | }
180 | if (!res.ok) {
181 | const d = await res.json();
182 | throw new Error(d.error);
183 | }
184 |
185 | const { url, uniqueId } = generateUrl(correlation, currentData.correlationIdNonceLength, 1, host);
186 | const tabData: Tab[] = [
187 | {
188 | "unique-id": uniqueId,
189 | correlationId: correlation,
190 | name: (1).toString(),
191 | url,
192 | note: "",
193 | },
194 | ];
195 |
196 | const data: StoredData = reregister
197 | ? { ...currentData, aesKey: "", token }
198 | : {
199 | privateKey: priv,
200 | publicKey: pub,
201 | correlationId: correlation,
202 | secretKey: secret,
203 | view: currentData.view,
204 | theme: currentData.theme,
205 | host,
206 | correlationIdLength: currentData.correlationIdLength,
207 | correlationIdNonceLength: currentData.correlationIdNonceLength,
208 | responseExport: false,
209 | increment: 1,
210 | token,
211 | telegram: {
212 | enabled: false,
213 | botToken: '',
214 | chatId: '',
215 | },
216 | slack: {
217 | enabled: false,
218 | hookKey: '',
219 | channel: '',
220 | },
221 | discord: {
222 | enabled: false,
223 | webhook: '',
224 | channel: '',
225 | },
226 | tabs: tabData,
227 | selectedTab: tabData[0],
228 | data: [],
229 | aesKey: "",
230 | notes: [],
231 | filter: defaultFilter,
232 | };
233 |
234 | if (!reregister) {
235 | clearIntervals();
236 | }
237 | if (deregisterCurrentInstance && res.ok) {
238 | deregister(
239 | currentData.secretKey,
240 | currentData.correlationId,
241 | currentData.host,
242 | currentData.token
243 | ).then(() => !reregister && window.location.reload());
244 | }
245 | return data;
246 | });
247 | };
248 |
249 | export const poll = (
250 | correlationId: string,
251 | secretKey: string,
252 | host: string,
253 | token: string,
254 | handleResetPopupDialogVisibility: () => void,
255 | handleCustomHostDialogVisibility: () => void
256 | ): Promise => {
257 | const headers = {
258 | Authorization: token,
259 | };
260 | return fetch(`https://${host}/poll?id=${correlationId}&secret=${secretKey}`, {
261 | method: "GET",
262 | cache: "no-cache",
263 | headers: token !== "" ? headers : {},
264 | referrerPolicy: "no-referrer",
265 | })
266 | .then(async (res: any) => {
267 | const status = res.status;
268 | const getRes = async (): Promise => {
269 | try {
270 | return await res.json();
271 | } catch {
272 | return { aes_key: "", data: [] };
273 | }
274 | };
275 | const data = await getRes();
276 | if (!res.ok) {
277 | const err = data.error;
278 | if (err === "could not get interactions: could not get correlation-id from cache") {
279 | register(host, token, false, true)
280 | .then((d) => {
281 | writeStoredData(d);
282 | })
283 | .catch((err2) => {
284 | if (
285 | err2.message !==
286 | "could not set id and public key: correlation-id provided is invalid"
287 | ) {
288 | clearIntervals();
289 | handleResetPopupDialogVisibility();
290 | }
291 | });
292 | } else if (
293 | err ===
294 | "could not set id and public key: could not read public Key: illegal base64 data at input byte 600"
295 | ) {
296 | register(host, token, false, false)
297 | .then((d) => {
298 | writeStoredData(d);
299 | })
300 | .catch((err2) => {
301 | if (
302 | err2.message !==
303 | "could not set id and public key: correlation-id provided is invalid"
304 | ) {
305 | clearIntervals();
306 | handleResetPopupDialogVisibility();
307 | }
308 | });
309 | } else if (status === 401) {
310 | handleCustomHostDialogVisibility();
311 | } else {
312 | clearIntervals();
313 | handleResetPopupDialogVisibility();
314 | }
315 | }
316 | return data;
317 | })
318 | .then((data) => data);
319 | };
320 |
--------------------------------------------------------------------------------
/src/lib/localStorage/index.test.ts:
--------------------------------------------------------------------------------
1 | import * as _ from ".";
2 | import { defaultStoredData } from ".";
3 |
4 | describe("localStorage", () => {
5 | it("writeStoredData", () => {
6 | jest.spyOn(window.localStorage.__proto__, "setItem");
7 | _.writeStoredData(defaultStoredData);
8 |
9 | // Tests that localStorage.setItem is called with the correct data.
10 | expect(localStorage.setItem).toHaveBeenCalledWith("app", JSON.stringify(defaultStoredData));
11 | });
12 |
13 | it("getStorageData", () => {
14 | _.writeStoredData(defaultStoredData); // Prep stored data.
15 | const data = _.getStoredData();
16 |
17 | jest.spyOn(window.localStorage.__proto__, "getItem");
18 | _.getStoredData();
19 |
20 | // Tests that localStorage.getItem is called with the right key.
21 | expect(localStorage.getItem).toHaveBeenCalledWith("app");
22 |
23 | // Test that data is retreived and decoded correctly.
24 | expect(data).toEqual(defaultStoredData);
25 | });
26 | });
27 |
--------------------------------------------------------------------------------
/src/lib/localStorage/index.ts:
--------------------------------------------------------------------------------
1 | import * as l from "fp-ts-local-storage";
2 | import { parseO } from "fp-ts-std/JSON";
3 | import { flow, pipe } from "fp-ts/function";
4 | import * as O from "fp-ts/Option";
5 |
6 | import { defaultFilter } from "lib/types/filter";
7 | import StoredDataS, { StoredData } from "lib/types/storedData";
8 |
9 | export const defaultStoredData: StoredData = {
10 | theme: "synth",
11 | privateKey: "",
12 | publicKey: "",
13 | correlationId: "",
14 | correlationIdLength: process.env.REACT_APP_CIDL ? +process.env.REACT_APP_CIDL : 20,
15 | correlationIdNonceLength: process.env.REACT_APP_CIDN ? +process.env.REACT_APP_CIDN : 13,
16 | responseExport: false,
17 | secretKey: "",
18 | data: [],
19 | aesKey: "",
20 | notes: [],
21 | view: "up_and_down",
22 | increment: 1,
23 | host: process.env?.REACT_APP_HOST || "oast.fun",
24 | tabs: [],
25 | token: process.env?.REACT_APP_TOKEN || "",
26 | telegram: {
27 | enabled: false,
28 | botToken: '',
29 | chatId: '',
30 | },
31 | slack: {
32 | enabled: false,
33 | hookKey: '',
34 | channel: '',
35 | },
36 | discord: {
37 | enabled: false,
38 | webhook: '',
39 | channel: '',
40 | },
41 | selectedTab: {
42 | "unique-id": "",
43 | correlationId: "",
44 | name: "1",
45 | url: "",
46 | note: "",
47 | },
48 | filter: defaultFilter,
49 | };
50 |
51 | export const writeStoredData = (data: StoredData) =>
52 | O.tryCatch(l.setItem("app", JSON.stringify(data)));
53 |
54 | export const getStoredData = () =>
55 | pipe(
56 | l.getItem("app"),
57 | O.tryCatch,
58 | O.flatten,
59 | O.chain(parseO),
60 | O.chain(flow(StoredDataS.type.decode, O.fromEither)),
61 | O.getOrElseW(() => StoredDataS.build(defaultStoredData))
62 | );
63 |
--------------------------------------------------------------------------------
/src/lib/notify/index.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable camelcase */
2 | // Notify
3 |
4 | const formatBody = (params: {[key: string]: any}) => Object.keys(params)
5 | .map((key) => `${key}=${encodeURIComponent(params[key])}`)
6 | .join('&');
7 |
8 | const headers = new Headers({
9 | 'content-type': 'application/x-www-form-urlencoded'
10 | })
11 |
12 | export const notifyTelegram = async (msg: string, token: string, target: string, mode?: string) => {
13 | if (token.trim() !== '' && target.trim() !== '') {
14 | const params: {[key: string]: string | boolean} = {
15 | chat_id : target,
16 | text: msg,
17 | }
18 | if (mode) {
19 | params.parse_mode = mode
20 | params.disable_web_page_preview = true
21 | }
22 | const body = formatBody(params)
23 | await fetch(`https://api.telegram.org/bot${token}/sendMessage`, {method: 'POST', body, headers})
24 | }
25 | }
26 |
27 | export const notifySlack = async (msg: any[], webhook: string, target: string) => {
28 | if (webhook.trim() !== '' && target.trim() !== '') {
29 | const params: {[key: string]: string} = {
30 | payload: JSON.stringify({
31 | "channel" : target,
32 | "text": '',
33 | "attachments": msg.map(m => ({"text": m.slack}))
34 | })
35 | }
36 | const body = formatBody(params)
37 | await fetch(`${webhook}`, {method: 'POST', body, headers})
38 | }
39 | }
40 |
41 | export const notifyDiscord = async (msg: any[], webhook: string) => {
42 | if (webhook.trim() !== '') {
43 | const chunkSize = 10;
44 | const chunkedData = msg.reduce((all,one,i) => {
45 | const ch = Math.floor(i/chunkSize);
46 | // eslint-disable-next-line no-param-reassign
47 | all[ch] = [].concat((all[ch]||[]),one);
48 | return all
49 | }, [])
50 | chunkedData.map(async (chunk: any) => {
51 | const params: any = {
52 | embeds: chunk.map((m: any) => ({"description": m.discord, "color":5298687}))
53 | }
54 | await fetch(`https://discordapp.com${new URL(webhook).pathname}`, {method: 'POST', body: JSON.stringify(params), headers: {"Content-Type": "application/json"}})
55 | })
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/src/lib/types/data/index.ts:
--------------------------------------------------------------------------------
1 | import { summonFor, AsOpaque } from "@morphic-ts/batteries/lib/summoner-ESBST";
2 | import type { AType, EType } from "@morphic-ts/summoners";
3 | import * as A from "fp-ts/Array";
4 | import * as NEA from "fp-ts/NonEmptyArray";
5 |
6 | import { eqId } from "lib/types/id";
7 | import Protocol from "lib/types/protocol";
8 | import { createRecord } from "lib/utils";
9 |
10 | const { summon } = summonFor<{}>({});
11 |
12 | // Must include "as const" to capture type information.
13 | const dnsRecordTypes = ["A", "AAAA", "ALIAS", "CNAME", "MX", "NS", "PTR", "SOA", "TXT"] as const;
14 |
15 | export const Data_ = summon((F) =>
16 | F.intersection(
17 | // Required
18 | F.interface(
19 | {
20 | id: F.string(),
21 | "full-id": F.string(),
22 | protocol: Protocol(F),
23 | "raw-request": F.string(),
24 | "remote-address": F.string(),
25 | timestamp: F.string(), // TODO: Convert to ISODate
26 | "unique-id": F.string(),
27 | },
28 | ""
29 | ),
30 | // Optional
31 | F.partial(
32 | {
33 | "raw-response": F.string(),
34 | "q-type": F.keysOf(createRecord(dnsRecordTypes)),
35 | "smtp-from": F.string(),
36 | },
37 | ""
38 | )
39 | )("Data", { EqURI: () => eqId })
40 | );
41 | interface Data extends AType {}
42 | interface DataRaw extends EType {}
43 | const Data = AsOpaque()(Data_);
44 |
45 | export const groupByTabId = NEA.groupBy(Data.lensFromProp("unique-id").get);
46 |
47 | // filterByProtocols : Protocal[] -> Data[] -> Data[]
48 | export const filterByProtocols = (ps: Protocol[]) =>
49 | A.filter((d) => A.elem(Protocol.eq)(Data.lensFromProp("protocol").get(d), ps));
50 |
51 | export default Data;
52 |
--------------------------------------------------------------------------------
/src/lib/types/discord.ts:
--------------------------------------------------------------------------------
1 | import { AsOpaque, summonFor } from "@morphic-ts/batteries/lib/summoner-ESBST";
2 | import type { AType, EType } from "@morphic-ts/summoners";
3 |
4 | const { summon } = summonFor<{}>({});
5 |
6 | const Discord_ = summon((F) =>
7 | F.interface(
8 | {
9 | enabled: F.boolean(),
10 | webhook: F.string(),
11 | channel: F.string(),
12 | },
13 | "Discord"
14 | )
15 | );
16 |
17 | export interface Discord extends AType {}
18 | export interface DiscordRaw extends EType {}
19 | export const Discord = AsOpaque()(Discord_);
20 |
21 | export default Discord;
22 |
--------------------------------------------------------------------------------
/src/lib/types/filter.ts:
--------------------------------------------------------------------------------
1 | import { summonFor } from "@morphic-ts/batteries/lib/summoner-ESBST";
2 | import * as t from "io-ts";
3 |
4 | import Protocol from "lib/types/protocol";
5 |
6 | const { summon } = summonFor<{}>({});
7 |
8 | const Filter = summon((F) => F.record(Protocol(F), F.boolean()));
9 |
10 | type Filter = t.TypeOf;
11 |
12 | export const defaultFilter: Filter = {
13 | dns: true,
14 | http: true,
15 | smtp: true,
16 | };
17 |
18 | export default Filter;
19 |
--------------------------------------------------------------------------------
/src/lib/types/id.ts:
--------------------------------------------------------------------------------
1 | import * as Eq from "fp-ts/Eq";
2 | import { pipe } from "fp-ts/function";
3 | import * as s from "fp-ts/string";
4 | import { Lens } from "monocle-ts";
5 |
6 | export interface ID { id: string };
7 |
8 | export const id = Lens.fromProp()("id").get;
9 | export const eqId = pipe(s.Eq, Eq.contramap(id));
--------------------------------------------------------------------------------
/src/lib/types/protocol/index.ts:
--------------------------------------------------------------------------------
1 | import { summonFor } from "@morphic-ts/batteries/lib/summoner-ESBST";
2 | import * as Eq from "fp-ts/Eq";
3 | import { pipe } from "fp-ts/function";
4 | import * as s from "fp-ts/string";
5 | import * as t from "io-ts";
6 |
7 | import { createRecord } from "lib/utils";
8 |
9 | const { summon } = summonFor<{}>({});
10 |
11 | const eqByView = pipe(
12 | s.Eq,
13 | Eq.contramap((p: string) => p)
14 | );
15 |
16 | export const protocols = ["dns", "http", "smtp"] as const;
17 |
18 | const Protocol = summon((F) =>
19 | F.keysOf(createRecord(protocols), {
20 | ShowURI: () => ({
21 | show: s.toUpperCase,
22 | }),
23 | EqURI: () => eqByView,
24 | })
25 | );
26 |
27 | type Protocol = t.TypeOf;
28 |
29 | export default Protocol;
30 |
--------------------------------------------------------------------------------
/src/lib/types/slack.ts:
--------------------------------------------------------------------------------
1 | import { AsOpaque, summonFor } from "@morphic-ts/batteries/lib/summoner-ESBST";
2 | import type { AType, EType } from "@morphic-ts/summoners";
3 |
4 | const { summon } = summonFor<{}>({});
5 |
6 | const Slack_ = summon((F) =>
7 | F.interface(
8 | {
9 | enabled: F.boolean(),
10 | hookKey: F.string(),
11 | channel: F.string(),
12 | },
13 | "Slack"
14 | )
15 | );
16 |
17 | export interface Slack extends AType {}
18 | export interface SlackRaw extends EType {}
19 | export const Slack = AsOpaque()(Slack_);
20 |
21 | export default Slack;
22 |
--------------------------------------------------------------------------------
/src/lib/types/storedData.ts:
--------------------------------------------------------------------------------
1 | import { summonFor, AsOpaque } from "@morphic-ts/batteries/lib/summoner-ESBST";
2 | import type { AType, EType } from "@morphic-ts/summoners";
3 |
4 | import { ThemeName } from "theme";
5 |
6 | import Data from "./data";
7 | import Discord from "./discord";
8 | import Filter from "./filter";
9 | import Slack from "./slack";
10 | import Tab from "./tab";
11 | import Telegram from "./telegram";
12 | import View from "./view";
13 |
14 | const { summon } = summonFor<{}>({});
15 |
16 | // Data structure of localStorage
17 | export const StoredData_ = summon((F) =>
18 | F.interface(
19 | {
20 | view: View(F),
21 | increment: F.number(),
22 | correlationId: F.string(),
23 | correlationIdLength: F.number(),
24 | correlationIdNonceLength: F.number(),
25 | responseExport: F.boolean(),
26 | theme: ThemeName(F),
27 |
28 | publicKey: F.string(),
29 | privateKey: F.string(),
30 | secretKey: F.string(),
31 |
32 | host: F.string(),
33 | token: F.string(),
34 | telegram: Telegram(F),
35 | slack: Slack(F),
36 | discord: Discord(F),
37 | selectedTab: Tab(F),
38 | tabs: F.array(Tab(F)),
39 | data: F.array(Data(F)),
40 | notes: F.array(F.string()),
41 | aesKey: F.string(),
42 | filter: Filter(F),
43 | },
44 | "StoredData"
45 | )
46 | );
47 |
48 | export interface StoredData extends AType {}
49 | export interface StoredDataRaw extends EType {}
50 | export default AsOpaque()(StoredData_);
51 |
--------------------------------------------------------------------------------
/src/lib/types/tab.ts:
--------------------------------------------------------------------------------
1 | import { AsOpaque, summonFor } from "@morphic-ts/batteries/lib/summoner-ESBST";
2 | import type { AType, EType } from "@morphic-ts/summoners";
3 | import * as Eq from 'fp-ts/Eq';
4 | import { pipe } from "fp-ts/function";
5 | import * as s from 'fp-ts/string';
6 |
7 | const eqByUniqueId = pipe(
8 | s.Eq,
9 | Eq.contramap((t: { "unique-id": string }) => t["unique-id"])
10 | );
11 |
12 |
13 | const { summon } = summonFor<{}>({});
14 |
15 | const Tab_ = summon((F) =>
16 | F.interface(
17 | {
18 | correlationId: F.string(),
19 | "unique-id": F.string(),
20 | name: F.string(),
21 | note: F.string(),
22 | url: F.string(),
23 | },
24 | "Tab",
25 | { EqURI: () => eqByUniqueId }
26 | )
27 | );
28 |
29 | export interface Tab extends AType {}
30 | export interface TabRaw extends EType {}
31 | export const Tab = AsOpaque()(Tab_);
32 |
33 | export default Tab;
34 |
--------------------------------------------------------------------------------
/src/lib/types/telegram.ts:
--------------------------------------------------------------------------------
1 | import { AsOpaque, summonFor } from "@morphic-ts/batteries/lib/summoner-ESBST";
2 | import type { AType, EType } from "@morphic-ts/summoners";
3 |
4 | const { summon } = summonFor<{}>({});
5 |
6 | const Telegram_ = summon((F) =>
7 | F.interface(
8 | {
9 | enabled: F.boolean(),
10 | botToken: F.string(),
11 | chatId: F.string(),
12 | },
13 | "Telegram"
14 | )
15 | );
16 |
17 | export interface Telegram extends AType {}
18 | export interface TelegramRaw extends EType {}
19 | export const Telegram = AsOpaque()(Telegram_);
20 |
21 | export default Telegram;
22 |
--------------------------------------------------------------------------------
/src/lib/types/view.ts:
--------------------------------------------------------------------------------
1 | import { summonFor } from "@morphic-ts/batteries/lib/summoner-ESBST";
2 | import * as Eq from "fp-ts/Eq";
3 | import { pipe } from "fp-ts/function";
4 | import * as s from "fp-ts/string";
5 | import * as t from "io-ts";
6 |
7 | const { summon } = summonFor<{}>({});
8 |
9 | const eqByView = pipe(
10 | s.Eq,
11 | Eq.contramap((v: string) => v)
12 | );
13 |
14 | const View = summon((F) =>
15 | F.keysOf(
16 | { request: null, response: null, up_and_down: null, side_by_side: null },
17 | { EqURI: () => eqByView }
18 | )
19 | );
20 | type View = t.TypeOf;
21 |
22 | export default View;
23 |
--------------------------------------------------------------------------------
/src/lib/utils/index.test.ts:
--------------------------------------------------------------------------------
1 | import { createRecord, trueKeys } from '.';
2 |
3 | describe("utils", () => {
4 | test("createRecord", () => {
5 | expect(createRecord(["a", "b"])).toEqual({ a: null, b: null });
6 | });
7 |
8 | test("trueKeys", () => {
9 | expect(trueKeys({ a: true, b: false, c: true })).toEqual(["a", "c"]);
10 | });
11 |
12 | })
13 |
--------------------------------------------------------------------------------
/src/lib/utils/index.ts:
--------------------------------------------------------------------------------
1 | import * as f from "fp-ts-std/Function";
2 | import * as ss from "fp-ts-std/String";
3 | import { pipe, flow } from "fp-ts/function";
4 | import * as O from "fp-ts/Option";
5 | import * as RA from "fp-ts/ReadonlyArray";
6 | import * as R from "fp-ts/Record";
7 | import * as S from "fp-ts/string";
8 | import * as T from "fp-ts/Tuple";
9 |
10 |
11 |
12 | export type TupleToRecord = {
13 | [K in X[number]]: null;
14 | };
15 |
16 | /*
17 | * @example
18 | *
19 | * createRecord(
20 | * ["dns", "http", "https"] as const // Must include "as const"
21 | * )
22 | */
23 | export const createRecord = (arr: X): TupleToRecord =>
24 | pipe(
25 | arr,
26 | RA.reduce({ [arr[0]]: null }, (a, b) => ({ ...a, [b]: null })),
27 | (x) => x as TupleToRecord
28 | );
29 |
30 |
31 | /**
32 | * trueKeys : Record string boolean -> string[]
33 | *
34 | * Returns a list of "true" keys in a record
35 | */
36 | export const trueKeys = (r: Record) => pipe(
37 | r,
38 | R.map>(O.fromPredicate(x => !!x)),
39 | R.compact, // Removes false keys
40 | R.keys, // Converts to array of keys.
41 | x => x as K[]
42 | )
43 |
44 | export const capitalize = flow(
45 | ss.splitAt(1),
46 | T.bimap(S.toLowerCase, S.toUpperCase), // snd, first
47 | pipe(
48 | // ((T, T) -> T) -> ([T, T] -> T)
49 | S.Semigroup.concat,
50 | f.curry2,
51 | f.uncurry2
52 | )
53 | );
54 |
55 | export const generateRandomString = (length: number, lettersOnly: boolean = false) => {
56 | let characters = '';
57 | if (lettersOnly) {
58 | characters = 'abcdefghijklmnopqrstuvwxyz';
59 | } else {
60 | characters = 'abcdefghijklmnopqrstuvwxyz0123456789';
61 | }
62 | let result = '';
63 | for (let i = 0; i < length; i+=1) {
64 | result += characters.charAt(Math.floor(Math.random() * characters.length));
65 | }
66 | return result;
67 | };
--------------------------------------------------------------------------------
/src/pages/homePage/index.tsx:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-expressions */
2 | /* eslint-disable camelcase */
3 | import React, { useEffect, useState } from "react";
4 |
5 | import format from "date-fns/format";
6 | import { ThemeProvider } from "styled-components";
7 |
8 | import "./styles.scss";
9 | // Icons
10 | import { ReactComponent as ChevronUpIcon } from "assets/svg/chevron_up.svg";
11 | import { ReactComponent as ClearIcon } from "assets/svg/clear.svg";
12 | import { ReactComponent as CloseIcon } from "assets/svg/close.svg";
13 | import { ReactComponent as CopyIcon } from "assets/svg/copy.svg";
14 | import { ReactComponent as SideBySideIcon } from "assets/svg/side_by_side.svg";
15 | import { ReactComponent as UpDownIcon } from "assets/svg/up_down.svg";
16 | import AppLoader from "components/appLoader";
17 | import { GlobalStyles } from "globalStyles";
18 | import {
19 | generateUrl,
20 | poll,
21 | decryptAESKey,
22 | processData,
23 | handleResponseExport,
24 | copyDataToClipboard,
25 | clearIntervals,
26 | register,
27 | } from "lib";
28 | import { notifyTelegram, notifySlack, notifyDiscord } from "lib/notify";
29 | import Data from "lib/types/data";
30 | import { StoredData } from "lib/types/storedData";
31 | import Tab from "lib/types/tab";
32 | import View from "lib/types/view";
33 | import { ThemeName, getTheme } from "theme";
34 |
35 | import Header from "../../components/header";
36 | import TabSwitcher from "../../components/tabSwitcher";
37 | import { writeStoredData, getStoredData, defaultStoredData } from "../../lib/localStorage";
38 | import RequestDetailsWrapper from "./requestDetailsWrapper";
39 | import RequestsTableWrapper from "./requestsTableWrapper";
40 |
41 |
42 | const HomePage = () => {
43 | const [aboutPopupVisibility, setAboutPopupVisibility] = useState(false);
44 | const [filteredData, setFilteredData] = useState>([]);
45 | const [isNotesOpen, setIsNotesOpen] = useState(false);
46 | const [isRegistered, setIsRegistered] = useState(false);
47 | const [isResetPopupDialogVisible, setIsResetPopupDialogVisible] = useState(false);
48 | const [isNotificationsDialogVisible, setIsNotificationsDialogVisible] = useState(false);
49 | const [loaderAnimationMode, setLoaderAnimationMode] = useState("loading");
50 | const [selectedInteraction, setSelectedInteraction] = useState(null);
51 | const [selectedInteractionData, setSelectedInteractionData] = useState(null);
52 | const [storedData, setStoredData] = useState(getStoredData());
53 | const [isCustomHostDialogVisible, setIsCustomHostDialogVisible] = useState(false);
54 |
55 | const handleResetPopupDialogVisibility = () => {
56 | setIsResetPopupDialogVisible(!isResetPopupDialogVisible);
57 | };
58 |
59 | const handleNotificationsDialogVisibility = () => {
60 | setIsNotificationsDialogVisible(!isNotificationsDialogVisible);
61 | };
62 |
63 | const handleCustomHostDialogVisibility = () => {
64 | setIsCustomHostDialogVisible(!isCustomHostDialogVisible);
65 | };
66 |
67 | // "Switch theme" function
68 | const handleThemeSelection = (value: ThemeName) => {
69 | setStoredData({
70 | ...storedData,
71 | theme: value,
72 | });
73 | };
74 |
75 | // "Select a tab" function
76 | const handleTabButtonClick = (tab: Tab) => {
77 | setStoredData({
78 | ...storedData,
79 | selectedTab: tab,
80 | });
81 | setSelectedInteraction(null);
82 | };
83 |
84 | // " Add new tab" function
85 | const handleAddNewTab = () => {
86 | const { increment, host, correlationId, correlationIdNonceLength } = storedData;
87 | const newIncrement = increment + 1;
88 | const { url, uniqueId } = generateUrl(correlationId, correlationIdNonceLength, newIncrement, host);
89 | const tabData: Tab = {
90 | "unique-id": uniqueId,
91 | correlationId,
92 | name: newIncrement.toString(),
93 | url,
94 | note: "",
95 | };
96 | setStoredData({
97 | ...storedData,
98 | tabs: storedData.tabs.concat([tabData]),
99 | selectedTab: tabData,
100 | increment: newIncrement,
101 | });
102 | setSelectedInteraction(null);
103 | };
104 |
105 | // "Show or hide notes" function
106 | const handleNotesVisibility = () => {
107 | setTimeout(() => {
108 | document.getElementById("notes_textarea")?.focus();
109 | }, 200);
110 | setIsNotesOpen(!isNotesOpen);
111 | };
112 |
113 | // "Notes input change handler" function
114 | const handleNoteInputChange: React.ChangeEventHandler = (e) => {
115 | const { selectedTab, tabs } = storedData;
116 | const index = tabs.findIndex((item) => Tab.eq.equals(item, selectedTab));
117 | const currentTab = tabs[index];
118 | const filteredTabList = tabs.filter((item) => !Tab.eq.equals(item, selectedTab));
119 | filteredTabList.push({ ...currentTab, note: e.target.value });
120 | setStoredData({
121 | ...storedData,
122 | tabs: filteredTabList,
123 | });
124 | };
125 |
126 | // "Selecting a specific interaction" function
127 | const handleRowClick = (id: string) => {
128 | setSelectedInteraction(id);
129 | const reqDetails =
130 | filteredData && filteredData[filteredData.findIndex((item) => item.id === id)];
131 | setSelectedInteractionData(reqDetails);
132 | };
133 |
134 | // "Deleting a tab" function
135 | const handleDeleteTab = (tab: Tab) => {
136 | const { tabs } = storedData;
137 | const index = tabs.findIndex((value) => Tab.eq.equals(value, tab));
138 | const filteredTempTabsList = tabs.filter((value) => !Tab.eq.equals(value, tab));
139 | const tempTabsData = storedData.data;
140 | const filteredTempTabsData = tempTabsData.filter(
141 | (value) => value["unique-id"] !== tab["unique-id"]
142 | );
143 | setStoredData({
144 | ...storedData,
145 | tabs: [...filteredTempTabsList],
146 | selectedTab: {
147 | ...filteredTempTabsList[filteredTempTabsList.length <= index ? index - 1 : index],
148 | },
149 | data: filteredTempTabsData,
150 | });
151 | };
152 |
153 | // "Renaming a tab" function
154 | const handleTabRename: React.ChangeEventHandler = (e) => {
155 | const tempTabsList = storedData.tabs;
156 | const index = tempTabsList.findIndex((item) => Tab.eq.equals(item, storedData.selectedTab));
157 | const filteredTabList = tempTabsList.filter(
158 | (item) => !Tab.eq.equals(item, storedData.selectedTab)
159 | );
160 | const tempTab = { ...tempTabsList[index], name: e.target.value };
161 |
162 | setStoredData({
163 | ...storedData,
164 | tabs: filteredTabList.concat(tempTab),
165 | });
166 | };
167 |
168 | // "View selector" function
169 | const handleChangeView = (value: View) => {
170 | setStoredData({
171 | ...storedData,
172 | view: value,
173 | });
174 | };
175 |
176 | // "Show or hide about popup" function
177 | const handleAboutPopupVisibility = () => {
178 | setAboutPopupVisibility(!aboutPopupVisibility);
179 | };
180 |
181 | // "Clear interactions of a tab" function
182 | const clearInteractions = () => {
183 | const { selectedTab, data } = storedData;
184 | const tempData = data.filter((item) => item["unique-id"] !== selectedTab["unique-id"]);
185 | setStoredData({
186 | ...storedData,
187 | data: tempData,
188 | });
189 | setFilteredData([]);
190 | };
191 |
192 | const processPolledData = () => {
193 | const dataFromLocalStorage = getStoredData();
194 | const { privateKey, aesKey, host, token, data, correlationId, secretKey } =
195 | dataFromLocalStorage;
196 |
197 | let decryptedAESKey = aesKey;
198 |
199 | poll(
200 | correlationId,
201 | secretKey,
202 | host,
203 | token,
204 | handleResetPopupDialogVisibility,
205 | handleCustomHostDialogVisibility
206 | )
207 | .then((pollData) => {
208 | setIsRegistered(true);
209 | if (pollData?.data?.length !== 0 && !pollData.error) {
210 | if (aesKey === "" && pollData.aes_key) {
211 | decryptedAESKey = decryptAESKey(privateKey, pollData.aes_key);
212 | }
213 | const processedData = processData(decryptedAESKey, pollData);
214 |
215 | // eslint-disable-next-line array-callback-return
216 | const formattedString = processedData.map((item: any) => {
217 |
218 | storedData.responseExport && handleResponseExport(item)
219 |
220 | const telegramMsg = `[${item['full-id']}] Received ${item.protocol.toUpperCase()} interaction from ${item['remote-address']} at ${format(new Date(item.timestamp), "yyyy-mm-dd_hh:mm:ss")} `
221 | storedData.telegram.enabled && notifyTelegram(telegramMsg, storedData.telegram.botToken, storedData.telegram.chatId, 'HTML')
222 | return {
223 | slack: `[${item['full-id']}] Received ${item.protocol.toUpperCase()} interaction from \n at ${format(new Date(item.timestamp), "yyyy-mm-dd_hh:mm:ss")}`,
224 | discord: `[${item['full-id']}] Received ${item.protocol.toUpperCase()} interaction from \n [${item['remote-address']}](https://ipinfo.io/${item['remote-address']}) at ${format(new Date(item.timestamp), "yyyy-mm-dd_hh:mm:ss")}`,
225 | }
226 | })
227 | storedData.slack.enabled && notifySlack(formattedString, storedData.slack.hookKey, storedData.slack.channel)
228 | storedData.discord.enabled && notifyDiscord(formattedString, storedData.discord.webhook)
229 |
230 | const combinedData: Data[] = data.concat(processedData);
231 |
232 | setStoredData({
233 | ...dataFromLocalStorage,
234 | data: combinedData,
235 | aesKey: decryptedAESKey,
236 | });
237 |
238 | const newData = combinedData
239 | .filter((item) => item["unique-id"] === dataFromLocalStorage.selectedTab["unique-id"])
240 | .map((item) => item);
241 | setFilteredData([...newData]);
242 | }
243 | })
244 | .catch(() => {
245 | setLoaderAnimationMode("server_error");
246 | setIsRegistered(false);
247 | });
248 | };
249 |
250 | useEffect(() => {
251 | writeStoredData(storedData);
252 | }, [storedData]);
253 |
254 | useEffect(() => {
255 | window.addEventListener("storage", () => {
256 | setStoredData(getStoredData());
257 | });
258 | setIsRegistered(true);
259 | if (storedData.correlationId === "") {
260 | setLoaderAnimationMode("loading");
261 | setIsRegistered(false);
262 | setTimeout(() => {
263 | register(storedData.host, storedData.token, false, false)
264 | .then((data) => {
265 | setStoredData(data);
266 | window.setInterval(() => {
267 | processPolledData();
268 | }, 4000);
269 | setIsRegistered(true);
270 | })
271 | .catch(() => {
272 | localStorage.clear();
273 | setStoredData(defaultStoredData);
274 | setLoaderAnimationMode("server_error");
275 | setIsRegistered(false);
276 | });
277 | }, 1500);
278 | }
279 | }, []);
280 |
281 | // Recalculate data when a tab is selected
282 | useEffect(() => {
283 | if (storedData.tabs.length > 0) {
284 | clearIntervals();
285 | window.setInterval(() => {
286 | processPolledData();
287 | }, 4000);
288 | const tempFilteredData = storedData.data
289 | .filter((item) => item["unique-id"] === storedData.selectedTab["unique-id"])
290 | .map((item) => item);
291 | setFilteredData(tempFilteredData);
292 | }
293 | }, [storedData.selectedTab]);
294 |
295 | const selectedTabsIndex = storedData.tabs.findIndex((item) =>
296 | Tab.eq.equals(item, storedData.selectedTab)
297 | );
298 |
299 | return (
300 |
301 |
302 |
303 |
304 | {aboutPopupVisibility && (
305 |
306 |
307 |
308 | About
309 |
310 |
311 |
312 | Interactsh is an Open-Source solution for Out of band Data Extraction, A tool
313 | designed to detect bugs that cause external interactions, For example - Blind SQLi,
314 | Blind CMDi, SSRF, etc.
315 |
316 |
317 | If you find communications or exchanges with the Interactsh.com server in your logs, it
318 | is possible that someone has been testing your applications using our hosted
319 | service,
320 |
321 | {` app.interactsh.com `}
322 |
323 | You should review the time when these interactions were initiated to identify the
324 | person responsible for this testing.
325 |
326 |
327 | For further details about Interactsh.com,
328 |
329 | {` checkout opensource code.`}
330 |
331 |
332 |
333 |
334 | )}
335 |
347 |
356 |
357 |
358 |
359 |
360 | {storedData.selectedTab && storedData.selectedTab.url}
361 |
362 |
copyDataToClipboard(storedData.selectedTab.url)} />
363 |
364 |
370 |
371 |
377 |
378 |
379 | {/* */}
380 | {/* {tabs[selectedTabsIndex].note} */}
381 |
390 | {/* */}
391 |
392 |
393 | Notes
394 |
399 |
400 |
401 |
402 | {selectedInteraction !== null && selectedInteractionData !== null && (
403 |
404 |
405 | {selectedInteractionData.protocol !== "smtp" && (
406 | <>
407 |
408 | handleChangeView("request")}
416 | >
417 | Request
418 |
419 | handleChangeView("response")}
427 | >
428 | Response
429 |
430 |
431 |
handleChangeView("side_by_side")}
438 | />
439 | handleChangeView("up_and_down")}
446 | />
447 | >
448 | )}
449 |
457 |
458 |
462 |
463 | )}
464 |
465 |
466 |
467 | );
468 | };
469 |
470 | export default HomePage;
471 |
--------------------------------------------------------------------------------
/src/pages/homePage/requestDetailsWrapper.tsx:
--------------------------------------------------------------------------------
1 | import React, { Suspense } from "react";
2 |
3 | import { ErrorBoundary } from "react-error-boundary";
4 |
5 | import Data from "lib/types/data";
6 | import Protocol from "lib/types/protocol";
7 | import View from "lib/types/view";
8 |
9 | import DetailedRequest from "../../components/detailedRequest";
10 | import { IssuesListFallback, IssuesListErrorFallback } from "../../helpers/fallback-loaders";
11 | import "./styles.scss";
12 |
13 | interface RequestDetailsWrapperP {
14 | view: View;
15 | selectedInteractionData: Data;
16 | }
17 |
18 | const RequestDetailsWrapper = (props: RequestDetailsWrapperP) => {
19 | const { selectedInteractionData, view } = props;
20 |
21 | return (
22 |
26 | (
28 |
29 | )}
30 | >
31 | }>
32 | {View.eq.equals(view, "request") ||
33 | Protocol.eq.equals(selectedInteractionData.protocol, "smtp") ? (
34 |
40 | ) : View.eq.equals(view, "response") ? (
41 |
47 | ) : (
48 | <>
49 |
55 |
61 | >
62 | )}
63 |
64 |
65 |
66 | );
67 | };
68 |
69 | export default RequestDetailsWrapper;
70 |
--------------------------------------------------------------------------------
/src/pages/homePage/requestsTableWrapper.tsx:
--------------------------------------------------------------------------------
1 | import React, { Suspense } from "react";
2 |
3 | import { ErrorBoundary } from "react-error-boundary";
4 |
5 | import Data from "lib/types/data";
6 | import Filter from "lib/types/filter";
7 |
8 | import RequestsTable from "../../components/requestsTable";
9 | import {
10 | RepoSidebarListFallback,
11 | RepoSidebarListErrorFallback,
12 | } from "../../helpers/fallback-loaders";
13 | import "./styles.scss";
14 |
15 | interface RequestsTableWrapperP {
16 | data: Data[];
17 | handleRowClick: (id: string) => void;
18 | selectedInteraction: string;
19 | filter: Filter;
20 | }
21 |
22 | const RequestsTableWrapper = ({
23 | data,
24 | handleRowClick,
25 | selectedInteraction,
26 | filter,
27 | }: RequestsTableWrapperP): JSX.Element => (
28 |
29 | (
31 |
32 | )}
33 | >
34 | }>
35 |
41 |
42 |
43 |
44 | );
45 |
46 | export default RequestsTableWrapper;
47 |
--------------------------------------------------------------------------------
/src/pages/homePage/styles.scss:
--------------------------------------------------------------------------------
1 | @import "../../breakpoints.scss";
2 | @include for-size(xsmall) {
3 | }
4 |
5 | @include for-size(small) {
6 | }
7 |
8 | @include for-size(medium) {
9 | }
10 |
11 | @include for-size(large) {
12 | }
13 |
14 | @include for-size(xlarge) {
15 | }
16 |
17 | .main {
18 | width: 100vw;
19 | height: 100vh;
20 | display: flex;
21 | flex-direction: column;
22 | position: relative;
23 | .about_popup_wrapper {
24 | display: flex;
25 | align-items: center;
26 | justify-content: center;
27 | position: fixed;
28 | height: 100vh;
29 | z-index: 100;
30 | width: 100vw;
31 | background: rgba(0, 0, 0, 0.4);
32 | .about_popup {
33 | display: flex;
34 | padding: 3.4rem 3rem;
35 | flex-direction: column;
36 | background: #000000;
37 | border-radius: 0.6rem;
38 | color: #ffffff;
39 | transform: translateY(-7);
40 | width: 40%;
41 | font-size: 1.5rem;
42 | .about_popup_body {
43 | & > a {
44 | color: #32aaee;
45 | }
46 | }
47 | .about_popup_header {
48 | display: flex;
49 | font-size: 2rem;
50 | margin-bottom: 3rem;
51 | & > svg {
52 | height: 2rem;
53 | cursor: pointer;
54 | margin-left: auto;
55 | }
56 | }
57 | }
58 | }
59 | .body {
60 | width: 100%;
61 | display: flex;
62 | height: calc(100% - 9rem);
63 |
64 | .left_section {
65 | width: 30%;
66 | box-sizing: border-box;
67 | height: 100%;
68 | overflow: hidden;
69 | display: flex;
70 | flex-direction: column;
71 | border-right: 1px solid rgba(151, 151, 151, 0.25);
72 |
73 | .url_container {
74 | width: 100%;
75 | height: 13.5rem;
76 | padding: 0 1.5rem;
77 | display: flex;
78 | align-items: center;
79 | .vertical_bar {
80 | border-right: 1px solid rgba(151, 151, 151, 0.5);
81 | height: 4.2rem;
82 | width: 1px;
83 | margin: 0 1.5rem;
84 | }
85 | .clear_button__disabled {
86 | pointer-events: none;
87 | g g {
88 | fill: #979797;
89 | }
90 | }
91 | & > div {
92 | font-size: 1.5rem;
93 | color: #ffffff;
94 | text-align: left;
95 | padding-bottom: 1.5rem;
96 | overflow: hidden;
97 | text-overflow: ellipsis;
98 | white-space: nowrap;
99 | border-bottom: 1px solid rgba(255, 255, 255, 0.3);
100 | }
101 | & > svg {
102 | margin: -0.5rem 0 0 1.5rem;
103 | cursor: pointer;
104 | height: 2.2rem;
105 | width: 1.8rem;
106 | transition: all 0.1s linear;
107 | &:nth-of-type(2) {
108 | height: 2.2rem;
109 | width: 2.2rem;
110 | margin: -0.4rem 0 0 0;
111 | }
112 | &:active {
113 | g g {
114 | fill: #979797;
115 | }
116 | }
117 | }
118 | }
119 | .requests_table_container {
120 | width: 100%;
121 | height: calc(100% - 18rem);
122 | overflow-y: scroll;
123 | }
124 | .notes {
125 | width: 100%;
126 | display: flex;
127 | flex-direction: column;
128 | max-height: calc(100% - 18.5rem);
129 | position: relative;
130 | .detailed_notes {
131 | display: none;
132 | position: absolute;
133 | bottom: 6.3rem;
134 | width: 100%;
135 | height: 40rem;
136 | & > textarea {
137 | padding: 1.5rem 2rem;
138 | height: 100%;
139 | background: transparent;
140 | overflow-y: scroll;
141 | width: 100%;
142 | border: none;
143 | color: #fcc28c;
144 | font-size: 1.6rem;
145 | &:focus {
146 | outline: none !important;
147 | box-shadow: none !important;
148 | }
149 | &::placeholder {
150 | color: #fcc28c;
151 | }
152 | }
153 | }
154 | .notes_footer {
155 | height: 6.3rem;
156 | cursor: pointer;
157 | display: flex;
158 | align-items: center;
159 | padding: 0 2rem;
160 | & > span {
161 | font-size: 1.6rem;
162 | color: #ffffff;
163 | }
164 | & > svg {
165 | margin-left: auto;
166 | transition: all 0.1s linear;
167 | }
168 | }
169 | }
170 | }
171 | .right_section {
172 | width: 70%;
173 | height: 100%;
174 |
175 | .result_header {
176 | height: 7.5rem;
177 | display: flex;
178 | align-items: center;
179 | padding: 0 1.6rem 0 3.4rem;
180 | color: #ffffff;
181 | margin-bottom: 1.5rem;
182 | .__selected_req_res_button {
183 | background: rgba(216, 216, 216, 0.2);
184 | }
185 | & > svg {
186 | margin-left: 1rem;
187 | cursor: pointer;
188 | fill: #ffffff;
189 | }
190 | .req_res_buttons {
191 | height: 3.5rem;
192 | border: 1px solid #979797;
193 | overflow: hidden;
194 | border-radius: 0.6rem;
195 | font-size: 1.4rem;
196 | color: #ffffff;
197 | display: flex;
198 | align-items: center;
199 | margin-right: 1rem;
200 | cursor: pointer;
201 | & > button {
202 | height: 100%;
203 | display: flex;
204 | align-items: center;
205 | padding: 0 1.1rem;
206 | }
207 | }
208 | .result_info {
209 | margin-left: auto;
210 | font-size: 1.6rem;
211 | color: #ffffff;
212 | & > span {
213 | padding-bottom: 0.2rem;
214 | border-bottom: 1px solid rgba(151, 151, 151, 0.6);
215 | }
216 | }
217 | }
218 | .detailed_request {
219 | display: flex;
220 | flex-direction: column;
221 | padding: 0 3.4rem;
222 | overflow-y: scroll;
223 | height: calc(100% - 9rem);
224 | justify-content: space-between;
225 | align-items: stretch;
226 | }
227 | }
228 | }
229 | }
230 |
--------------------------------------------------------------------------------
/src/pages/termsPage/index.tsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 |
3 | export default () => (
4 |
9 | Privacy Policy
10 |
11 | );
12 |
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
--------------------------------------------------------------------------------
/src/reportWebVitals.ts:
--------------------------------------------------------------------------------
1 | import { ReportHandler } from "web-vitals";
2 |
3 | const reportWebVitals = (onPerfEntry?: ReportHandler) => {
4 | if (onPerfEntry && onPerfEntry instanceof Function) {
5 | import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => {
6 | getCLS(onPerfEntry);
7 | getFID(onPerfEntry);
8 | getFCP(onPerfEntry);
9 | getLCP(onPerfEntry);
10 | getTTFB(onPerfEntry);
11 | });
12 | }
13 | };
14 |
15 | export default reportWebVitals;
16 |
--------------------------------------------------------------------------------
/src/styles.scss:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | * {
6 | -webkit-font-smoothing: antialiased;
7 | -moz-osx-font-smoothing: grayscale;
8 | scrollbar-width: none;
9 | box-sizing: border-box;
10 | }
11 |
12 | html {
13 | font-size: 10px;
14 | font-family: "Nunito Sans", sans-serif;
15 | body {
16 | color: #000;
17 | :global .ReactVirtualized__List {
18 | &:focus {
19 | outline: none !important;
20 | }
21 | }
22 | i:focus {
23 | outline: none;
24 | }
25 | button:focus {
26 | outline: none;
27 | }
28 | :global #download_anchor_element {
29 | height: 0;
30 | width: 0;
31 | }
32 | :global #root {
33 | min-height: 100vh;
34 | display: flex;
35 | flex-direction: column;
36 | position: relative;
37 | }
38 | :global #portal_root {
39 | position: fixed;
40 | top: 0;
41 | left: 0;
42 | z-index: 100;
43 | overflow-y: scroll;
44 | width: 100vw;
45 | display: flex;
46 | }
47 | ::-webkit-scrollbar {
48 | width: 0px;
49 | background: transparent;
50 |
51 | .remove-btn {
52 | margin-right: 0.5rem;
53 | }
54 |
55 | :global .slide {
56 | position: absolute;
57 | top: 0;
58 | left: 0;
59 | opacity: 0;
60 | }
61 | :global .slide-enter {
62 | opacity: 0;
63 | }
64 | :global .slide-enter-done {
65 | opacity: 1;
66 | transition: opacity 300ms ease-in;
67 | }
68 | :global .slide-enter-active {
69 | opacity: 0;
70 | }
71 | :global .slide-exit {
72 | opacity: 0;
73 | }
74 | :global .slide-exit-done {
75 | opacity: 0;
76 | }
77 | :global .slide-exit-active {
78 | opacity: 0;
79 | }
80 | }
81 | p,
82 | h1,
83 | h2,
84 | h3,
85 | h4,
86 | h5,
87 | h6 {
88 | margin: 0;
89 | }
90 |
91 | ul {
92 | margin: 0;
93 | list-style-type: none;
94 | padding: 0;
95 | }
96 |
97 | a {
98 | text-decoration: none;
99 | color: inherit;
100 | }
101 |
102 | :global .anticon-spin {
103 | display: inline-block;
104 | animation: loadingCircle 1s infinite linear;
105 | }
106 | }
107 | }
108 |
109 | @keyframes :global(loadingCircle) {
110 | 100% {
111 | transform: rotate(360deg);
112 | }
113 | }
114 |
--------------------------------------------------------------------------------
/src/theme.ts:
--------------------------------------------------------------------------------
1 | import { summonFor } from "@morphic-ts/batteries/lib/summoner-ESBST";
2 | import * as Eq from "fp-ts/Eq";
3 | import { pipe } from "fp-ts/function";
4 | import { Show } from "fp-ts/Show";
5 | import * as s from "fp-ts/string";
6 | import * as t from "io-ts";
7 | import { match } from "ts-pattern";
8 |
9 |
10 | import { capitalize } from "lib/utils";
11 |
12 | const { summon } = summonFor<{}>({});
13 |
14 | const eqByName = pipe(
15 | s.Eq,
16 | Eq.contramap((v: string) => v)
17 | );
18 |
19 | export const ThemeName = summon((F) => F.keysOf({ dark: null, synth: null, blue: null },{ EqURI: () => eqByName }));
20 | export type ThemeName = t.TypeOf;
21 |
22 | export interface Theme {
23 | background: string;
24 | secondaryBackground: string;
25 | lightBackground: string;
26 | }
27 |
28 | export const showThemeName: Show = { show: capitalize };
29 |
30 | export const darkTheme: Theme = {
31 | background: "#03030d",
32 | secondaryBackground: "#101624",
33 | lightBackground: "#192030",
34 | };
35 | export const synthTheme: Theme = {
36 | background: "#240d2c",
37 | secondaryBackground: "#15071a",
38 | lightBackground: "#341D3B",
39 | };
40 | export const blueTheme: Theme = {
41 | background: "#001729",
42 | secondaryBackground: "#001123",
43 | lightBackground: "#192030",
44 | };
45 |
46 | export const getTheme = (theme: ThemeName): Theme =>
47 | match(theme)
48 | .with("blue", () => blueTheme)
49 | .with("dark", () => darkTheme)
50 | .with("synth", () => synthTheme)
51 | .exhaustive();
52 |
--------------------------------------------------------------------------------
/src/vitals.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable import/prefer-default-export */
2 | const vitalsUrl = 'https://vitals.vercel-analytics.com/v1/vitals';
3 |
4 | function getConnectionSpeed() {
5 | return 'connection' in navigator &&
6 | navigator.connection &&
7 | 'effectiveType' in navigator.connection
8 | ? navigator.connection.effectiveType
9 | : '';
10 | }
11 |
12 | export function sendToVercelAnalytics(metric) {
13 | const analyticsId = process.env.REACT_APP_VERCEL_ANALYTICS_ID;
14 | if (!analyticsId) {
15 | return;
16 | }
17 |
18 | const body = {
19 | dsn: analyticsId,
20 | id: metric.id,
21 | page: window.location.pathname,
22 | href: window.location.href,
23 | // eslint-disable-next-line camelcase
24 | event_name: metric.name,
25 | value: metric.value.toString(),
26 | speed: getConnectionSpeed(),
27 | };
28 |
29 | const blob = new Blob([new URLSearchParams(body).toString()], {
30 | // This content type is necessary for `sendBeacon`
31 | type: 'application/x-www-form-urlencoded',
32 | });
33 | if (navigator.sendBeacon) {
34 | navigator.sendBeacon(vitalsUrl, blob);
35 | } else
36 | fetch(vitalsUrl, {
37 | body: blob,
38 | method: 'POST',
39 | credentials: 'omit',
40 | keepalive: true,
41 | });
42 | }
--------------------------------------------------------------------------------
/src/xid-js.d.ts:
--------------------------------------------------------------------------------
1 | declare module 'xid-js' {
2 | export declare const next: () => string;
3 | }
4 |
--------------------------------------------------------------------------------
/tailwind.config.js:
--------------------------------------------------------------------------------
1 | const colors = require("tailwindcss/colors");
2 |
3 | const keyframes = {
4 | width: {
5 | "0%": { width: "0%" },
6 | "100%": { transform: "100%" },
7 | },
8 | };
9 |
10 | module.exports = {
11 | important: true,
12 | purge: ["./src/**/*.{js,jsx,ts,tsx}", "./public/index.html"],
13 | darkMode: "media",
14 | theme: {
15 | extend: {
16 | flex: {
17 | auto: "2 1 auto",
18 | },
19 | keyframes,
20 | animation: {
21 | width: "width 2s ease-out infinite",
22 | },
23 | },
24 | transitionDuration: {
25 | fast: "300ms",
26 | DEFAULT: "500ms",
27 | slow: "700ms",
28 | },
29 | fontFamily: {
30 | primary: ["Nunito Sans", "sans-serif"],
31 | },
32 | boxShadow: {
33 | DEFAULT: "0 -2px 10px rgba(0, 0, 0, 1)",
34 | none: "none",
35 | },
36 | fontWeight: {
37 | light: 300,
38 | normal: 400,
39 | bold: 600,
40 | },
41 | colors: {
42 | transparent: "transparent",
43 | current: "currentColor",
44 |
45 | white: "#FFF",
46 | black: "#121212",
47 | accent: "rgba(149,90,231,100)",
48 | gray: {
49 | light: "#979797",
50 | DEFAULT: "rgba(216, 216, 216, 0.17)",
51 | dark: "#202020",
52 | },
53 | success: {
54 | light: "rgba(61,255,206,.19)",
55 | DEFAULT: "#3DFFCE",
56 | },
57 | danger: {
58 | light: "rgba(207,87,87,.17)",
59 | DEFAULT: "#FF7777",
60 | },
61 | info: {
62 | DEFAULT: "#5A52FF",
63 | },
64 | },
65 | },
66 | variants: {
67 | extend: {
68 | visibility: ["group-hover"],
69 | },
70 | },
71 | plugins: [require("@tailwindcss/forms"), require("@tailwindcss/typography")],
72 | };
73 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "baseUrl": "src",
5 | "lib": ["dom", "dom.iterable", "esnext"],
6 | "allowJs": true,
7 | "skipLibCheck": true,
8 | "esModuleInterop": true,
9 | "allowSyntheticDefaultImports": true,
10 | "strict": true,
11 | "forceConsistentCasingInFileNames": true,
12 | "noFallthroughCasesInSwitch": true,
13 | "module": "esnext",
14 | "moduleResolution": "node",
15 | "resolveJsonModule": true,
16 | "isolatedModules": true,
17 | "noEmit": true,
18 | "jsx": "react-jsx"
19 | },
20 | "include": ["src"]
21 | }
22 |
--------------------------------------------------------------------------------