├── .dockerignore ├── .env ├── .env.development ├── .eslintrc.cjs ├── .firebaserc ├── .gitignore ├── .prettierrc ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── firebase.json ├── index.html ├── package-lock.json ├── package.json ├── postcss.config.js ├── public ├── 404.html ├── index.html ├── logo-v2-t.png ├── logo.jpg └── vite.svg ├── secrets-ninja-proxy ├── .gitignore ├── README.md ├── requirements.txt ├── secrets-ninja-proxy.py └── services │ ├── aws.py │ ├── gcp.py │ ├── mongodb.py │ ├── postgres.py │ └── rabbitmq.py ├── src ├── App.css ├── App.jsx ├── assets │ ├── logo-t.png │ ├── logo.jpg │ └── react.svg ├── components │ ├── copy_button.jsx │ ├── dashboard.jsx │ ├── footer.jsx │ ├── navbar.jsx │ ├── pages │ │ └── secrets_checker.jsx │ ├── request_window.jsx │ ├── requests.jsx │ ├── response_window.jsx │ ├── response_window │ │ ├── json_grid_view.jsx │ │ └── json_view.jsx │ ├── sidebar.jsx │ └── table.jsx ├── css │ └── json_theme.css ├── data │ └── detectors.json ├── index.css ├── main.jsx └── modules │ └── universal.jsx ├── tailwind.config.js └── vite.config.js /.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | __pycache__ 3 | *.pyc 4 | .git 5 | dist 6 | .env 7 | *.log 8 | Dockerfile 9 | .dockerignore 10 | **/venv/ 11 | venv/ 12 | **/.firebase/ 13 | .firebase/ 14 | dist/ 15 | public/ 16 | .DS_Store -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | VITE_SECRETS_NINJA_PROXY_ENDPOINT=https://proxy.secrets.ninja -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | VITE_SECRETS_NINJA_PROXY_ENDPOINT=http://localhost:8001 -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { browser: true, es2020: true }, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:react/recommended', 7 | 'plugin:react/jsx-runtime', 8 | 'plugin:react-hooks/recommended', 9 | ], 10 | ignorePatterns: ['dist', '.eslintrc.cjs'], 11 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' }, 12 | settings: { react: { version: '18.2' } }, 13 | plugins: ['react-refresh'], 14 | rules: { 15 | 'react/jsx-no-target-blank': 'off', 16 | 'react-refresh/only-export-components': [ 17 | 'warn', 18 | { allowConstantExport: true }, 19 | ], 20 | }, 21 | }; 22 | -------------------------------------------------------------------------------- /.firebaserc: -------------------------------------------------------------------------------- 1 | { 2 | "projects": { 3 | "default": "api-keys-checker" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | .github/dependabot.yml 26 | .firebase/hosting.ZGlzdA.cache 27 | firebase.json 28 | .firebaserc 29 | secrets-ninja.code-workspace 30 | .env 31 | .env.development 32 | *.env.development -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": true, 3 | "singleQuote": true, 4 | "printWidth": 80, 5 | "tabWidth": 2, 6 | "trailingComma": "es5", 7 | "endOfLine": "auto" 8 | } 9 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # How to Contribute 2 | 3 | ## Adding a new service 4 | 5 | 1. Add the module to `src/data/detectors.json`. Refer the below example for OpenAI 6 | 7 | ```json 8 | { 9 | "OpenAI": { 10 | "endpoints": { 11 | "organizations": { 12 | "label": "Get Organizations", 13 | "curl": "curl -X GET 'https://api.openai.com/v1/organizations' -H 'Authorization: Bearer sk-xxxx'", 14 | "request_url": "https://api.openai.com/v1/organizations", 15 | "request_method": "GET" 16 | }, 17 | "additional_endpoints": {} 18 | }, 19 | "input_fields": { 20 | "api_key": { 21 | "type": "text", 22 | "label": "Enter OpenAI API Key", 23 | "placeholder": "sk-xxxxx-xxxxx-xxxxx-xxxxx" 24 | }, 25 | "additional_input_fields": {} 26 | } 27 | } 28 | } 29 | ``` 30 | 31 | 2. Update `src/components/requests.jsx` with the request code for this service 32 | 33 | ```js 34 | case 'OpenAI': 35 | response = await fetch(endpointURL, { 36 | method: requestMethod, 37 | headers: { 38 | 'Authorization': `Bearer ${inputData.api_key}`, 39 | } 40 | }); 41 | break; 42 | ``` 43 | 44 | 3. **(OPTIONAL)** Add the service icon in `src/components/sidebar.jsx` 45 | 46 | - Icon for services can be discovered at [https://react-icons.github.io/react-icons/](https://react-icons.github.io/react-icons/) 47 | - Adding Icon is optional as services with no specified icons already use a placeholder icon 48 | 49 | ```js 50 | import { RiOpenaiFill } from "react-icons/ri"; 51 | 52 | let serviceIcons = { 53 | ...., 54 | OpenAI: RiOpenaiFill, 55 | .... 56 | } 57 | ``` 58 | 59 | 4. **CORS Error Workarounds** 60 | 61 | If the api can't be accessed from browser due to CORS, add the following to the `src/data/detectors.json`. This will auto enable the thingproxy.freeboard.io checkbox 62 | 63 | ```json 64 | "alert": { 65 | "alert_message": "Use the curl on your local machine to test creds privately. Or check the proxy box to use secrets-ninja-proxy to bypass CORS.", 66 | "color": "failure" 67 | } 68 | ``` 69 | 70 | ## Note 71 | The following [GPT bot](https://chatgpt.com/g/g-67d9165cb410819191d7b463e4f0a9a2-secrets-ninja-contribution-bot) can be used to generate somewhat functional modules for secrets.ninja 72 | - Output Modules generated using GPT may not be 100% correct and may require testing 73 | 74 | 75 | - I understand that the size of `src/data/detectors.json` is getting big, in future will be moving it to database or having individual JSON for each service. 76 | - I understand that creating a different switch case of each service is redundant, and a universal function can be created for most of these, but I wanted the code to be easily contributable as it can be. 77 | - Only contribute readonly endpoints, don't want to add any endpoints which allows adding, updating, pushing, or deletion of data, change of state on the service. 78 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:20-bullseye 2 | 3 | # Install Python and build deps 4 | RUN apt-get update && \ 5 | apt-get install -y python3 python3-pip python3-venv gcc libpq-dev git && \ 6 | rm -rf /var/lib/apt/lists/* 7 | 8 | WORKDIR /app 9 | 10 | # Copy project files into the image 11 | COPY . . 12 | 13 | # Install frontend deps 14 | RUN npm install 15 | 16 | # Install backend deps 17 | WORKDIR /app/secrets-ninja-proxy 18 | RUN pip3 install --no-cache-dir -r requirements.txt 19 | 20 | # Final config 21 | WORKDIR /app 22 | EXPOSE 5173 8001 23 | 24 | CMD bash -c "npm run dev -- --host 0.0.0.0 & \ 25 | cd /app/secrets-ninja-proxy && \ 26 | python3 -m uvicorn secrets-ninja-proxy:app --host 0.0.0.0 --port 8001 & \ 27 | wait -n" 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 NIKHIL PANWAR 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 |
2 | 3 | 4 | 5 | [![Repo Size](https://img.shields.io/github/languages/code-size/NikhilPanwar/secrets-ninja.svg)](https://github.com/NikhilPanwar/secrets-ninja) 6 | [![LICENSE](https://img.shields.io/github/license/NikhilPanwar/secrets-ninja.svg)](https://github.com/NikhilPanwar/secrets-ninja/blob/master/LICENSE) 7 | [![Contributors](https://img.shields.io/github/contributors/NikhilPanwar/secrets-ninja.svg)](https://github.com/NikhilPanwar/secrets-ninja/graphs/contributors) 8 | [![Last Commit](https://img.shields.io/github/last-commit/NikhilPanwar/secrets-ninja.svg)](https://github.com/NikhilPanwar/secrets-ninja) 9 | 10 |

Secrets Ninja

11 | 12 | [secrets.ninja](https://secrets.ninja) is a tool for validating API keys and credentials discovered during pentesting & bug bounty hunting. 13 |
It proivdes a unified interface for testing these keys across SaaS, Databases, Cloud Providers & services 14 | 15 |
16 | 17 | ## Features 18 | 19 | - **Multiple Service Support:** Secrets Ninja supports a wide range of services, each with a dedicated module for validating API keys. 20 | - **Extensible Design:** The project is designed to be easily extensible, allowing for the addition of new modules for other services. 21 | - **User-Friendly Interface:** A simple and intuitive interface for inputting API keys and making requests. 22 | - **Clear Feedback:** Provides clear feedback on the validity of the keys and any information retrieved from the API calls. 23 | 24 | ## Getting Started 25 | 26 | To get started with Secrets Ninja, install the dependencies and run the development server. 27 | 28 | - Install dependencies using below command 29 | 30 | ```bash 31 | $ npm install 32 | $ npm run dev 33 | ``` 34 | 35 | Or Run Using Docker, Including the Secrets Ninja Proxy for testing AWS, MongoDB creds privately 36 | ``` 37 | docker run -p 5173:5173 -p 8001:8001 secretsninja/secrets-ninja:latest 38 | ``` 39 | 40 | Access the development server at [http://localhost:5173/](http://localhost:5173/) 41 | 42 | ## Contributing 43 | 44 | Contributions are welcome, particularly new modules for validating API keys on additional services. 45 | Please note that due to CORS restrictions or in case of Cloud Creds which requires SDK, CLI tools, some APIs can't be accessed using frontend JS only. In such cases, the project provides workaround using secrets-ninja-proxy module. 46 | 47 | Interested in contributing to the project? [Here's](CONTRIBUTING.md) how you can get started. 48 | 49 | ## Disclaimer 50 | 51 | This tool is intended for ethical use only. It is the user's responsibility to comply with all applicable laws and terms of service when using this tool. 52 | 53 | ## License 54 | 55 | Secrets Ninja is licensed under the MIT License. See the [LICENSE](LICENSE) file for more details. 56 | -------------------------------------------------------------------------------- /firebase.json: -------------------------------------------------------------------------------- 1 | { 2 | "hosting": { 3 | "public": "dist", 4 | "ignore": [ 5 | "firebase.json", 6 | "**/.*", 7 | "**/node_modules/**" 8 | ], 9 | "rewrites": [ 10 | { 11 | "source": "**", 12 | "destination": "/index.html" 13 | } 14 | ] 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Secrets Ninja 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 |
25 | 26 | 27 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "secrets-ninja", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build", 9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview", 11 | "format": "prettier --write ." 12 | }, 13 | "dependencies": { 14 | "@floating-ui/core": "^1.6.9", 15 | "@floating-ui/dom": "^1.6.13", 16 | "@redheadphone/react-json-grid": "^0.9.4", 17 | "autoprefixer": "^10.4.17", 18 | "flowbite-react": "^0.7.2", 19 | "hamburger-react": "^2.5.0", 20 | "postcss": "^8.5.3", 21 | "react": "^18.2.0", 22 | "react-dom": "^18.2.0", 23 | "react-icons": "^5.5.0", 24 | "react-json-pretty": "^2.2.0", 25 | "react-router-dom": "^6.22.1", 26 | "react-syntax-highlighter": "^15.6.1", 27 | "react-table": "^7.8.0", 28 | "react-use": "^17.6.0", 29 | "tailwindcss": "^3.4.1" 30 | }, 31 | "devDependencies": { 32 | "@types/react": "^18.2.56", 33 | "@types/react-dom": "^18.2.19", 34 | "@typescript-eslint/eslint-plugin": "^8.20.0", 35 | "@typescript-eslint/parser": "^8.20.0", 36 | "@vitejs/plugin-react": "^4.2.1", 37 | "eslint": "^8.57.1", 38 | "eslint-config-prettier": "^10.0.1", 39 | "eslint-plugin-jsx-a11y": "^6.10.2", 40 | "eslint-plugin-prettier": "^5.2.1", 41 | "eslint-plugin-react": "^7.37.4", 42 | "eslint-plugin-react-hooks": "^4.6.2", 43 | "eslint-plugin-react-refresh": "^0.4.5", 44 | "prettier": "^3.4.2", 45 | "vite": "^5.1.4" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /public/404.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Page Not Found 7 | 8 | 79 | 80 | 81 |
82 |

404

83 |

Page Not Found

84 |

85 | The specified file was not found on this website. Please check the URL 86 | for mistakes and try again. 87 |

88 |

Why am I seeing this?

89 |

90 | This page was generated by the Firebase Command-Line Interface. To 91 | modify it, edit the 404.html file in your project's 92 | configured public directory. 93 |

94 |
95 | 96 | 97 | -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Welcome to Firebase Hosting 7 | 8 | 9 | 10 | 11 | 12 | 16 | 20 | 24 | 28 | 29 | 33 | 37 | 41 | 45 | 46 | 47 | 112 | 113 | 114 |
115 |

Welcome

116 |

Firebase Hosting Setup Complete

117 |

118 | You're seeing this because you've successfully setup Firebase Hosting. 119 | Now it's time to go build something extraordinary! 120 |

121 | Open Hosting Documentation 124 |
125 |

Firebase SDK Loading…

126 | 127 | 166 | 167 | 168 | -------------------------------------------------------------------------------- /public/logo-v2-t.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NikhilPanwar/secrets-ninja/9db8616d4024cfe7329b439818769f94b8cde70a/public/logo-v2-t.png -------------------------------------------------------------------------------- /public/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NikhilPanwar/secrets-ninja/9db8616d4024cfe7329b439818769f94b8cde70a/public/logo.jpg -------------------------------------------------------------------------------- /public/vite.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /secrets-ninja-proxy/.gitignore: -------------------------------------------------------------------------------- 1 | *pycache* 2 | *venv* -------------------------------------------------------------------------------- /secrets-ninja-proxy/README.md: -------------------------------------------------------------------------------- 1 | # secrets-ninja-proxy 2 | Proxy Server for secrets ninja, use it for bypassing CORS, verifying complex keys like AWS, DB creds 3 | 4 | # Install Dependencies 5 | ``` 6 | python3 -m pip install -r requirements.txt 7 | ``` 8 | 9 | # Run 10 | ``` 11 | python3 -m uvicorn secrets-ninja-proxy:app --reload --host 0.0.0.0 --port 8001 --reload 12 | ``` -------------------------------------------------------------------------------- /secrets-ninja-proxy/requirements.txt: -------------------------------------------------------------------------------- 1 | fastapi 2 | uvicorn 3 | boto3 4 | requests 5 | pymongo 6 | pika 7 | psycopg2 8 | google-auth 9 | google-auth-oauthlib 10 | google-auth-httplib2 11 | google-api-python-client 12 | google-cloud-storage 13 | google-cloud-iam 14 | -------------------------------------------------------------------------------- /secrets-ninja-proxy/secrets-ninja-proxy.py: -------------------------------------------------------------------------------- 1 | from fastapi import FastAPI, Request 2 | from fastapi.responses import JSONResponse 3 | from fastapi.middleware.cors import CORSMiddleware 4 | import requests 5 | from urllib.parse import unquote, parse_qsl 6 | import json 7 | from services import aws, mongodb, rabbitmq, postgres, gcp 8 | 9 | app = FastAPI() 10 | 11 | app.include_router(aws.router, prefix="/aws", tags=["aws"]) 12 | app.include_router(mongodb.router, prefix="/mongodb", tags=["mongodb"]) 13 | app.include_router(rabbitmq.router, prefix="/rabbitmq", tags=["rabbitmq"]) 14 | app.include_router(postgres.router, prefix="/postgres", tags=["postgres"]) 15 | app.include_router(gcp.router, prefix="/gcp", tags=["gcp"]) 16 | 17 | app.add_middleware( 18 | CORSMiddleware, 19 | allow_origins=["*"], 20 | allow_methods=["*"], 21 | allow_headers=["*"], 22 | ) 23 | 24 | def clean_headers(browser_headers): 25 | values_to_delete = [ 26 | "host", "Host", "content-length", "Content-Length", "Content-Type", "content-type", "connection", 27 | "Connection", "accept-encoding", "Accept-Encoding", "accept", 28 | "Accept", "origin", "Origin", "referer", "Referer", "user-agent", "User-Agent", 29 | "sec-ch-ua-platform", "sec-ch-ua-mobile", "sec-fetch-dest", "sec-fetch-mode", 30 | "accept-language", "sec-fetch-site", "sec-fetch-user", "sec-ch-ua", "sec-ch-ua-arch", "sec-gpc", 31 | ] 32 | for value in values_to_delete: 33 | browser_headers.pop(value, None) 34 | return browser_headers 35 | 36 | def make_request(url, headers, method, json_body=None): 37 | url = unquote(url) 38 | if method == "GET": 39 | print("Making GET request to", url) 40 | response = requests.get(url, headers=headers) 41 | try: 42 | return json.loads(response.text), response.status_code 43 | except: 44 | return {"response": response.text}, response.status_code 45 | elif method == "POST": 46 | if 'x-www-form-urlencoded' in json.dumps(headers): 47 | response = requests.post(url, headers=headers, data=json_body) 48 | else: 49 | response = requests.post(url, headers=headers, json=json_body) 50 | return response.json(), response.status_code 51 | 52 | @app.options("/{full_path:path}") 53 | async def handle_options(full_path: str): 54 | return JSONResponse(status_code=200, content={"message": "OK"}) 55 | 56 | @app.api_route("/fetch/{rest_of_path:path}", methods=["POST", "GET"]) 57 | async def fetch_handler(request: Request, rest_of_path: str): 58 | try: 59 | json_body = await request.json() 60 | except: 61 | json_body = {} 62 | method = request.method 63 | headers = json_body.get("proxied_data", {}).get("headers", {}) 64 | if headers == {}: 65 | real_headers = dict(request.headers) 66 | headers = clean_headers(real_headers) 67 | full_url = str(request.url).replace(str(request.base_url) + "fetch/", "") 68 | body = json_body.get("body", None) 69 | if body: 70 | parsed_body = dict(parse_qsl(body)) if isinstance(body, str) else body 71 | else: 72 | parsed_body = {} 73 | 74 | response, status_code = make_request(full_url, headers, method, parsed_body) 75 | return JSONResponse(status_code=status_code, content=response) 76 | 77 | if __name__ == "__main__": 78 | import uvicorn 79 | uvicorn.run("main:app", host="0.0.0.0", port=8001, reload=True) -------------------------------------------------------------------------------- /secrets-ninja-proxy/services/aws.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Request, HTTPException 2 | import boto3 3 | from botocore.exceptions import ClientError 4 | from datetime import datetime 5 | 6 | router = APIRouter() 7 | 8 | @router.post("/list_buckets") 9 | async def list_buckets(request: Request): 10 | data = await request.json() 11 | aws_access_key = data.get("aws_access_key") 12 | aws_secret_key = data.get("aws_secret_key") 13 | region = data.get("region", "us-east-1") 14 | if not aws_access_key or not aws_secret_key: 15 | raise HTTPException(status_code=400, detail="Missing AWS credentials") 16 | try: 17 | s3_client = boto3.client( 18 | "s3", 19 | aws_access_key_id=aws_access_key, 20 | aws_secret_access_key=aws_secret_key, 21 | region_name=region 22 | ) 23 | response = s3_client.list_buckets() 24 | buckets = [bucket["Name"] for bucket in response.get("Buckets", [])] 25 | return {"buckets": buckets} 26 | except ClientError as e: 27 | raise HTTPException(status_code=500, detail=str(e)) 28 | 29 | @router.post("/list_ec2_instances") 30 | async def list_ec2_instances(request: Request): 31 | data = await request.json() 32 | aws_access_key = data.get("aws_access_key") 33 | aws_secret_key = data.get("aws_secret_key") 34 | region = data.get("region", "us-east-1") 35 | if not aws_access_key or not aws_secret_key: 36 | raise HTTPException(status_code=400, detail="Missing AWS credentials") 37 | try: 38 | ec2_client = boto3.client( 39 | "ec2", 40 | aws_access_key_id=aws_access_key, 41 | aws_secret_access_key=aws_secret_key, 42 | region_name=region 43 | ) 44 | response = ec2_client.describe_instances() 45 | instance_ids = [] 46 | for reservation in response.get("Reservations", []): 47 | for instance in reservation.get("Instances", []): 48 | instance_ids.append(instance.get("InstanceId")) 49 | return {"instances": instance_ids} 50 | except ClientError as e: 51 | raise HTTPException(status_code=500, detail=str(e)) 52 | 53 | @router.post("/get_cost_and_usage") 54 | async def get_cost_and_usage(request: Request): 55 | data = await request.json() 56 | aws_access_key = data.get("aws_access_key") 57 | aws_secret_key = data.get("aws_secret_key") 58 | region = "us-east-1" # Cost Explorer only works in us-east-1 59 | if not aws_access_key or not aws_secret_key: 60 | raise HTTPException(status_code=400, detail="Missing AWS credentials") 61 | try: 62 | start = datetime.today().replace(day=1).strftime("%Y-%m-%d") 63 | end = datetime.today().strftime("%Y-%m-%d") 64 | ce_client = boto3.client( 65 | "ce", 66 | aws_access_key_id=aws_access_key, 67 | aws_secret_access_key=aws_secret_key, 68 | region_name=region 69 | ) 70 | response = ce_client.get_cost_and_usage( 71 | TimePeriod={ 72 | "Start": start, 73 | "End": end 74 | }, 75 | Granularity="MONTHLY", 76 | Metrics=["UnblendedCost"] 77 | ) 78 | return {"cost_and_usage": response} 79 | except ClientError as e: 80 | raise HTTPException(status_code=500, detail=str(e)) 81 | 82 | @router.post("/get_service_cost_and_usage") 83 | async def get_service_cost_and_usage(request: Request): 84 | data = await request.json() 85 | aws_access_key = data.get("aws_access_key") 86 | aws_secret_key = data.get("aws_secret_key") 87 | region = "us-east-1" # Cost Explorer works only in us-east-1 88 | if not aws_access_key or not aws_secret_key: 89 | raise HTTPException(status_code=400, detail="Missing AWS credentials") 90 | try: 91 | start = datetime.today().replace(day=1).strftime("%Y-%m-%d") 92 | end = datetime.today().strftime("%Y-%m-%d") 93 | ce_client = boto3.client( 94 | "ce", 95 | aws_access_key_id=aws_access_key, 96 | aws_secret_access_key=aws_secret_key, 97 | region_name=region 98 | ) 99 | response = ce_client.get_cost_and_usage( 100 | TimePeriod={ 101 | "Start": start, 102 | "End": end 103 | }, 104 | Granularity="MONTHLY", 105 | Metrics=["UnblendedCost"], 106 | GroupBy=[ 107 | { 108 | 'Type': 'DIMENSION', 109 | 'Key': 'SERVICE' 110 | } 111 | ] 112 | ) 113 | return {"cost_and_usage": response.get('ResultsByTime', [])} 114 | except ClientError as e: 115 | raise HTTPException(status_code=500, detail=str(e)) 116 | 117 | @router.post("/list_account_aliases") 118 | async def list_account_aliases(request: Request): 119 | data = await request.json() 120 | aws_access_key = data.get("aws_access_key") 121 | aws_secret_key = data.get("aws_secret_key") 122 | if not aws_access_key or not aws_secret_key: 123 | raise HTTPException(status_code=400, detail="Missing AWS credentials") 124 | try: 125 | iam_client = boto3.client( 126 | "iam", 127 | aws_access_key_id=aws_access_key, 128 | aws_secret_access_key=aws_secret_key 129 | ) 130 | response = iam_client.list_account_aliases() 131 | return {"account_aliases": response.get("AccountAliases", [])} 132 | except ClientError as e: 133 | raise HTTPException(status_code=500, detail=str(e)) 134 | 135 | @router.post("/list_hosted_zones") 136 | async def list_hosted_zones(request: Request): 137 | data = await request.json() 138 | aws_access_key = data.get("aws_access_key") 139 | aws_secret_key = data.get("aws_secret_key") 140 | if not aws_access_key or not aws_secret_key: 141 | raise HTTPException(status_code=400, detail="Missing AWS credentials") 142 | try: 143 | route53_client = boto3.client( 144 | "route53", 145 | aws_access_key_id=aws_access_key, 146 | aws_secret_access_key=aws_secret_key 147 | ) 148 | response = route53_client.list_hosted_zones() 149 | zones = [zone["Name"] for zone in response.get("HostedZones", [])] 150 | return {"hosted_zones": zones} 151 | except ClientError as e: 152 | raise HTTPException(status_code=500, detail=str(e)) 153 | 154 | @router.post("/list_roles") 155 | async def list_roles(request: Request): 156 | data = await request.json() 157 | aws_access_key = data.get("aws_access_key") 158 | aws_secret_key = data.get("aws_secret_key") 159 | if not aws_access_key or not aws_secret_key: 160 | raise HTTPException(status_code=400, detail="Missing AWS credentials") 161 | try: 162 | iam_client = boto3.client( 163 | "iam", 164 | aws_access_key_id=aws_access_key, 165 | aws_secret_access_key=aws_secret_key 166 | ) 167 | response = iam_client.list_roles() 168 | roles = [role["RoleName"] for role in response.get("Roles", [])] 169 | return {"roles": roles} 170 | except ClientError as e: 171 | raise HTTPException(status_code=500, detail=str(e)) 172 | 173 | @router.post("/get_caller_identity") 174 | async def get_caller_identity(request: Request): 175 | data = await request.json() 176 | aws_access_key = data.get("aws_access_key") 177 | aws_secret_key = data.get("aws_secret_key") 178 | region = data.get("region", "us-east-1") 179 | if not aws_access_key or not aws_secret_key: 180 | raise HTTPException(status_code=400, detail="Missing AWS credentials") 181 | try: 182 | sts_client = boto3.client( 183 | "sts", 184 | aws_access_key_id=aws_access_key, 185 | aws_secret_access_key=aws_secret_key, 186 | region_name=region 187 | ) 188 | response = sts_client.get_caller_identity() 189 | return {"caller_identity": response} 190 | except ClientError as e: 191 | raise HTTPException(status_code=500, detail=str(e)) 192 | 193 | @router.post("/describe_organization") 194 | async def describe_organization(request: Request): 195 | data = await request.json() 196 | aws_access_key = data.get("aws_access_key") 197 | aws_secret_key = data.get("aws_secret_key") 198 | region = data.get("region", "us-east-1") 199 | if not aws_access_key or not aws_secret_key: 200 | raise HTTPException(status_code=400, detail="Missing AWS credentials") 201 | try: 202 | org_client = boto3.client( 203 | "organizations", 204 | aws_access_key_id=aws_access_key, 205 | aws_secret_access_key=aws_secret_key, 206 | region_name=region 207 | ) 208 | response = org_client.describe_organization() 209 | return {"organization": response} 210 | except ClientError as e: 211 | raise HTTPException(status_code=500, detail=str(e)) 212 | 213 | @router.post("/get_contact_information") 214 | async def get_contact_information(request: Request): 215 | data = await request.json() 216 | aws_access_key = data.get("aws_access_key") 217 | aws_secret_key = data.get("aws_secret_key") 218 | region = data.get("region", "us-east-1") 219 | if not aws_access_key or not aws_secret_key: 220 | raise HTTPException(status_code=400, detail="Missing AWS credentials") 221 | try: 222 | account_client = boto3.client( 223 | "account", 224 | aws_access_key_id=aws_access_key, 225 | aws_secret_access_key=aws_secret_key, 226 | region_name=region 227 | ) 228 | response = account_client.get_contact_information() 229 | return {"contact_information": response} 230 | except ClientError as e: 231 | raise HTTPException(status_code=500, detail=str(e)) 232 | -------------------------------------------------------------------------------- /secrets-ninja-proxy/services/gcp.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Request, HTTPException 2 | from google.oauth2 import service_account 3 | from googleapiclient.discovery import build 4 | from google.cloud import storage 5 | from google.cloud import iam 6 | import json 7 | 8 | router = APIRouter() 9 | 10 | @router.post("/list_projects") 11 | async def list_projects(request: Request): 12 | data = await request.json() 13 | gcp_creds = data.get("gcp_creds") 14 | gcp_creds = json.loads(gcp_creds) 15 | if not gcp_creds: 16 | raise HTTPException(status_code=400, detail="Missing GCP credentials") 17 | try: 18 | credentials = service_account.Credentials.from_service_account_info(gcp_creds) 19 | service = build('cloudresourcemanager', 'v1', credentials=credentials) 20 | request = service.projects().list() 21 | response = request.execute() 22 | projects = response.get('projects', []) 23 | return {"projects": projects} 24 | except Exception as e: 25 | raise HTTPException(status_code=500, detail=str(e)) 26 | 27 | @router.post("/list_compute_instances") 28 | async def list_compute_instances(request: Request): 29 | data = await request.json() 30 | gcp_creds = data.get("gcp_creds") 31 | gcp_creds = json.loads(gcp_creds) 32 | project_id = data.get("project_id") 33 | zone = data.get("zone") 34 | if not gcp_creds or not project_id or not zone: 35 | raise HTTPException(status_code=400, detail="Missing GCP credentials, project ID, or zone") 36 | try: 37 | credentials = service_account.Credentials.from_service_account_info(gcp_creds) 38 | service = build('compute', 'v1', credentials=credentials) 39 | request = service.instances().list(project=project_id, zone=zone) 40 | response = request.execute() 41 | instances = response.get('items', []) 42 | return {"instances": instances} 43 | except Exception as e: 44 | raise HTTPException(status_code=500, detail=str(e)) 45 | 46 | @router.post("/list_buckets") 47 | async def list_buckets(request: Request): 48 | data = await request.json() 49 | gcp_creds = data.get("gcp_creds") 50 | gcp_creds = json.loads(gcp_creds) 51 | if not gcp_creds: 52 | raise HTTPException(status_code=400, detail="Missing GCP credentials") 53 | try: 54 | credentials = service_account.Credentials.from_service_account_info(gcp_creds) 55 | client = storage.Client(credentials=credentials) 56 | buckets = list(client.list_buckets()) 57 | bucket_names = [bucket.name for bucket in buckets] 58 | return {"buckets": bucket_names} 59 | except Exception as e: 60 | raise HTTPException(status_code=500, detail=str(e)) 61 | 62 | @router.post("/list_iam_users") 63 | async def list_iam_users(request: Request): 64 | data = await request.json() 65 | gcp_creds = data.get("gcp_creds") 66 | gcp_creds = json.loads(gcp_creds) 67 | if not gcp_creds: 68 | raise HTTPException(status_code=400, detail="Missing GCP credentials") 69 | try: 70 | credentials = service_account.Credentials.from_service_account_info(gcp_creds) 71 | client = iam.IAMClient(credentials=credentials) 72 | users = list(client.list_service_accounts()) 73 | user_emails = [user.email for user in users] 74 | return {"iam_users": user_emails} 75 | except Exception as e: 76 | raise HTTPException(status_code=500, detail=str(e)) 77 | -------------------------------------------------------------------------------- /secrets-ninja-proxy/services/mongodb.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Request, HTTPException 2 | from pymongo import MongoClient 3 | from pymongo.errors import ConnectionFailure 4 | 5 | router = APIRouter() 6 | 7 | @router.post("/list_databases") 8 | async def list_databases(request: Request): 9 | data = await request.json() 10 | mongodb_uri = data.get("mongodb_uri") 11 | if not mongodb_uri: 12 | raise HTTPException(status_code=400, detail="Missing MongoDB URI") 13 | try: 14 | client = MongoClient(mongodb_uri) 15 | db_list = client.list_database_names() 16 | dbs_info = [] 17 | for db_name in db_list: 18 | db = client[db_name] 19 | stats = db.command("dbstats") 20 | collections = db.list_collection_names() 21 | num_collections = len(collections) 22 | total_indexes = 0 23 | for coll in collections: 24 | indexes = db[coll].index_information() 25 | total_indexes += len(indexes) 26 | dbs_info.append({ 27 | "dbname": db_name, 28 | "dbsize": stats.get("dataSize", 0), 29 | "number_of_collections": num_collections, 30 | "number_of_indexes": total_indexes 31 | }) 32 | return {"databases": dbs_info} 33 | except Exception as e: 34 | raise HTTPException(status_code=500, detail=str(e)) 35 | 36 | @router.post("/list_db_collections") 37 | async def list_db_collections(request: Request): 38 | data = await request.json() 39 | mongodb_uri = data.get("mongodb_uri") 40 | database_name = data.get("database") 41 | if not mongodb_uri or not database_name: 42 | raise HTTPException(status_code=400, detail="Missing MongoDB URI or database name") 43 | try: 44 | client = MongoClient(mongodb_uri) 45 | db = client[database_name] 46 | collections = db.list_collection_names() 47 | collections_info = [] 48 | for coll in collections: 49 | stats = db.command("collstats", coll) 50 | collections_info.append({ 51 | "collection_name": coll, 52 | "size": stats.get("size", 0), 53 | "doc_count": stats.get("count", 0), 54 | "avg_doc_size": stats.get("avgObjSize", 0), 55 | "index_count": stats.get("nindexes", 0) 56 | }) 57 | return {"collections": collections_info} 58 | except Exception as e: 59 | raise HTTPException(status_code=500, detail=str(e)) 60 | 61 | @router.post("/list_records") 62 | async def list_records(request: Request): 63 | data = await request.json() 64 | mongodb_uri = data.get("mongodb_uri") 65 | database_name = data.get("database") 66 | collection_name = data.get("collection") 67 | if not mongodb_uri or not database_name or not collection_name: 68 | raise HTTPException(status_code=400, detail="Missing MongoDB URI, database, or collection name") 69 | try: 70 | client = MongoClient(mongodb_uri) 71 | db = client[database_name] 72 | collection = db[collection_name] 73 | records = list(collection.find().limit(5)) 74 | # Convert ObjectId to string for JSON serialization if needed 75 | for record in records: 76 | if "_id" in record: 77 | record["_id"] = str(record["_id"]) 78 | return {"records": records} 79 | except Exception as e: 80 | raise HTTPException(status_code=500, detail=str(e)) 81 | -------------------------------------------------------------------------------- /secrets-ninja-proxy/services/postgres.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Request, HTTPException 2 | import psycopg2 3 | import psycopg2.extras 4 | from urllib.parse import urlparse, parse_qs 5 | 6 | router = APIRouter() 7 | 8 | def connect_to_postgres(postgres_uri): 9 | def try_connect(uri): 10 | try: 11 | return psycopg2.connect(uri) 12 | except psycopg2.OperationalError: 13 | return None 14 | 15 | conn = try_connect(postgres_uri) 16 | if conn: 17 | return conn 18 | 19 | try: 20 | parsed_uri = urlparse(postgres_uri) 21 | query_params = parse_qs(parsed_uri.query) 22 | 23 | current_sslmode = query_params.get('sslmode', ['require'])[0] 24 | 25 | # Flip sslmode 26 | new_sslmode = 'disable' if current_sslmode == 'require' else 'require' 27 | 28 | new_params = {k: v[0] for k, v in query_params.items() if k != 'sslmode'} 29 | new_params['sslmode'] = new_sslmode 30 | query_string = '&'.join([f"{k}={v}" for k, v in new_params.items()]) 31 | 32 | new_uri = f"{parsed_uri.scheme}://{parsed_uri.netloc}{parsed_uri.path}?{query_string}" 33 | 34 | conn = try_connect(new_uri) 35 | if conn: 36 | return conn 37 | 38 | raise HTTPException(status_code=500, detail="Failed to connect to PostgreSQL with both sslmode options") 39 | 40 | except Exception as e: 41 | raise HTTPException(status_code=500, detail=f"Failed to connect to PostgreSQL: {str(e)}") 42 | 43 | @router.post("/list_databases") 44 | async def list_databases(request: Request): 45 | data = await request.json() 46 | postgres_uri = data.get("postgres_uri") 47 | 48 | if not postgres_uri: 49 | raise HTTPException(status_code=400, detail="Missing PostgreSQL URI") 50 | 51 | try: 52 | conn = connect_to_postgres(postgres_uri) 53 | cursor = conn.cursor(cursor_factory=psycopg2.extras.DictCursor) 54 | 55 | query = """ 56 | SELECT 57 | d.datname as database_name, 58 | pg_database_size(d.datname) as database_size, 59 | pg_size_pretty(pg_database_size(d.datname)) as pretty_size 60 | FROM 61 | pg_database d 62 | WHERE 63 | d.datistemplate = false 64 | ORDER BY 65 | pg_database_size(d.datname) DESC; 66 | """ 67 | 68 | cursor.execute(query) 69 | databases = [] 70 | 71 | for row in cursor.fetchall(): 72 | databases.append({ 73 | "dbname": row["database_name"], 74 | "dbsize": row["database_size"], 75 | "pretty_size": row["pretty_size"] 76 | }) 77 | 78 | cursor.close() 79 | conn.close() 80 | 81 | return {"databases": databases} 82 | 83 | except Exception as e: 84 | raise HTTPException(status_code=500, detail=str(e)) 85 | 86 | def modify_uri_database(postgres_uri, database_name): 87 | parsed_uri = urlparse(postgres_uri) 88 | 89 | if parsed_uri.path and parsed_uri.path != "/": 90 | path_parts = parsed_uri.path.split('/') 91 | path_parts[-1] = database_name 92 | new_path = '/'.join(path_parts) 93 | else: 94 | new_path = f"/{database_name}" 95 | 96 | netloc = parsed_uri.netloc 97 | scheme = parsed_uri.scheme 98 | query = parsed_uri.query 99 | 100 | new_uri = f"{scheme}://{netloc}{new_path}" 101 | if query: 102 | new_uri += f"?{query}" 103 | 104 | return new_uri 105 | 106 | @router.post("/list_db_tables") 107 | async def list_db_tables(request: Request): 108 | data = await request.json() 109 | postgres_uri = data.get("postgres_uri") 110 | database_name = data.get("database") 111 | 112 | if not postgres_uri or not database_name: 113 | raise HTTPException(status_code=400, detail="Missing PostgreSQL URI or database name") 114 | 115 | try: 116 | new_uri = modify_uri_database(postgres_uri, database_name) 117 | conn = connect_to_postgres(new_uri) 118 | cursor = conn.cursor(cursor_factory=psycopg2.extras.DictCursor) 119 | 120 | query = """ 121 | SELECT 122 | t.table_schema, 123 | t.table_name, 124 | pg_relation_size(quote_ident(t.table_schema) || '.' || quote_ident(t.table_name)) as table_size, 125 | pg_size_pretty(pg_relation_size(quote_ident(t.table_schema) || '.' || quote_ident(t.table_name))) as pretty_size, 126 | (SELECT count(*) FROM information_schema.columns WHERE table_schema = t.table_schema AND table_name = t.table_name) as column_count, 127 | (SELECT count(*) FROM pg_indexes WHERE schemaname = t.table_schema AND tablename = t.table_name) as index_count, 128 | coalesce(s.n_live_tup, 0) as estimated_row_count 129 | FROM 130 | information_schema.tables t 131 | LEFT JOIN 132 | pg_stat_user_tables s ON s.schemaname = t.table_schema AND s.relname = t.table_name 133 | WHERE 134 | t.table_schema NOT IN ('pg_catalog', 'information_schema') 135 | AND t.table_type = 'BASE TABLE' 136 | ORDER BY 137 | t.table_schema, t.table_name; 138 | """ 139 | 140 | cursor.execute(query) 141 | tables = [] 142 | 143 | for row in cursor.fetchall(): 144 | tables.append({ 145 | "schema": row["table_schema"], 146 | "table_name": row["table_name"], 147 | "size": row["table_size"], 148 | "pretty_size": row["pretty_size"], 149 | "column_count": row["column_count"], 150 | "index_count": row["index_count"], 151 | "estimated_row_count": row["estimated_row_count"] 152 | }) 153 | 154 | cursor.close() 155 | conn.close() 156 | 157 | return {"tables": tables} 158 | 159 | except Exception as e: 160 | raise HTTPException(status_code=500, detail=str(e)) 161 | 162 | @router.post("/list_records") 163 | async def list_records(request: Request): 164 | data = await request.json() 165 | postgres_uri = data.get("postgres_uri") 166 | database_name = data.get("database") 167 | table_name = data.get("table") 168 | schema_name = data.get("schema", "public") 169 | 170 | if not postgres_uri or not database_name or not table_name: 171 | raise HTTPException(status_code=400, detail="Missing PostgreSQL URI, database, or table name") 172 | 173 | try: 174 | new_uri = modify_uri_database(postgres_uri, database_name) 175 | conn = connect_to_postgres(new_uri) 176 | cursor = conn.cursor(cursor_factory=psycopg2.extras.DictCursor) 177 | 178 | full_table_name = f'"{schema_name}"."{table_name}"' 179 | 180 | cursor.execute(""" 181 | SELECT EXISTS ( 182 | SELECT FROM information_schema.tables 183 | WHERE table_schema = %s AND table_name = %s 184 | ) 185 | """, (schema_name, table_name)) 186 | 187 | if not cursor.fetchone()[0]: 188 | raise HTTPException(status_code=404, detail=f"Table {schema_name}.{table_name} not found") 189 | 190 | query = f"SELECT * FROM {full_table_name} LIMIT 5" 191 | cursor.execute(query) 192 | 193 | columns = [desc[0] for desc in cursor.description] 194 | records = [] 195 | 196 | for row in cursor.fetchall(): 197 | record = {} 198 | for i, col in enumerate(columns): 199 | if isinstance(row[i], (bytes, memoryview)): 200 | record[col] = "binary data" 201 | else: 202 | record[col] = row[i] 203 | records.append(record) 204 | 205 | cursor.close() 206 | conn.close() 207 | 208 | return {"records": records} 209 | 210 | except Exception as e: 211 | raise HTTPException(status_code=500, detail=str(e)) 212 | -------------------------------------------------------------------------------- /secrets-ninja-proxy/services/rabbitmq.py: -------------------------------------------------------------------------------- 1 | from fastapi import APIRouter, Request, HTTPException 2 | import pika 3 | import requests 4 | from requests.auth import HTTPBasicAuth 5 | 6 | router = APIRouter() 7 | 8 | def parse_connection_string(connection_string): 9 | if not connection_string.startswith(("amqp://", "amqps://")): 10 | raise ValueError("Invalid AMQP/AMQPS connection string") 11 | scheme_removed = connection_string.split("://", 1)[1] 12 | creds, host_port = scheme_removed.split("@") 13 | username, password = creds.split(":") 14 | if ":" in host_port: 15 | host, port = host_port.split(":") 16 | else: 17 | host = host_port 18 | port = "5671" if connection_string.startswith("amqps://") else "5672" 19 | return username, password, host, port 20 | 21 | @router.post("/get_queues") 22 | async def get_queues(request: Request): 23 | data = await request.json() 24 | connection_string = data.get("connection_string") 25 | if not connection_string: 26 | raise HTTPException(status_code=400, detail="Missing connection string") 27 | try: 28 | username, password, host, port = parse_connection_string(connection_string) 29 | protocol = "https" if connection_string.startswith("amqps://") else "http" 30 | url = f"{protocol}://{host}:15672/api/queues" 31 | response = requests.get(url, auth=HTTPBasicAuth(username, password), timeout=15, verify=False if protocol=="https" else True) 32 | if response.status_code == 200: 33 | queues = response.json() 34 | result = [] 35 | for q in queues: 36 | result.append({ 37 | "name": q["name"], 38 | "messages": q["messages"], 39 | "consumers": q["consumers"] 40 | }) 41 | return {"queues": result} 42 | else: 43 | raise HTTPException(status_code=500, detail=f"Failed to get queues, HTTP {response.status_code}") 44 | except Exception as e: 45 | raise HTTPException(status_code=500, detail=str(e)) 46 | 47 | @router.post("/get_queue_data") 48 | async def get_queue_data(request: Request): 49 | data = await request.json() 50 | connection_string = data.get("connection_string") 51 | queue_name = data.get("queue_name") 52 | if not connection_string or not queue_name: 53 | raise HTTPException(status_code=400, detail="Missing connection string or queue name") 54 | try: 55 | params = pika.URLParameters(connection_string) 56 | connection = pika.BlockingConnection(params) 57 | channel = connection.channel() 58 | 59 | messages = [] 60 | for _ in range(5): 61 | method_frame, header_frame, body = channel.basic_get(queue=queue_name, auto_ack=True) 62 | if method_frame: 63 | messages.append(body.decode()) 64 | else: 65 | break 66 | 67 | connection.close() 68 | return {"messages": messages} 69 | except Exception as e: 70 | raise HTTPException(status_code=500, detail=str(e)) 71 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | #root { 2 | max-width: 1280px; 3 | margin: 0 auto; 4 | padding: 2rem; 5 | text-align: center; 6 | } 7 | 8 | .logo { 9 | height: 6em; 10 | padding: 1.5em; 11 | will-change: filter; 12 | transition: filter 300ms; 13 | } 14 | .logo:hover { 15 | filter: drop-shadow(0 0 2em #646cffaa); 16 | } 17 | .logo.react:hover { 18 | filter: drop-shadow(0 0 2em #61dafbaa); 19 | } 20 | 21 | @keyframes logo-spin { 22 | from { 23 | transform: rotate(0deg); 24 | } 25 | to { 26 | transform: rotate(360deg); 27 | } 28 | } 29 | 30 | @media (prefers-reduced-motion: no-preference) { 31 | a:nth-of-type(2) .logo { 32 | animation: logo-spin infinite 20s linear; 33 | } 34 | } 35 | 36 | .card { 37 | padding: 2em; 38 | } 39 | 40 | .read-the-docs { 41 | color: #888; 42 | } 43 | -------------------------------------------------------------------------------- /src/App.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import FlowbiteNavbar from './components/navbar'; 3 | import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; 4 | import SB from './components/sidebar'; 5 | import { Flowbite } from 'flowbite-react'; 6 | import UniversalComponent from './modules/universal'; 7 | import Dashboard from './components/dashboard'; 8 | import servicesConfig from './data/detectors.json'; 9 | 10 | // Sort servicesConfig by keys alphabetically 11 | const sortedServicesConfig = Object.keys(servicesConfig) 12 | .sort() 13 | .reduce((result, key) => { 14 | result[key] = servicesConfig[key]; 15 | return result; 16 | }, {}); 17 | 18 | // Custom hook to listen to window resize events 19 | const useWindowSize = () => { 20 | const [size, setSize] = useState([window.innerWidth, window.innerHeight]); 21 | useEffect(() => { 22 | const handleResize = () => { 23 | setSize([window.innerWidth, window.innerHeight]); 24 | }; 25 | window.addEventListener('resize', handleResize); 26 | return () => window.removeEventListener('resize', handleResize); 27 | }, []); 28 | return size; 29 | }; 30 | 31 | export default function MyPage() { 32 | const [sidebarVisible, setSidebarVisible] = useState(true); 33 | const [width] = useWindowSize(); 34 | 35 | const toggleSidebar = () => { 36 | setSidebarVisible(!sidebarVisible); 37 | }; 38 | 39 | const isLargeScreen = width >= 1024; 40 | 41 | // Adjusted styles to ensure footer is always visible and not overlaid 42 | const containerStyle = `flex flex-col h-screen`; 43 | const contentContainerStyle = isLargeScreen 44 | ? 'flex flex-1 overflow-hidden' 45 | : 'flex flex-1 overflow-hidden'; 46 | const sidebarStyle = isLargeScreen 47 | ? { maxHeight: 'calc(100vh - 60px)', overflowY: 'auto' } // Adjust for navbar height 48 | : { 49 | zIndex: 30, 50 | position: 'fixed', 51 | width: '100%', 52 | height: 'calc(100vh - 60px)', 53 | overflowY: 'auto', 54 | }; 55 | const contentStyle = isLargeScreen 56 | ? 'flex-1 bg-gray-100 dark:bg-gray-700 overflow-auto' 57 | : `flex-1 bg-gray-100 dark:bg-gray-700 overflow-auto ${sidebarVisible ? 'sidebar-overlay' : ''}`; 58 | 59 | return ( 60 | 61 | 62 |
63 |
64 | 65 |
66 |
{ 69 | if (!isLargeScreen && sidebarVisible) { 70 | toggleSidebar(); 71 | } 72 | }} 73 | > 74 | {sidebarVisible && ( 75 |
76 | 81 |
82 | )} 83 |
84 | 85 | } 88 | /> 89 | {Object.keys(sortedServicesConfig).map((service) => ( 90 | 98 | } 99 | /> 100 | ))} 101 | 102 |
103 |
104 |
105 |
106 |
107 | ); 108 | } 109 | -------------------------------------------------------------------------------- /src/assets/logo-t.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NikhilPanwar/secrets-ninja/9db8616d4024cfe7329b439818769f94b8cde70a/src/assets/logo-t.png -------------------------------------------------------------------------------- /src/assets/logo.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NikhilPanwar/secrets-ninja/9db8616d4024cfe7329b439818769f94b8cde70a/src/assets/logo.jpg -------------------------------------------------------------------------------- /src/assets/react.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/copy_button.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import { Button } from 'flowbite-react'; 3 | import { FaRegClipboard, FaCheck } from 'react-icons/fa'; 4 | 5 | const CopyButton = ({ textToCopy }) => { 6 | const [buttonText, setButtonText] = useState('Copy'); 7 | const [isCopied, setIsCopied] = useState(false); 8 | 9 | const copyToClipboard = () => { 10 | navigator.clipboard 11 | .writeText(textToCopy) 12 | .then(() => { 13 | setButtonText('Copied'); 14 | setIsCopied(true); 15 | setTimeout(() => { 16 | setButtonText('Copy'); 17 | setIsCopied(false); 18 | }, 1000); 19 | }) 20 | .catch((err) => { 21 | console.error('Could not copy text: ', err); 22 | }); 23 | }; 24 | 25 | return ( 26 | 34 | ); 35 | }; 36 | 37 | export default CopyButton; 38 | -------------------------------------------------------------------------------- /src/components/dashboard.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MainPageTable from './table'; 3 | import { Alert, Button } from 'flowbite-react'; 4 | import { ImNewTab } from 'react-icons/im'; 5 | import { FaGithub } from 'react-icons/fa'; 6 | import { FaXTwitter } from 'react-icons/fa6'; 7 | 8 | export default function Dashboard({ servicesConfig }) { 9 | return ( 10 |
11 | 12 | 63 | 64 |
65 |

66 | Supported Keys 67 |

68 |
69 | 70 |
71 |
72 |
73 | ); 74 | } 75 | -------------------------------------------------------------------------------- /src/components/footer.jsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Footer } from 'flowbite-react'; 4 | import { 5 | BsDribbble, 6 | BsFacebook, 7 | BsGithub, 8 | BsInstagram, 9 | BsTwitter, 10 | } from 'react-icons/bs'; 11 | import logo from '../assets/logo-t.png'; 12 | 13 | function FT() { 14 | return ( 15 | 60 | ); 61 | } 62 | 63 | export default FT; 64 | -------------------------------------------------------------------------------- /src/components/navbar.jsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; // Import useState 2 | import { Navbar } from 'flowbite-react'; 3 | import { DarkThemeToggle } from 'flowbite-react'; 4 | import logo from '../assets/logo-t.png'; 5 | import Hamburger from 'hamburger-react'; 6 | 7 | function FlowbiteNavbar({ toggleSidebar }) { 8 | const [isOpen, setIsOpen] = useState(true); // State to manage sidebar open/close 9 | 10 | // Function to toggle sidebar and hamburger state 11 | const handleToggle = () => { 12 | // setIsOpen(!isOpen); // Toggle the state 13 | toggleSidebar(); // Call the prop function to actually toggle the sidebar 14 | }; 15 | 16 | const toggleStyle = { 17 | fontSize: '0.8rem', 18 | padding: '10px', 19 | }; 20 | 21 | return ( 22 | 23 |
24 | 34 | 35 | Secrets Ninja Logo 36 | 37 | Secrets Ninja 38 | 39 | 40 |
41 |
{/* */}
42 | {/* */} 43 | {/* 44 | Keys Checker 45 | */} 46 | {/* Find Your Secrets */} 47 |
48 | 49 |
50 | {/*
*/} 51 |
52 | ); 53 | } 54 | 55 | export default FlowbiteNavbar; 56 | -------------------------------------------------------------------------------- /src/components/pages/secrets_checker.jsx: -------------------------------------------------------------------------------- 1 | export {}; 2 | -------------------------------------------------------------------------------- /src/components/request_window.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Card } from 'flowbite-react'; 3 | import CopyButton from './copy_button'; 4 | import '../css/json_theme.css'; 5 | import { Light as SyntaxHighlighter } from 'react-syntax-highlighter'; 6 | import bash from 'react-syntax-highlighter/dist/esm/languages/hljs/bash'; 7 | import { github } from 'react-syntax-highlighter/dist/esm/styles/hljs'; 8 | 9 | SyntaxHighlighter.registerLanguage('bash', bash); 10 | 11 | const customStyle = { 12 | ...github, 13 | 'hljs': { 14 | ...github['hljs'], 15 | background: 'transparent', 16 | color: '#333' 17 | }, 18 | 'hljs-string': { 19 | ...github['hljs-string'], 20 | color: '#ff5e5e' 21 | }, 22 | 'hljs-literal': { 23 | ...github['hljs-literal'], 24 | color: '#ff5e5e' 25 | }, 26 | 'hljs-number': { 27 | ...github['hljs-number'], 28 | color: '#ff5e5e' 29 | }, 30 | 'hljs-built_in': { 31 | ...github['hljs-built_in'], 32 | color: '#ff5e5e' 33 | } 34 | }; 35 | 36 | function RequestWindow({ curl = '' }) { 37 | return ( 38 | 39 |
40 |

Request

41 | 42 |
43 |
44 | 53 | {curl} 54 | 55 |
56 |
57 | ); 58 | } 59 | 60 | export default RequestWindow; 61 | -------------------------------------------------------------------------------- /src/components/requests.jsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-case-declarations */ 2 | async function makeUniversalRequest( 3 | isProxyEnabled, 4 | serviceType, 5 | inputData, 6 | endpointURL, 7 | requestMethod 8 | ) { 9 | let response, data; 10 | const proxyURL = import.meta.env.VITE_SECRETS_NINJA_PROXY_ENDPOINT; 11 | endpointURL = isProxyEnabled 12 | ? proxyURL + `/fetch/` + endpointURL 13 | : endpointURL; 14 | 15 | switch (serviceType) { 16 | case 'Stripe': 17 | response = await fetch(endpointURL, { 18 | method: 'GET', 19 | headers: { 20 | Authorization: `Bearer ${inputData.api_key}`, 21 | }, 22 | }); 23 | break; 24 | case 'Paypal': 25 | let accessToken; 26 | const credentials = `${inputData.client_id}:${inputData.client_secret}`; 27 | const encodedCredentials = btoa(credentials); 28 | let tokenResponse = await fetch( 29 | 'https://api.paypal.com/v1/oauth2/token', 30 | { 31 | method: 'POST', 32 | headers: { 33 | Accept: 'application/json', 34 | 'Accept-Language': 'en_US', 35 | Authorization: `Basic ${encodedCredentials}`, 36 | }, 37 | body: new URLSearchParams({ grant_type: 'client_credentials' }), 38 | } 39 | ); 40 | if (endpointURL !== 'https://api.paypal.com/v1/oauth2/token') { 41 | const tokenData = await tokenResponse.json(); 42 | accessToken = tokenData.access_token; 43 | response = await fetch(endpointURL, { 44 | method: 'GET', 45 | headers: { 46 | Authorization: `Bearer ${accessToken}`, 47 | 'Content-Type': 'application/json', 48 | }, 49 | }); 50 | } else { 51 | response = tokenResponse; 52 | } 53 | break; 54 | case 'OpenAI': 55 | response = await fetch(endpointURL, { 56 | method: requestMethod, 57 | headers: { 58 | Authorization: `Bearer ${inputData.api_key}`, 59 | }, 60 | }); 61 | break; 62 | case 'Paystack': 63 | response = await fetch(endpointURL, { 64 | method: requestMethod, 65 | headers: { 66 | Authorization: `Bearer ${inputData.api_key}`, 67 | }, 68 | }); 69 | break; 70 | case 'Github': 71 | response = await fetch( 72 | endpointURL 73 | .replace('', inputData.org_name) 74 | .replace('', inputData.package_type) 75 | .replace('', inputData.query), 76 | { 77 | method: requestMethod, 78 | headers: { 79 | Authorization: `token ${inputData.access_token}`, 80 | }, 81 | } 82 | ); 83 | break; 84 | case 'LaunchDarkly': 85 | response = await fetch(endpointURL, { 86 | method: requestMethod, 87 | headers: { 88 | Authorization: ` ${inputData.api_key}`, 89 | }, 90 | }); 91 | break; 92 | case 'Omnisend': 93 | response = await fetch(endpointURL, { 94 | method: requestMethod, 95 | headers: { 96 | 'X-API-KEY': `${inputData.api_key}`, 97 | }, 98 | }); 99 | break; 100 | case 'Telegram': 101 | response = await fetch( 102 | endpointURL.replace('', inputData.bot_token), 103 | { 104 | method: requestMethod, 105 | } 106 | ); 107 | break; 108 | case 'Clearbit': 109 | response = await fetch(endpointURL + inputData.email, { 110 | method: requestMethod, 111 | headers: { 112 | Authorization: 'Basic ' + btoa(inputData.api_key + ':'), 113 | }, 114 | }); 115 | break; 116 | case 'SendInBlue': 117 | response = await fetch(endpointURL, { 118 | method: requestMethod, 119 | headers: { 120 | 'api-key': `${inputData.api_key}`, 121 | }, 122 | }); 123 | break; 124 | case 'Twitter': 125 | const isTokenEndpoint = endpointURL.includes('oauth2/token'); 126 | const options = isTokenEndpoint 127 | ? { 128 | method: 'POST', 129 | headers: { 'Content-Type': 'application/json' }, 130 | body: JSON.stringify({ 131 | proxied_data: { 132 | headers: { 133 | Authorization: 134 | 'Basic ' + btoa(`${inputData.api_key}:${inputData.api_secret_key}`), 135 | 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', 136 | }, 137 | }, 138 | body: 'grant_type=client_credentials', 139 | }), 140 | } 141 | : { 142 | method: requestMethod, 143 | headers: { 144 | Authorization: 'Bearer ' + inputData.access_token, 145 | }, 146 | }; 147 | response = await fetch(endpointURL, options); 148 | break; 149 | case 'RechargePayments': 150 | response = await fetch(endpointURL, { 151 | method: requestMethod, 152 | headers: { 153 | 'X-Recharge-Access-Token': `${inputData.api_key}`, 154 | }, 155 | }); 156 | break; 157 | case 'MailerLite': 158 | response = await fetch(endpointURL, { 159 | method: requestMethod, 160 | headers: { 161 | 'X-MailerLite-ApiKey': `${inputData.api_key}`, 162 | }, 163 | }); 164 | break; 165 | case 'Trello': 166 | response = await fetch( 167 | endpointURL 168 | .replace('', inputData.key) 169 | .replace('', inputData.token), 170 | { 171 | method: requestMethod, 172 | } 173 | ); 174 | break; 175 | case 'RazorPay': 176 | response = await fetch(endpointURL, { 177 | method: requestMethod, 178 | headers: { 179 | Authorization: `Basic ${btoa(inputData.key_id + ':' + inputData.key_secret)}`, 180 | }, 181 | }); 182 | break; 183 | case 'Twilio': 184 | response = await fetch( 185 | endpointURL.replace('', inputData.account_sid), 186 | { 187 | method: requestMethod, 188 | headers: { 189 | Authorization: `Basic ${btoa(inputData.account_sid + ':' + inputData.auth_token)}`, 190 | }, 191 | } 192 | ); 193 | break; 194 | case 'NpmToken': 195 | response = await fetch(endpointURL.replace('', inputData.org).replace('', inputData.user), { 196 | method: requestMethod, 197 | headers: { 198 | Authorization: `Bearer ${inputData.token}`, 199 | }, 200 | }); 201 | break; 202 | case 'Mailgun': 203 | response = await fetch( 204 | endpointURL.replace('', inputData.domain), 205 | { 206 | method: requestMethod, 207 | headers: { 208 | Authorization: `Basic ${btoa('api:' + inputData.api_key)}`, 209 | }, 210 | } 211 | ); 212 | break; 213 | case 'Klaviyo': 214 | response = await fetch(endpointURL, { 215 | method: requestMethod, 216 | headers: { 217 | Revision: '2023-02-22', 218 | Authorization: `Klaviyo-API-Key ${inputData.api_key}`, 219 | }, 220 | }); 221 | break; 222 | case 'DigitalOcean': 223 | response = await fetch(endpointURL, { 224 | method: requestMethod, 225 | headers: { 226 | Authorization: `Bearer ${inputData.api_key}`, 227 | }, 228 | }); 229 | break; 230 | case 'Honeycomb': 231 | response = await fetch(endpointURL, { 232 | method: requestMethod, 233 | headers: { 234 | 'X-Honeycomb-Team': `${inputData.api_key}`, 235 | }, 236 | }); 237 | break; 238 | case 'Eventbrite': 239 | response = await fetch(endpointURL + `?token=${inputData.token}`, { 240 | method: requestMethod, 241 | }); 242 | break; 243 | case 'SendGrid': 244 | response = await fetch(endpointURL, { 245 | method: requestMethod, 246 | headers: { 247 | Authorization: `Bearer ${inputData.api_key}`, 248 | }, 249 | }); 250 | break; 251 | case 'MailChimp': 252 | response = await fetch(endpointURL.replace('', inputData.dc), { 253 | method: requestMethod, 254 | headers: { 255 | Authorization: `Basic ${btoa('anystring:' + inputData.api_key)}`, 256 | }, 257 | }); 258 | break; 259 | case 'Postmark': 260 | response = await fetch(endpointURL, { 261 | method: requestMethod, 262 | headers: { 263 | 'Content-Type': 'application/json', 264 | 'X-Postmark-Server-Token': `${inputData.server_token}`, 265 | }, 266 | }); 267 | break; 268 | case 'Telnyx': 269 | response = await fetch(endpointURL, { 270 | method: requestMethod, 271 | headers: { 272 | Authorization: `Bearer ${inputData.api_key}`, 273 | }, 274 | }); 275 | break; 276 | case 'Pipedrive': 277 | response = await fetch( 278 | endpointURL + `?api_token=${inputData.api_token}`, 279 | { 280 | method: requestMethod, 281 | } 282 | ); 283 | break; 284 | case 'Vercel': 285 | response = await fetch(endpointURL, { 286 | method: requestMethod, 287 | headers: { 288 | Authorization: `Bearer ${inputData.api_token}`, 289 | }, 290 | }); 291 | break; 292 | case 'Bitly': 293 | response = await fetch(endpointURL, { 294 | method: requestMethod, 295 | headers: { 296 | Authorization: `Bearer ${inputData.api_token}`, 297 | }, 298 | }); 299 | break; 300 | case 'Algolia': 301 | response = await fetch( 302 | endpointURL.replace('', inputData.app_id), 303 | { 304 | method: requestMethod, 305 | headers: { 306 | 'X-Algolia-API-Key': `${inputData.api_key}`, 307 | 'X-Algolia-Application-Id': `${inputData.app_id}`, 308 | }, 309 | } 310 | ); 311 | break; 312 | case 'Posthog': 313 | response = await fetch( 314 | endpointURL.replace('', inputData.api_key), 315 | { 316 | method: requestMethod, 317 | } 318 | ); 319 | if (response.status === 401) { 320 | response = await fetch( 321 | endpointURL 322 | .replace('', inputData.api_key) 323 | .replace('https://app.', 'https://eu.'), 324 | { 325 | method: requestMethod, 326 | } 327 | ); 328 | } 329 | break; 330 | case 'Opsgenie': 331 | response = await fetch(endpointURL, { 332 | method: requestMethod, 333 | headers: { 334 | Authorization: `GenieKey ${inputData.api_key}`, 335 | }, 336 | }); 337 | break; 338 | case 'Helpscout': 339 | response = await fetch(endpointURL, { 340 | method: requestMethod, 341 | headers: { 342 | Authorization: 'Basic ' + btoa(inputData.api_key + ':'), 343 | }, 344 | }); 345 | break; 346 | case 'Typeform': 347 | response = await fetch(endpointURL, { 348 | method: requestMethod, 349 | headers: { 350 | Authorization: `Bearer ${inputData.api_key}`, 351 | }, 352 | }); 353 | break; 354 | case 'GetResponse': 355 | response = await fetch(endpointURL, { 356 | method: requestMethod, 357 | headers: { 358 | 'X-Auth-Token': `api-key ${inputData.api_key}`, 359 | }, 360 | }); 361 | break; 362 | case 'YouSign': 363 | response = await fetch(endpointURL, { 364 | method: requestMethod, 365 | headers: { 366 | Authorization: `Bearer ${inputData.api_key}`, 367 | }, 368 | }); 369 | break; 370 | case 'Notion': 371 | response = await fetch(endpointURL, { 372 | method: requestMethod, 373 | headers: { 374 | Authorization: `Bearer ${inputData.api_key}`, 375 | 'Notion-Version': '2022-06-28', 376 | }, 377 | }); 378 | break; 379 | case 'MadKudu': 380 | response = await fetch(endpointURL, { 381 | method: requestMethod, 382 | headers: { 383 | Authorization: `Basic ${btoa(inputData.api_key + ':')}`, 384 | }, 385 | }); 386 | break; 387 | case 'Autopilot': 388 | response = await fetch(endpointURL, { 389 | method: requestMethod, 390 | headers: { 391 | autopilotapikey: `${inputData.api_key}`, 392 | }, 393 | }); 394 | break; 395 | case 'Slack': 396 | response = await fetch(endpointURL.replace('', inputData.channel_id), { 397 | method: requestMethod, 398 | headers: { 399 | Authorization: `Bearer ${inputData.api_token}`, 400 | }, 401 | }); 402 | break; 403 | case 'Gitlab': 404 | response = await fetch( 405 | endpointURL 406 | .replace('', inputData.project_id) 407 | .replace('', inputData.user_id), 408 | { 409 | method: requestMethod, 410 | headers: { 411 | Authorization: `Bearer ${inputData.access_token}`, 412 | }, 413 | } 414 | ); 415 | break; 416 | case 'BitBucket': 417 | response = await fetch(endpointURL, { 418 | method: requestMethod, 419 | headers: { 420 | Authorization: `Basic ${btoa(inputData.username + ':' + inputData.password)}`, 421 | }, 422 | }); 423 | break; 424 | case 'HuggingFace': 425 | response = await fetch(endpointURL.replace('', inputData.org), { 426 | method: requestMethod, 427 | headers: { 428 | authorization: `Bearer ${inputData.api_token}`, 429 | }, 430 | }); 431 | break; 432 | case 'Shodan': 433 | response = await fetch( 434 | endpointURL.replace('', inputData.api_key), 435 | { 436 | method: requestMethod, 437 | } 438 | ); 439 | break; 440 | case 'Postman': 441 | response = await fetch(endpointURL, { 442 | method: requestMethod, 443 | headers: { 444 | 'X-Api-Key': `${inputData.api_key}`, 445 | }, 446 | }); 447 | break; 448 | case 'Terraform': 449 | response = await fetch(endpointURL, { 450 | method: requestMethod, 451 | headers: { 452 | Authorization: `Bearer ${inputData.api_token}`, 453 | }, 454 | }); 455 | break; 456 | case 'Doppler': 457 | response = await fetch(endpointURL, { 458 | method: requestMethod, 459 | headers: { 460 | Authorization: `Bearer ${inputData.api_key}`, 461 | }, 462 | }); 463 | break; 464 | case 'Shopify': 465 | response = await fetch( 466 | endpointURL.replace( 467 | encodeURIComponent(''), 468 | inputData.store_domain 469 | ), 470 | { 471 | method: requestMethod, 472 | headers: { 473 | 'X-Shopify-Access-Token': `${inputData.api_key}`, 474 | }, 475 | } 476 | ); 477 | break; 478 | case 'Jfrog': 479 | response = await fetch( 480 | endpointURL.replace(encodeURIComponent(''), inputData.domain), 481 | { 482 | method: requestMethod, 483 | headers: { 484 | Authorization: `Basic ${btoa(`${inputData.username}:${inputData.password}`)}`, 485 | 'Content-Type': 'application/json', 486 | }, 487 | } 488 | ); 489 | break; 490 | case 'Buildkite': 491 | response = await fetch(endpointURL, { 492 | method: requestMethod, 493 | headers: { 494 | Authorization: `Bearer ${inputData.api_key}`, 495 | }, 496 | }); 497 | break; 498 | case 'Pulumi': 499 | response = await fetch(endpointURL, { 500 | method: requestMethod, 501 | headers: { 502 | Authorization: `token ${inputData.api_key}`, 503 | }, 504 | }); 505 | break; 506 | case 'Snyk': 507 | response = await fetch(endpointURL, { 508 | method: requestMethod, 509 | headers: { 510 | Authorization: `token ${inputData.api_key}`, 511 | }, 512 | }); 513 | break; 514 | case 'EvolutionAPI': 515 | response = await fetch( 516 | endpointURL.replace('', inputData.instance_url), 517 | { 518 | method: requestMethod, 519 | headers: { 520 | apikey: `${inputData.api_key}`, 521 | }, 522 | } 523 | ); 524 | break; 525 | case 'PushBullet': 526 | response = await fetch(endpointURL, { 527 | method: requestMethod, 528 | headers: { 529 | 'Access-Token': `${inputData.api_key}`, 530 | }, 531 | }); 532 | break; 533 | case 'SquareAccessToken': 534 | response = await fetch(endpointURL, { 535 | method: requestMethod, 536 | headers: { 537 | Authorization: `Bearer ${inputData.access_token}`, 538 | }, 539 | }); 540 | break; 541 | case 'Sentry': 542 | response = await fetch(endpointURL, { 543 | method: requestMethod, 544 | headers: { 545 | Authorization: `Bearer ${inputData.auth_token}`, 546 | }, 547 | }); 548 | break; 549 | case 'Bitbucket': 550 | response = await fetch(endpointURL.replace('', inputData.org), { 551 | method: requestMethod, 552 | headers: { 553 | Authorization: `Basic ${btoa(inputData.username + ':' + inputData.password)}`, 554 | }, 555 | }); 556 | break; 557 | case 'Jira': 558 | response = await fetch(endpointURL.replace(encodeURIComponent(''), inputData.app_domain), { 559 | method: requestMethod, 560 | headers: { 561 | Authorization: `Basic ${btoa(inputData.email + ':' + inputData.api_token)}`, 562 | Accept: 'application/json', 563 | }, 564 | }); 565 | break; 566 | case 'Pandadoc': 567 | response = await fetch(endpointURL, { 568 | method: requestMethod, 569 | headers: { 570 | Authorization: `API-Key ${inputData.api_key}`, 571 | }, 572 | }); 573 | break; 574 | case 'HubSpot': 575 | response = await fetch(endpointURL, { 576 | method: requestMethod, 577 | headers: { 578 | Authorization: `Bearer ${inputData.access_token}`, 579 | 'Content-Type': 'application/json' 580 | }, 581 | }); 582 | break; 583 | case 'AWS': 584 | response = await fetch(endpointURL.replace('', proxyURL), { 585 | method: requestMethod, 586 | headers: { 587 | 'Content-Type': 'application/json', 588 | }, 589 | body: JSON.stringify({ 590 | aws_access_key: inputData.access_key, 591 | aws_secret_key: inputData.secrets_access_key, 592 | region: inputData.region, 593 | }), 594 | }); 595 | break; 596 | case 'MongoDB': 597 | response = await fetch(endpointURL.replace('', proxyURL), { 598 | method: requestMethod, 599 | headers: { 600 | 'Content-Type': 'application/json', 601 | }, 602 | body: JSON.stringify({ 603 | mongodb_uri: inputData.mongodb_uri, 604 | database: inputData.database, 605 | collection: inputData.collection, 606 | }), 607 | }); 608 | break; 609 | case 'RabbitMQ': 610 | response = await fetch(endpointURL.replace('', proxyURL), { 611 | method: requestMethod, 612 | headers: { 613 | 'Content-Type': 'application/json', 614 | }, 615 | body: JSON.stringify({ 616 | connection_string: inputData.connection_string, 617 | queue_name: inputData.queue_name, 618 | }), 619 | }); 620 | break; 621 | case 'Postgres': 622 | response = await fetch(endpointURL.replace('', proxyURL), { 623 | method: requestMethod, 624 | headers: { 625 | 'Content-Type': 'application/json', 626 | }, 627 | body: JSON.stringify({ 628 | postgres_uri: inputData.connection_string, 629 | database: inputData.database, 630 | table: inputData.table, 631 | }), 632 | }); 633 | break; 634 | case 'Zendesk': 635 | response = await fetch(endpointURL.replace('', inputData.subdomain), { 636 | method: requestMethod, 637 | headers: { 638 | Authorization: `Basic ${btoa(inputData.email + '/token:' + inputData.api_token)}`, 639 | }, 640 | }); 641 | break; 642 | case 'GCP': 643 | response = await fetch(endpointURL.replace('', proxyURL), { 644 | method: requestMethod, 645 | headers: { 646 | 'Content-Type': 'application/json', 647 | }, 648 | body: JSON.stringify({ 649 | gcp_creds: inputData.gcp_creds 650 | }), 651 | }); 652 | break; 653 | case 'NVIDIA': 654 | response = await fetch(endpointURL, { 655 | method: 'POST', 656 | body: JSON.stringify({ 657 | proxied_data: { 658 | method: 'POST', 659 | headers: { 660 | "Content-Type": "application/x-www-form-urlencoded" 661 | } 662 | }, 663 | body: { 664 | "credentials": inputData.api_key, 665 | } 666 | }) 667 | }); 668 | break; 669 | case 'SonarCloud': 670 | response = await fetch(endpointURL, { 671 | method: requestMethod, 672 | headers: { 673 | Authorization: `Basic ${btoa(inputData.token + ':')}`, 674 | }, 675 | }); 676 | break; 677 | case 'Clerk': 678 | response = await fetch(endpointURL, { 679 | method: requestMethod, 680 | headers: { 681 | Authorization: `Bearer ${inputData.secret_key}`, 682 | }, 683 | }); 684 | break; 685 | case 'Okta': 686 | response = await fetch(endpointURL.replace('', inputData.your_okta_domain), { 687 | method: requestMethod, 688 | headers: { 689 | Authorization: `SSWS ${inputData.api_token}` 690 | } 691 | }); 692 | break; 693 | case 'CircleCI': 694 | response = await fetch(endpointURL, { 695 | method: requestMethod, 696 | headers: { 697 | 'Circle-Token': inputData.api_key, 698 | }, 699 | }); 700 | break; 701 | case 'WeightsAndBiases': 702 | response = await fetch(endpointURL, { 703 | method: 'POST', 704 | body: JSON.stringify({ 705 | proxied_data: { 706 | method: 'POST', 707 | headers: { 708 | Authorization: 'Basic ' + btoa(`api:${inputData.api_key}`), 709 | }, 710 | body: JSON.stringify({ 711 | query: "query Viewer { viewer { id username email admin } }", 712 | }), 713 | }, 714 | }), 715 | }); 716 | break; 717 | default: 718 | return { status: 400, data: { message: 'Unsupported service type' } }; 719 | } 720 | 721 | data = await response.json(); 722 | return { status: response.status, data }; 723 | } 724 | 725 | export default makeUniversalRequest; 726 | -------------------------------------------------------------------------------- /src/components/response_window.jsx: -------------------------------------------------------------------------------- 1 | import { Card, Button } from 'flowbite-react'; 2 | import { Tabs } from 'flowbite-react'; 3 | import { BsTable } from "react-icons/bs"; 4 | import { VscJson } from "react-icons/vsc"; 5 | import CopyButton from './copy_button'; 6 | import RawJsonTab from './response_window/json_view'; 7 | import JsonGridView from './response_window/json_grid_view'; 8 | import '../css/json_theme.css'; 9 | 10 | function OutputWindow({ status_code = 0, output_str = '{}' }) { 11 | if (status_code === 0) return null; 12 | 13 | let parsedData; 14 | try { 15 | parsedData = JSON.parse(output_str); 16 | } catch { 17 | parsedData = output_str; 18 | } 19 | 20 | return ( 21 | 22 |
23 |

Response

24 |
25 | 26 | 29 |
30 |
31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 |
41 | ); 42 | } 43 | 44 | export default OutputWindow; 45 | -------------------------------------------------------------------------------- /src/components/response_window/json_grid_view.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import JSONGrid from '@redheadphone/react-json-grid'; 3 | 4 | const lightTheme = { 5 | bgColor: 'transparent', 6 | keyColor: '#ff5e5e', 7 | stringColor: '#168f46', 8 | booleanColor: '#6699cc', 9 | numberColor: '#fdb082', 10 | objectColor: '#9ca3af', 11 | indexColor: '#9ca3af', 12 | borderColor: '#d1d5db', 13 | cellBorderColor: '#d1d5db', 14 | tableHeaderBgColor: '#f9fafb', 15 | tableIconColor: '#6b7280', 16 | selectHighlightBgColor: '#f3f4f6', 17 | }; 18 | 19 | const darkTheme = { 20 | bgColor: 'transparent', 21 | keyColor: '#ff5e5e', 22 | stringColor: '#168f46', 23 | booleanColor: '#6699cc', 24 | numberColor: '#fdb082', 25 | objectColor: '#9ca3af', 26 | indexColor: '#9ca3af', 27 | borderColor: '#4b5563', 28 | cellBorderColor: '#4b5563', 29 | tableHeaderBgColor: '#1f2937', 30 | tableIconColor: '#9ca3af', 31 | selectHighlightBgColor: '#374151', 32 | }; 33 | 34 | export default function JsonGridView({ parsedData }) { 35 | const [activeTheme, setActiveTheme] = useState(lightTheme); 36 | 37 | useEffect(() => { 38 | const updateTheme = () => { 39 | const isDarkMode = document.documentElement.classList.contains('dark'); 40 | setActiveTheme(isDarkMode ? darkTheme : lightTheme); 41 | }; 42 | 43 | updateTheme(); 44 | 45 | const observer = new MutationObserver(updateTheme); 46 | observer.observe(document.documentElement, { 47 | attributes: true, 48 | attributeFilter: ['class'], 49 | }); 50 | 51 | return () => { 52 | observer.disconnect(); 53 | }; 54 | }, []); 55 | 56 | return ( 57 |
61 | 66 |
67 | ); 68 | } -------------------------------------------------------------------------------- /src/components/response_window/json_view.jsx: -------------------------------------------------------------------------------- 1 | import JSONPretty from 'react-json-pretty'; 2 | 3 | function RawJsonTab({ parsedData }) { 4 | return ( 5 |
6 |
 7 |         
 8 |       
9 |
10 | ); 11 | } 12 | 13 | export default RawJsonTab; 14 | -------------------------------------------------------------------------------- /src/components/sidebar.jsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Sidebar } from 'flowbite-react'; 4 | import { LiaStripeS } from 'react-icons/lia'; 5 | import { RiOpenaiFill } from 'react-icons/ri'; 6 | import { HiOutlineRocketLaunch } from 'react-icons/hi2'; 7 | import { SlPaypal } from 'react-icons/sl'; 8 | import { FaGithub, FaShopify, FaTelegramPlane } from 'react-icons/fa'; 9 | import { CiCircleInfo } from 'react-icons/ci'; 10 | import { SiBrevo } from "react-icons/si"; 11 | import { FaTrello } from 'react-icons/fa'; 12 | import { IoMdArrowDroprightCircle } from 'react-icons/io'; 13 | import { SiRazorpay } from 'react-icons/si'; 14 | import { SiTwilio } from 'react-icons/si'; 15 | import { RiNpmjsLine } from 'react-icons/ri'; 16 | import { SiMailgun } from 'react-icons/si'; 17 | import { FaDigitalOcean } from 'react-icons/fa'; 18 | import { GiHoneycomb } from 'react-icons/gi'; 19 | import { SiEventbrite } from 'react-icons/si'; 20 | import { FaMailchimp } from 'react-icons/fa'; 21 | import { TbSquareLetterP } from 'react-icons/tb'; 22 | import { SiRavelry } from 'react-icons/si'; 23 | import { GrTextAlignFull } from 'react-icons/gr'; 24 | import { RiFlag2Line } from 'react-icons/ri'; 25 | import { TbCircleLetterP } from 'react-icons/tb'; 26 | import { IoLogoVercel } from 'react-icons/io5'; 27 | import { SiBitly } from 'react-icons/si'; 28 | import { SiAlgolia } from 'react-icons/si'; 29 | import { SiPosthog } from 'react-icons/si'; 30 | import { SiOpsgenie } from 'react-icons/si'; 31 | import { SiHelpscout } from 'react-icons/si'; 32 | import { SiTypeform } from 'react-icons/si'; 33 | import { SiNotion } from 'react-icons/si'; 34 | import { FaSlack } from 'react-icons/fa'; 35 | import { FaSquareGitlab } from 'react-icons/fa6'; 36 | import { SiPostman } from 'react-icons/si'; 37 | import { SiTerraform } from 'react-icons/si'; 38 | import { SiJfrog } from 'react-icons/si'; 39 | import { SiBuildkite } from 'react-icons/si'; 40 | import { SiPulumi } from 'react-icons/si'; 41 | import { SiSnyk } from 'react-icons/si'; 42 | import { CgSquare } from 'react-icons/cg'; 43 | import { SiSentry } from "react-icons/si"; 44 | import { FaBitbucket } from "react-icons/fa"; 45 | import { SiJira } from "react-icons/si"; 46 | import { SiHuggingface } from "react-icons/si"; 47 | import { SiSendgrid } from "react-icons/si"; 48 | import { FaHubspot } from "react-icons/fa"; 49 | import { FaAws } from "react-icons/fa"; 50 | import { SiMongodb } from "react-icons/si"; 51 | import { SiRabbitmq } from "react-icons/si"; 52 | import { BiLogoPostgresql } from "react-icons/bi"; 53 | import { SiZendesk } from "react-icons/si"; 54 | import { SiGooglecloud } from "react-icons/si"; 55 | import { BsNvidia } from "react-icons/bs"; 56 | import { SiSonar } from "react-icons/si"; 57 | import { SiClerk } from "react-icons/si"; 58 | import { FaXTwitter } from "react-icons/fa6"; 59 | import { SiOkta } from "react-icons/si"; 60 | import { SiCircleci } from "react-icons/si"; 61 | import { SiWeightsandbiases } from "react-icons/si"; 62 | import { useRef, useEffect } from 'react'; 63 | import { useMatch } from 'react-router-dom'; 64 | 65 | 66 | function SB({ visible, servicesConfig }) { 67 | // Accept visible as a prop 68 | if (!visible) return null; // Do not render if not visible 69 | 70 | let serviceIcons = { 71 | Stripe: LiaStripeS, 72 | Paypal: SlPaypal, 73 | OpenAI: RiOpenaiFill, 74 | LaunchDarkly: HiOutlineRocketLaunch, 75 | Github: FaGithub, 76 | Shopify: FaShopify, 77 | Telegram: FaTelegramPlane, 78 | SendInBlue: SiBrevo, 79 | Trello: FaTrello, 80 | RazorPay: SiRazorpay, 81 | Twilio: SiTwilio, 82 | NpmToken: RiNpmjsLine, 83 | Mailgun: SiMailgun, 84 | DigitalOcean: FaDigitalOcean, 85 | Honeycomb: GiHoneycomb, 86 | Eventbrite: SiEventbrite, 87 | MailChimp: FaMailchimp, 88 | Postmark: TbSquareLetterP, 89 | RechargePayments: SiRavelry, 90 | Paystack: GrTextAlignFull, 91 | Klaviyo: RiFlag2Line, 92 | Pipedrive: TbCircleLetterP, 93 | Vercel: IoLogoVercel, 94 | Bitly: SiBitly, 95 | Algolia: SiAlgolia, 96 | Posthog: SiPosthog, 97 | Opsgenie: SiOpsgenie, 98 | Helpscout: SiHelpscout, 99 | Typeform: SiTypeform, 100 | Notion: SiNotion, 101 | Slack: FaSlack, 102 | Gitlab: FaSquareGitlab, 103 | Postman: SiPostman, 104 | Terraform: SiTerraform, 105 | Jfrog: SiJfrog, 106 | Buildkite: SiBuildkite, 107 | Pulumi: SiPulumi, 108 | Snyk: SiSnyk, 109 | SquareAccessToken: CgSquare, 110 | Sentry: SiSentry, 111 | Bitbucket: FaBitbucket, 112 | Jira: SiJira, 113 | HuggingFace: SiHuggingface, 114 | SendGrid: SiSendgrid, 115 | HubSpot: FaHubspot, 116 | AWS: FaAws, 117 | MongoDB: SiMongodb, 118 | RabbitMQ: SiRabbitmq, 119 | Postgres: BiLogoPostgresql, 120 | Zendesk: SiZendesk, 121 | GCP: SiGooglecloud, 122 | NVIDIA: BsNvidia, 123 | SonarCloud: SiSonar, 124 | Clerk: SiClerk, 125 | Twitter: FaXTwitter, 126 | Okta: SiOkta, 127 | CircleCI: SiCircleci, 128 | WeightsAndBiases: SiWeightsandbiases 129 | }; 130 | 131 | return ( 132 | 133 | 134 | 135 |

136 | Modules 137 |

138 | {/* 139 | About 140 | */} 141 | {Object.keys(servicesConfig).map((service) => { 142 | const path = `/${service.toLowerCase()}`; 143 | const isActive = useMatch(path); 144 | const itemRef = useRef(null); 145 | 146 | useEffect(() => { 147 | if (isActive && itemRef.current) { 148 | itemRef.current.scrollIntoView({ block: 'center' }); 149 | } 150 | }, [isActive]); 151 | 152 | return ( 153 |
154 | 160 | {service} 161 | 162 |
163 | ); 164 | })} 165 | 166 |
167 |
168 |
169 | ); 170 | } 171 | 172 | export default SB; 173 | -------------------------------------------------------------------------------- /src/components/table.jsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { Table } from 'flowbite-react'; 4 | 5 | function MainPageTable({ servicesConfig }) { 6 | // Helper function to capitalize first letter and replace underscores 7 | const formatEndpointName = (name) => { 8 | return name 9 | .split('_') 10 | .map((part) => part.charAt(0).toUpperCase() + part.slice(1)) 11 | .join(' '); 12 | }; 13 | 14 | // Function to transform service config into rows for rendering 15 | const renderServiceRows = () => { 16 | return Object.entries(servicesConfig).map( 17 | ([serviceName, serviceDetails]) => ( 18 | 22 | 23 | 24 | {serviceName} 25 | 26 | 27 | 28 | {Object.keys(serviceDetails.endpoints) 29 | .map((endpointName) => formatEndpointName(endpointName)) 30 | .join(', ')} 31 | 32 | 33 | {serviceDetails.api_documentation_page ? ( 34 | 40 | Docs 41 | 42 | ) : ( 43 | 'No Documentation' 44 | )} 45 | 46 | 47 | ) 48 | ); 49 | }; 50 | 51 | return ( 52 |
53 | 54 | 55 | Service Name 56 | Endpoints 57 | Documentation 58 | 59 | {renderServiceRows()} 60 |
61 |
62 | ); 63 | } 64 | 65 | export default MainPageTable; 66 | -------------------------------------------------------------------------------- /src/css/json_theme.css: -------------------------------------------------------------------------------- 1 | .__json-pretty__ { 2 | line-height: 1.3; 3 | color: #9ca3af; 4 | overflow: auto; 5 | } 6 | 7 | .__json-pretty__ .__json-key__ { 8 | color: #ff5e5e; 9 | } 10 | 11 | .__json-pretty__ .__json-value__ { 12 | color: #fdb082; 13 | } 14 | 15 | .__json-pretty__ .__json-string__ { 16 | color: #168f46; 17 | } 18 | 19 | .__json-pretty__ .__json-boolean__ { 20 | color: #69c; 21 | } 22 | 23 | .__json-pretty-error__ { 24 | line-height: 1.3; 25 | color: #9ca3af; 26 | overflow: auto; 27 | } 28 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | /* Disable Flowbite/Tailwind focus rings globally */ 6 | [multiple]:focus, 7 | [type="date"]:focus, 8 | [type="datetime-local"]:focus, 9 | [type="email"]:focus, 10 | [type="month"]:focus, 11 | [type="number"]:focus, 12 | [type="password"]:focus, 13 | [type="search"]:focus, 14 | [type="tel"]:focus, 15 | [type="text"]:focus, 16 | [type="time"]:focus, 17 | [type="url"]:focus, 18 | [type="week"]:focus, 19 | select:focus, 20 | textarea:focus, 21 | button:focus, 22 | a:focus { 23 | --tw-ring-inset: initial !important; 24 | --tw-ring-offset-width: 0px !important; 25 | --tw-ring-offset-color: transparent !important; 26 | --tw-ring-color: transparent !important; 27 | --tw-ring-offset-shadow: none !important; 28 | --tw-ring-shadow: none !important; 29 | border-color: inherit !important; 30 | box-shadow: none !important; 31 | outline: none !important; 32 | outline-offset: 0 !important; 33 | } 34 | -------------------------------------------------------------------------------- /src/main.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom/client'; 3 | import App from './App.jsx'; 4 | import './index.css'; 5 | 6 | ReactDOM.createRoot(document.getElementById('root')).render( 7 | 8 | 9 | 10 | ); 11 | -------------------------------------------------------------------------------- /src/modules/universal.jsx: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { useHash } from 'react-use'; 3 | import { 4 | Button, 5 | Dropdown, 6 | Label, 7 | TextInput, 8 | Alert, 9 | Tooltip, 10 | Checkbox, 11 | } from 'flowbite-react'; 12 | import OutputWindow from '../components/response_window'; 13 | import RequestWindow from '../components/request_window'; 14 | import { HiInformationCircle } from 'react-icons/hi'; 15 | import makeUniversalRequest from '../components/requests'; 16 | import { AiOutlineLoading } from 'react-icons/ai'; 17 | 18 | export default function UniversalComponent({ serviceType, servicesConfig }) { 19 | const [hash, setHash] = useHash(); 20 | const serviceConfig = servicesConfig[serviceType] || {}; 21 | const apiDocumentationPage = serviceConfig.api_documentation_page; 22 | const endpoints = serviceConfig.endpoints || {}; 23 | const defaultInputFields = serviceConfig.input_fields || {}; 24 | 25 | const firstEndpointKey = Object.keys(endpoints)[0]; 26 | const firstEndpoint = endpoints[firstEndpointKey] || {}; 27 | 28 | const [selectedEndpoint, setSelectedEndpoint] = useState( 29 | firstEndpoint.label || 'Select Endpoint' 30 | ); 31 | const [curlCommand, setCurlCommand] = useState(firstEndpoint.curl || ''); 32 | const [requestURL, setRequestURL] = useState(firstEndpoint.request_url || ''); 33 | const [requestMethod, setRequestMethod] = useState( 34 | firstEndpoint.request_method || '' 35 | ); 36 | const [inputFields, setInputFields] = useState( 37 | firstEndpoint.override_default_input_field 38 | ? firstEndpoint.input_fields 39 | : defaultInputFields 40 | ); 41 | const [status_code, setStatusCode] = useState(0); 42 | const [output_str, setOutputStr] = useState(''); 43 | const [inputValues, setInputValues] = useState({}); 44 | const [loading, setLoading] = useState(false); 45 | const [isChecked, setIsChecked] = useState(false); 46 | const colorIsFailure = serviceConfig?.alert?.color === 'failure'; 47 | const enableButton = !colorIsFailure || (colorIsFailure && isChecked); 48 | 49 | const handleTestEndpoint = async () => { 50 | setLoading(true); 51 | try { 52 | const { status, data } = await makeUniversalRequest( 53 | isChecked, 54 | serviceType, 55 | inputValues, 56 | requestURL, 57 | requestMethod 58 | ); 59 | setStatusCode(status); 60 | setOutputStr(JSON.stringify(data, null, 2)); 61 | } catch (error) { 62 | console.error('Error:', error); 63 | } finally { 64 | setLoading(false); 65 | } 66 | }; 67 | 68 | const generateDynamicCurl = (curlCommandTemplate, inputs) => { 69 | let dynamicCurl = curlCommandTemplate; 70 | Object.keys(inputs).forEach((key) => { 71 | dynamicCurl = dynamicCurl.replace(`<${key}>`, inputs[key]); 72 | }); 73 | return dynamicCurl; 74 | }; 75 | 76 | const handleDropdownChange = (endpointKey, preserveHash = false) => { 77 | const endpointConfig = endpoints[endpointKey]; 78 | setSelectedEndpoint(endpointConfig.label); 79 | const updatedCurl = generateDynamicCurl(endpointConfig.curl, inputValues); 80 | setCurlCommand(updatedCurl); 81 | setRequestURL(endpointConfig.request_url); 82 | setRequestMethod(endpointConfig.request_method); 83 | 84 | if (endpointConfig.override_default_input_field) { 85 | setInputFields(endpointConfig.input_fields || {}); 86 | } else { 87 | setInputFields(defaultInputFields); 88 | } 89 | 90 | // Only update hash if we're not preserving it 91 | if (!preserveHash) { 92 | const updatedInputs = { ...inputValues, endpoint: endpointKey }; 93 | setHash(new URLSearchParams(updatedInputs).toString()); 94 | } 95 | }; 96 | 97 | const handleInputChange = (key, value) => { 98 | setInputValues((prev) => { 99 | const updatedInputs = { ...prev, [key]: value }; 100 | const updatedCurl = generateDynamicCurl(curlCommand, updatedInputs); 101 | setCurlCommand(updatedCurl); 102 | // Include the current endpoint in the hash 103 | const hashInputs = { ...updatedInputs, endpoint: Object.keys(endpoints).find(key => endpoints[key].label === selectedEndpoint) }; 104 | setHash(new URLSearchParams(hashInputs).toString()); 105 | return updatedInputs; 106 | }); 107 | }; 108 | 109 | const handleCheckboxChange = (e) => { 110 | setIsChecked(e.target.checked); 111 | }; 112 | 113 | useEffect(() => { 114 | // Only reset state variables when serviceType changes 115 | const newServiceConfig = servicesConfig[serviceType] || {}; 116 | const newEndpoints = newServiceConfig.endpoints || {}; 117 | const newDefaultInputFields = newServiceConfig.input_fields || {}; 118 | 119 | const firstNewEndpointKey = Object.keys(newEndpoints)[0]; 120 | const firstNewEndpoint = newEndpoints[firstNewEndpointKey] || {}; 121 | 122 | // Reset the selected endpoint and input fields only when serviceType changes 123 | setSelectedEndpoint(firstNewEndpoint.label || 'Select Endpoint'); 124 | setRequestURL(firstNewEndpoint.request_url || ''); 125 | setRequestMethod(firstNewEndpoint.request_method || ''); 126 | setInputFields( 127 | firstNewEndpoint.override_default_input_field 128 | ? firstNewEndpoint.input_fields 129 | : newDefaultInputFields 130 | ); 131 | 132 | // Reset status code when serviceType changes 133 | setStatusCode(0); 134 | }, [serviceType]); // Only depend on serviceType 135 | 136 | useEffect(() => { 137 | // Update the curl content dynamically based on inputValues but don't reset the endpoint 138 | const newServiceConfig = servicesConfig[serviceType] || {}; 139 | const newEndpoints = newServiceConfig.endpoints || {}; 140 | 141 | // Get the currently selected endpoint 142 | const selectedEndpointData = 143 | Object.values(newEndpoints).find( 144 | (endpoint) => endpoint.label === selectedEndpoint 145 | ) || {}; 146 | 147 | // Generate the updated cURL command based on the selected endpoint and current input values 148 | const updatedCurl = generateDynamicCurl( 149 | selectedEndpointData.curl, 150 | inputValues 151 | ); 152 | setCurlCommand(updatedCurl); 153 | }, [inputValues, selectedEndpoint, serviceType]); // Depend on inputValues, selectedEndpoint, and serviceType 154 | 155 | useEffect(() => { 156 | if (hash) { 157 | try { 158 | const hashString = hash.substring(1); // Remove the # character 159 | if (!hashString) return; 160 | const hashParamsUrl = new URLSearchParams(hashString); 161 | var _inputFields = inputFields; 162 | // First handle the endpoint if it exists 163 | const endpointKey = hashParamsUrl.get('endpoint'); 164 | if (endpointKey && endpoints[endpointKey]) { 165 | handleDropdownChange(endpointKey, true); // Pass true to preserve hash 166 | _inputFields = endpoints[endpointKey].input_fields ?? inputFields; 167 | } 168 | 169 | // Then handle the input fields 170 | Object.keys(_inputFields).forEach((key) => { 171 | const value = hashParamsUrl.get(key); 172 | if (value) { 173 | setInputValues((prev) => ({ ...prev, [key]: value })); 174 | } 175 | }); 176 | } catch (error) { 177 | console.error('Error parsing hash:', error); 178 | } 179 | } 180 | }, [hash]); 181 | 182 | return ( 183 |
184 | {apiDocumentationPage ? ( 185 | 👈🏻 Click to View Official API Documentation} 187 | placement="right" 188 | > 189 | 195 | Check {serviceType} Keys 196 | 197 | 198 | ) : ( 199 |

200 | Check {serviceType} Keys 201 |

202 | )} 203 |
204 | {Object.keys(inputFields).map((key) => ( 205 |
206 |
207 |
209 | handleInputChange(key, e.target.value)} 215 | required 216 | /> 217 |
218 | ))} 219 | 220 | {serviceConfig.alert && ( 221 | 222 | Alert!{' '} 223 | {serviceConfig.alert.alert_message} 224 | 225 | )} 226 | 227 |
228 | {serviceConfig?.alert?.color === 'failure' && ( 229 |
230 | 236 | 239 |
240 | )} 241 | 242 |
269 |
270 | 275 |
276 | 277 |
278 |
279 | ); 280 | } 281 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | export default { 3 | content: [ 4 | './src/**/*.{js,jsx,ts,tsx}', 5 | 'node_modules/flowbite-react/lib/esm/**/*.js', 6 | ], 7 | theme: { 8 | extend: {}, 9 | }, 10 | plugins: [require('flowbite/plugin')], 11 | }; 12 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import react from '@vitejs/plugin-react'; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [react()], 7 | }); 8 | --------------------------------------------------------------------------------