├── CONTRIBUTING.md ├── LICENSE.md ├── README.md ├── keyflow-api-with-convex ├── .gitignore ├── package.json ├── src │ ├── config │ │ ├── ErrorHandlingFetch.ts │ │ ├── convex.ts │ │ ├── key-generator.ts │ │ └── schema-validation.ts │ ├── index.ts │ ├── lib │ │ └── ratelimit.ts │ ├── routes │ │ ├── create.ts │ │ └── verify.ts │ └── types │ │ └── api.ts ├── tsconfig.json └── wrangler.example.toml ├── keyflow-api ├── .gitignore ├── package.json ├── src │ ├── config │ │ ├── generateApiKey.ts │ │ └── schema-validation.ts │ ├── index.ts │ ├── lib │ │ └── ratelimit.ts │ ├── routes │ │ ├── create.ts │ │ └── verify.ts │ └── types │ │ └── api.ts └── wrangler.example.toml └── www ├── .gitignore ├── bun.lockb ├── components.json ├── convex ├── _generated │ ├── api.d.ts │ ├── api.js │ ├── dataModel.d.ts │ ├── server.d.ts │ └── server.js └── apiRequests.ts ├── next.config.ts ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── public ├── apple-touch-icon.png ├── favicon-48x48.png ├── favicon.ico ├── favicon.svg ├── site.webmanifest ├── web-app-manifest-192x192.png └── web-app-manifest-512x512.png ├── sentry.client.config.ts ├── sentry.edge.config.ts ├── sentry.server.config.ts ├── src ├── actions │ ├── create-apiKey.ts │ └── verify-apiKey.ts ├── app │ ├── _components │ │ ├── create-apiKey.tsx │ │ ├── real-time-logs │ │ │ ├── columns.tsx │ │ │ ├── data-table.tsx │ │ │ └── dialog.tsx │ │ ├── status-text.tsx │ │ └── verify-apiKey.tsx │ ├── global-error.tsx │ ├── layout.tsx │ ├── logs │ │ └── page.tsx │ ├── manifest.ts │ ├── page.tsx │ ├── robots.ts │ └── sitemap.ts ├── components │ ├── core │ │ └── animated-background.tsx │ ├── footer.tsx │ ├── github-button.tsx │ ├── main-nav.tsx │ ├── mobile-nav.tsx │ ├── skeletons │ │ └── table-skeleton.tsx │ ├── theme-toggle.tsx │ └── ui │ │ ├── alert.tsx │ │ ├── badge.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── dialog.tsx │ │ ├── dropdown-menu.tsx │ │ ├── flickering-grid.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── separator.tsx │ │ ├── skeleton.tsx │ │ ├── sonner.tsx │ │ ├── switch.tsx │ │ ├── table.tsx │ │ ├── tabs.tsx │ │ └── textarea.tsx ├── config │ ├── custom-data.ts │ ├── dateFormatter.ts │ ├── safe-action.ts │ ├── utils.ts │ ├── validateJson.ts │ └── zod.ts ├── instrumentation.ts ├── providers │ ├── ConvexClientProvider.tsx │ └── theme-provider.tsx ├── styles │ ├── CalSans-SemiBold.ttf │ ├── fonts.ts │ └── globals.css └── types │ └── action-types.ts ├── tailwind.config.ts └── tsconfig.json /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to KeyFlow 2 | 3 | We love your input! We want to make contributing to this project as easy and transparent as possible, whether it's: 4 | 5 | - Reporting a bug 6 | - Discussing the current state of the code 7 | - Submitting a fix 8 | - Proposing new features 9 | - Becoming a maintainer 10 | 11 | ## We Develop with GitHub 12 | 13 | We use GitHub to host code, to track issues and feature requests, as well as accept pull requests. 14 | 15 | ## We Use [Github Flow](https://guides.github.com/introduction/flow/index.html) 16 | 17 | Pull requests are the best way to propose changes to the codebase. We actively welcome your pull requests: 18 | 19 | 1. Fork the repo and create your branch from `main`. 20 | 2. If you've added code that should be tested, add tests. 21 | 3. If you've changed APIs, update the documentation. 22 | 4. Ensure the test suite passes. 23 | 5. Make sure your code lints. 24 | 6. Issue that pull request! 25 | 26 | ## Any contributions you make will be under the MIT Software License 27 | 28 | In short, when you submit code changes, your submissions are understood to be under the same [MIT License](http://choosealicense.com/licenses/mit/) that covers the project. Feel free to contact the maintainers if that's a concern. 29 | 30 | ## Report bugs using GitHub's [issues](https://github.com/evansso-bit/keyflow/issues) 31 | 32 | We use GitHub issues to track public bugs. Report a bug by [opening a new issue](https://github.com/evansso-bit/keyflow/issues/new); it's that easy! 33 | 34 | ## Write bug reports with detail, background, and sample code 35 | 36 | **Great Bug Reports** tend to have: 37 | 38 | - A quick summary and/or background 39 | - Steps to reproduce 40 | - Be specific! 41 | - Give sample code if you can. 42 | - What you expected would happen 43 | - What actually happens 44 | - Notes (possibly including why you think this might be happening, or stuff you tried that didn't work) 45 | 46 | ## Use a Consistent Coding Style 47 | 48 | * 2 spaces for indentation rather than tabs 49 | * You can try running `npm run lint` for style unification 50 | 51 | ## License 52 | 53 | By contributing, you agree that your contributions will be licensed under its MIT License. 54 | 55 | ## References 56 | 57 | This document was adapted from the open-source contribution guidelines for [Facebook's Draft](https://github.com/facebook/draft-js/blob/master/CONTRIBUTING.md) -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 MpesaFlow 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # KeyFlow: Open-Source API Key Generator 2 | 3 | KeyFlow is an open-source API key generation built with Next.js and Convex. It provides a simple and efficient way to create, verify, and manage API keys for your applications. 4 | 5 | ## Features 6 | 7 | - Create and verify API keys 8 | - Real-time logs of API requests 9 | - Built with Next.js and Convex 10 | - Easy to integrate and customize 11 | - Rate limiting support 12 | - Customizable API key prefixes and expiration 13 | 14 | ## Tech Stack 15 | 16 | - Next.js 17 | - Convex 18 | - Cloudflare Workers 19 | - Hono 20 | - Upstash Redis 21 | - Tailwind CSS 22 | - TypeScript 23 | 24 | ## Getting Started 25 | 26 | 1. Clone the repository: 27 | ``` 28 | git clone https://github.com/evansso-bit/keyflow.git 29 | ``` 30 | 31 | 2. Install dependencies: 32 | ``` 33 | cd keyflow/www 34 | npm install 35 | ``` 36 | 37 | 3. Set up environment variables: 38 | Copy the `.env.example` file to `.env.local` and fill in the required values. 39 | 40 | 4. Run the development server: 41 | ``` 42 | npm run dev 43 | ``` 44 | 45 | 5. Open [http://localhost:3000](http://localhost:3000) in your browser to see the application. 46 | 47 | ## Project Structure 48 | 49 | - `www/`: Next.js frontend application 50 | - `keyflow-api/`: API server built with Hono and Cloudflare Workers 51 | - `keyflow-api-with-convex/`: API server with Convex integration 52 | 53 | ## Deployment 54 | 55 | The project is set up for deployment on Vercel and Cloudflare Workers. Refer to the deployment documentation for each platform for detailed instructions. 56 | 57 | ## Contributing 58 | 59 | We welcome contributions! Please see our [Contributing Guide](CONTRIBUTING.md) for more details. 60 | 61 | ## License 62 | 63 | This project is licensed under the MIT License. See the [LICENSE](LICENSE.md) file for details. 64 | 65 | ## Links 66 | 67 | - [Live Demo](https://keyflow.mpesaflow.com/) 68 | - [GitHub Repository](https://github.com/evansso-bit/keyflow) 69 | 70 | ## Acknowledgements 71 | 72 | Built with ❤️ by the MpesaFlow team. -------------------------------------------------------------------------------- /keyflow-api-with-convex/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /wrangler 3 | /wrangler.toml 4 | /bun.lockb 5 | -------------------------------------------------------------------------------- /keyflow-api-with-convex/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mpesaflow-api-key-engine", 3 | "module": "src/index.ts", 4 | "type": "module", 5 | "devDependencies": { 6 | "@types/bun": "latest" 7 | }, 8 | "scripts": { 9 | "start": "bun run src/index.ts", 10 | "dev": "bun run src/index.ts --watch", 11 | "deploy": "wrangler deploy --minify src/index.ts" 12 | }, 13 | "peerDependencies": { 14 | "typescript": "^5.0.0" 15 | }, 16 | "dependencies": { 17 | "@hono/zod-validator": "^0.4.1", 18 | "@upstash/ratelimit": "^2.0.4", 19 | "@upstash/redis": "^1.34.3", 20 | "hono": "^4.6.5", 21 | "wrangler": "^3.81.0", 22 | "zod": "^3.23.8" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /keyflow-api-with-convex/src/config/ErrorHandlingFetch.ts: -------------------------------------------------------------------------------- 1 | export async function fetchWithErrorHandling( 2 | url: string, 3 | options: RequestInit 4 | ) { 5 | const { credentials, ...safeOptions } = options; 6 | 7 | const response = await fetch(url, safeOptions); 8 | const responseBody = await response.text(); 9 | 10 | // biome-ignore lint/suspicious/noImplicitAnyLet: 11 | let parsedBody; 12 | try { 13 | parsedBody = JSON.parse(responseBody); 14 | } catch (e) { 15 | console.error("Failed to parse response as JSON:", responseBody); 16 | throw new Error("Invalid JSON response from server"); 17 | } 18 | 19 | if (!response.ok) { 20 | console.error(`HTTP error! status: ${response.status}, body:`, parsedBody); 21 | 22 | // Check for the specific "transaction is being processed" error 23 | if ( 24 | parsedBody.errorCode === "500.001.1001" && 25 | parsedBody.errorMessage === "The transaction is being processed" 26 | ) { 27 | return { 28 | status: "pending", 29 | message: "The transaction is being processed", 30 | ...parsedBody, 31 | }; 32 | } 33 | 34 | throw new Error( 35 | `HTTP error! status: ${response.status}, body: ${JSON.stringify( 36 | parsedBody 37 | )}` 38 | ); 39 | } 40 | 41 | return parsedBody; 42 | } 43 | -------------------------------------------------------------------------------- /keyflow-api-with-convex/src/config/convex.ts: -------------------------------------------------------------------------------- 1 | import { fetchWithErrorHandling } from "./ErrorHandlingFetch"; 2 | 3 | // biome-ignore lint/suspicious/noExplicitAny: 4 | export async function convexMutation(url: string, path: string, args: any) { 5 | const result = await fetchWithErrorHandling(`${url}/api/mutation`, { 6 | method: "POST", 7 | headers: { 8 | "Content-Type": "application/json", 9 | }, 10 | body: JSON.stringify({ 11 | path, 12 | args, 13 | format: "json", 14 | }), 15 | }); 16 | 17 | if (result.status === "error") { 18 | throw new Error(`Convex mutation error: ${result.errorMessage}`); 19 | } 20 | return result.value; 21 | } 22 | 23 | // biome-ignore lint/suspicious/noExplicitAny: 24 | export async function convexQuery(url: string, path: string, args: any) { 25 | const result = await fetchWithErrorHandling(`${url}/api/query`, { 26 | method: "POST", 27 | headers: { 28 | "Content-Type": "application/json", 29 | }, 30 | body: JSON.stringify({ 31 | path, 32 | args, 33 | format: "json", 34 | }), 35 | }); 36 | 37 | if (result.status === "error") { 38 | throw new Error(`Convex query error: ${result.errorMessage}`); 39 | } 40 | return result.value; 41 | } 42 | -------------------------------------------------------------------------------- /keyflow-api-with-convex/src/config/key-generator.ts: -------------------------------------------------------------------------------- 1 | // Helper function to generate API keys 2 | export function generateApiKey( 3 | prefix: string | undefined, 4 | byteLength: number 5 | ): string { 6 | const randomBytes = crypto.getRandomValues(new Uint8Array(byteLength)); 7 | const key = btoa(String.fromCharCode(...new Uint8Array(randomBytes))) 8 | .replace(/\+/g, "-") 9 | .replace(/\//g, "_") 10 | .replace(/=/g, ""); 11 | return prefix ? `${prefix}_${key}` : key; 12 | } 13 | -------------------------------------------------------------------------------- /keyflow-api-with-convex/src/config/schema-validation.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | // Schema for validating the request body 4 | export const createApiKeySchema = z.object({ 5 | apiId: z.string().optional(), 6 | prefix: z.string().optional(), 7 | byteLength: z.number().int().min(16).max(32).optional(), 8 | ownerId: z.string().optional(), 9 | name: z.string(), 10 | meta: z 11 | .object({ 12 | plan: z.string().optional(), 13 | createdBy: z.string().optional(), 14 | }) 15 | .optional(), 16 | expires: z.number().optional(), 17 | ratelimit: z 18 | .object({ 19 | type: z.enum(["consistent", "fast"]).optional(), 20 | limit: z.number().int().positive().optional(), 21 | refillRate: z.number().int().positive().optional(), 22 | refillInterval: z.number().int().positive().optional(), 23 | }) 24 | .optional(), 25 | remaining: z.number().int().nonnegative().optional(), 26 | refill: z 27 | .object({ 28 | amount: z.number().int().positive().optional(), 29 | interval: z.enum(["daily", "monthly"]).optional(), 30 | }) 31 | .optional(), 32 | enabled: z.boolean().optional(), 33 | }); 34 | 35 | // Schema for validating the request body 36 | export const verifyApiKeySchema = z.object({ 37 | apiKey: z.string(), 38 | }); 39 | -------------------------------------------------------------------------------- /keyflow-api-with-convex/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import type { Env } from "./types/api"; 3 | import create from "./routes/create"; 4 | import verify from "./routes/verify"; 5 | import { rateLimitMiddleware } from "./lib/ratelimit"; 6 | 7 | const app = new Hono<{ 8 | Bindings: Env; 9 | }>().basePath("/keys"); 10 | 11 | app.use("*", rateLimitMiddleware); 12 | 13 | app.route("/", create); 14 | app.route("/", verify); 15 | 16 | export default app; 17 | -------------------------------------------------------------------------------- /keyflow-api-with-convex/src/lib/ratelimit.ts: -------------------------------------------------------------------------------- 1 | import { Ratelimit } from "@upstash/ratelimit"; 2 | import { Redis } from "@upstash/redis/cloudflare"; 3 | import { env } from "hono/adapter"; 4 | import type { Context, Next } from "hono"; 5 | import type { Bindings } from "../types/api"; 6 | 7 | // Middleware for rate limiting 8 | export async function rateLimitMiddleware(c: Context, next: Next) { 9 | const { UPSTASH_REDIS_REST_TOKEN, UPSTASH_REDIS_REST_URL } = env(c); 10 | 11 | const ratelimit = new Ratelimit({ 12 | redis: new Redis({ 13 | url: UPSTASH_REDIS_REST_URL, 14 | token: UPSTASH_REDIS_REST_TOKEN, 15 | }), 16 | limiter: Ratelimit.slidingWindow(5, "30 s"), 17 | analytics: true, 18 | }); 19 | 20 | const ip = c.req.header("CF-Connecting-IP") || "127.0.0.1"; 21 | const { success, limit, remaining, reset } = await ratelimit.limit(ip); 22 | 23 | if (!success) { 24 | return c.json({ error: "Rate limit exceeded" }, 429); 25 | } 26 | 27 | c.header("X-RateLimit-Limit", limit.toString()); 28 | c.header("X-RateLimit-Remaining", remaining.toString()); 29 | c.header("X-RateLimit-Reset", reset.toString()); 30 | 31 | await next(); 32 | } 33 | -------------------------------------------------------------------------------- /keyflow-api-with-convex/src/routes/create.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import type { 3 | CreateKeyRequest, 4 | CreateKeyResponse, 5 | Bindings, 6 | } from "../types/api"; 7 | import { Redis } from "@upstash/redis/cloudflare"; 8 | import { convexMutation } from "../config/convex"; 9 | import { generateApiKey } from "../config/key-generator"; 10 | import { zValidator } from "@hono/zod-validator"; 11 | import { createApiKeySchema } from "../config/schema-validation"; 12 | 13 | const create = new Hono<{ 14 | Bindings: Bindings; 15 | }>(); 16 | 17 | // Create API Key endpoint with proper JSON stringification 18 | create.post( 19 | "/create", 20 | zValidator("json", createApiKeySchema, (result, c) => { 21 | if (!result.success) { 22 | return c.text("Invalid!", 400); 23 | } 24 | }), 25 | async (c) => { 26 | const { UPSTASH_REDIS_REST_TOKEN, UPSTASH_REDIS_REST_URL, CONVEX_URL } = 27 | c.env; 28 | 29 | const redis = new Redis({ 30 | url: UPSTASH_REDIS_REST_URL, 31 | token: UPSTASH_REDIS_REST_TOKEN, 32 | }); 33 | const body = await c.req.json(); 34 | 35 | const keyId = crypto.randomUUID(); 36 | const key = generateApiKey(body.prefix, body.byteLength || 16); 37 | 38 | const keyData = { 39 | ...body, 40 | key, 41 | keyId, 42 | createdAt: Date.now(), 43 | }; 44 | 45 | // Make sure to stringify the keyData before storing 46 | const encodedKey = encodeURIComponent(key); 47 | 48 | try { 49 | await redis.set(`key:${keyId}`, JSON.stringify(keyData)); 50 | await redis.set(`lookup:${encodedKey}`, keyId); 51 | 52 | // Save the request in the database through the workflow 53 | await convexMutation(CONVEX_URL, "apiRequests:create", { 54 | method: "POST", 55 | url: "/keys/create", 56 | status_code: 200, 57 | request_body: { 58 | ...body, 59 | }, 60 | result_body: { 61 | key: key, 62 | keyId: keyId, 63 | }, 64 | }); 65 | 66 | return c.json({ key, keyId }); 67 | } catch (error) { 68 | await convexMutation(CONVEX_URL, "apiRequests:create", { 69 | method: "POST", 70 | url: "/keys/create", 71 | status_code: 500, 72 | request_body: { 73 | ...body, 74 | }, 75 | result_body: { 76 | error: error instanceof Error ? error.message : "Unknown error", 77 | }, 78 | }); 79 | 80 | console.error("Error in /keys/create:", error); 81 | return c.json({ error: "Internal Server Error" }, 500); 82 | } 83 | } 84 | ); 85 | 86 | export default create; 87 | -------------------------------------------------------------------------------- /keyflow-api-with-convex/src/routes/verify.ts: -------------------------------------------------------------------------------- 1 | import { Redis } from "@upstash/redis/cloudflare"; 2 | import type { 3 | VerifyKeyRequest, 4 | VerifyKeyResponse, 5 | Bindings, 6 | CreateKeyRequest, 7 | } from "../types/api"; 8 | import { Hono } from "hono"; 9 | import { convexMutation } from "../config/convex"; 10 | import { verifyApiKeySchema } from "../config/schema-validation"; 11 | import { zValidator } from "@hono/zod-validator"; 12 | 13 | const verify = new Hono<{ 14 | Bindings: Bindings; 15 | }>(); 16 | 17 | verify.post( 18 | "/verify", 19 | zValidator("json", verifyApiKeySchema, (result, c) => { 20 | if (!result.success) { 21 | return c.text("Invalid!", 400); 22 | } 23 | }), 24 | async (c) => { 25 | const { UPSTASH_REDIS_REST_TOKEN, UPSTASH_REDIS_REST_URL, CONVEX_URL } = 26 | c.env; 27 | const redis = new Redis({ 28 | url: UPSTASH_REDIS_REST_URL, 29 | token: UPSTASH_REDIS_REST_TOKEN, 30 | }); 31 | const body = await c.req.json(); 32 | try { 33 | console.log("Parsed request body:", body); 34 | 35 | if (!body.key) { 36 | return c.json({ error: "key is required" }, 400); 37 | } 38 | 39 | const encodedKey = encodeURIComponent(body.key); 40 | const keyId = await redis.get(`lookup:${encodedKey}`); 41 | 42 | console.log("Key ID:", keyId); 43 | 44 | // Save the request in the database through the workflow 45 | await convexMutation(CONVEX_URL, "apiRequests:create", { 46 | method: "POST", 47 | url: "/keys/verify", 48 | status_code: 200, 49 | request_body: { 50 | ...body, 51 | }, 52 | result_body: { 53 | // biome-ignore lint/complexity/noUselessTernary: 54 | valid: keyId ? true : false, 55 | }, 56 | }); 57 | 58 | if (keyId) { 59 | return c.json({ valid: true }); 60 | // biome-ignore lint/style/noUselessElse: 61 | } else if (!keyId) { 62 | return c.json({ valid: false }); 63 | } 64 | 65 | // Get the raw string data from Redis 66 | const keyDataString = await redis.get(`key:${keyId}`); 67 | console.log("Raw key data from Redis:", keyDataString); 68 | 69 | if (!keyDataString || typeof keyDataString !== "string") { 70 | return c.json({ valid: false }); 71 | } 72 | 73 | // Parse the string data 74 | let keyData: CreateKeyRequest & { 75 | key: string; 76 | keyId: string; 77 | createdAt: number; 78 | }; 79 | 80 | try { 81 | // Handle case where Redis might return an object instead of a string 82 | if (typeof keyDataString === "object") { 83 | // biome-ignore lint/suspicious/noExplicitAny: 84 | keyData = keyDataString as any; 85 | } else { 86 | keyData = JSON.parse(keyDataString); 87 | } 88 | } catch (parseError) { 89 | console.error("Key data parse error:", parseError); 90 | // If the data in Redis is corrupt, clean it up 91 | await Promise.all([ 92 | redis.del(`key:${keyId}`), 93 | redis.del(`lookup:${encodedKey}`), 94 | ]); 95 | return c.json( 96 | { 97 | error: "Invalid key data in storage", 98 | details: 99 | parseError instanceof Error 100 | ? parseError.message 101 | : "Unknown parse error", 102 | valid: false, 103 | }, 104 | 500 105 | ); 106 | } 107 | 108 | if (keyData.expires && keyData.expires < Date.now()) { 109 | await Promise.all([ 110 | redis.del(`key:${keyId}`), 111 | redis.del(`lookup:${encodedKey}`), 112 | ]); 113 | return c.json({ valid: false }); 114 | } 115 | 116 | const response: VerifyKeyResponse = { 117 | valid: true, 118 | ownerId: keyData.ownerId, 119 | meta: keyData.meta, 120 | expires: keyData.expires, 121 | }; 122 | 123 | if (keyData.ratelimit) { 124 | response.ratelimit = { 125 | limit: keyData.ratelimit.limit, 126 | remaining: keyData.ratelimit.limit, 127 | reset: Date.now() + keyData.ratelimit.refillInterval, 128 | }; 129 | } 130 | 131 | return c.json(response); 132 | } catch (error) { 133 | console.error("Error in /keys/verify:", error); 134 | await convexMutation(CONVEX_URL, "apiRequests:create", { 135 | method: "POST", 136 | url: "/keys/verify", 137 | status_code: 500, 138 | request_body: { 139 | ...body, 140 | }, 141 | result_body: { 142 | error: error instanceof Error ? error.message : "Unknown error", 143 | }, 144 | }); 145 | 146 | return c.json( 147 | { 148 | error: "Internal Server Error", 149 | details: error instanceof Error ? error.message : "Unknown error", 150 | valid: false, 151 | }, 152 | 500 153 | ); 154 | } 155 | } 156 | ); 157 | 158 | export default verify; 159 | -------------------------------------------------------------------------------- /keyflow-api-with-convex/src/types/api.ts: -------------------------------------------------------------------------------- 1 | export type Bindings = { 2 | UPSTASH_REDIS_REST_URL: string; 3 | UPSTASH_REDIS_REST_TOKEN: string; 4 | CONVEX_URL: string; 5 | ENVIRONMENT: "development" | "production"; 6 | WORKFLOW_BASE_URL: string; 7 | }; 8 | 9 | // Types for our API key schema 10 | export type CreateKeyRequest = { 11 | apiId: string; 12 | prefix?: string; 13 | byteLength?: number; 14 | ownerId?: string; 15 | name?: string; 16 | meta?: Record; 17 | expires?: number; 18 | ratelimit?: { 19 | type: "fast" | "consistent"; 20 | limit: number; 21 | refillRate: number; 22 | refillInterval: number; 23 | }; 24 | remaining?: number; 25 | refill?: { 26 | amount: number; 27 | interval: "daily" | "monthly"; 28 | }; 29 | enabled?: boolean; 30 | }; 31 | 32 | export type CreateKeyResponse = { 33 | key: string; 34 | keyId: string; 35 | }; 36 | 37 | export type VerifyKeyRequest = { 38 | key: string; 39 | }; 40 | 41 | export type VerifyKeyResponse = { 42 | valid: boolean; 43 | ownerId?: string; 44 | meta?: Record; 45 | expires?: number; 46 | ratelimit?: { 47 | limit: number; 48 | remaining: number; 49 | reset: number; 50 | }; 51 | }; 52 | -------------------------------------------------------------------------------- /keyflow-api-with-convex/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | // Enable latest features 4 | "lib": ["ESNext", "DOM"], 5 | "target": "ESNext", 6 | "module": "ESNext", 7 | "moduleDetection": "force", 8 | "jsx": "react-jsx", 9 | "allowJs": true, 10 | 11 | // Bundler mode 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "verbatimModuleSyntax": true, 15 | "noEmit": true, 16 | 17 | // Best practices 18 | "strict": true, 19 | "skipLibCheck": true, 20 | "noFallthroughCasesInSwitch": true, 21 | 22 | // Some stricter flags (disabled by default) 23 | "noUnusedLocals": false, 24 | "noUnusedParameters": false, 25 | "noPropertyAccessFromIndexSignature": false 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /keyflow-api-with-convex/wrangler.example.toml: -------------------------------------------------------------------------------- 1 | name = "" 2 | main = "src/index.ts" 3 | compatibility_date = "2023-05-18" 4 | 5 | [vars] 6 | 7 | UPSTASH_REDIS_REST_URL = "" 8 | UPSTASH_REDIS_REST_TOKEN = "" 9 | WORKFLOW_BASE_URL = "" 10 | ENVIRONMENT = "production" 11 | CONVEX_URL = "" 12 | 13 | 14 | routes = [ 15 | { pattern = "", custom_domain = true } 16 | ] 17 | 18 | [observability] 19 | enabled = true -------------------------------------------------------------------------------- /keyflow-api/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .wrangler 3 | wrangler.toml 4 | bun.lockb 5 | -------------------------------------------------------------------------------- /keyflow-api/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "mpesaflow-api-key-engine", 3 | "module": "src/index.ts", 4 | "type": "module", 5 | "devDependencies": { 6 | "@biomejs/biome": "1.9.4", 7 | "@types/bun": "latest" 8 | }, 9 | "scripts": { 10 | "start": "bun run src/index.ts", 11 | "dev": "bun run src/index.ts --watch", 12 | "deploy": "wrangler deploy --minify src/index.ts" 13 | }, 14 | "peerDependencies": { 15 | "typescript": "^5.0.0" 16 | }, 17 | "dependencies": { 18 | "@hono/zod-validator": "^0.4.1", 19 | "@upstash/ratelimit": "^2.0.4", 20 | "@upstash/redis": "^1.34.3", 21 | "hono": "^4.6.5", 22 | "wrangler": "^3.81.0", 23 | "zod": "^3.23.8" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /keyflow-api/src/config/generateApiKey.ts: -------------------------------------------------------------------------------- 1 | // Helper function to generate API keys 2 | export function generateApiKey( 3 | prefix: string | undefined, 4 | byteLength: number, 5 | ): string { 6 | const randomBytes = crypto.getRandomValues(new Uint8Array(byteLength)); 7 | const key = btoa(String.fromCharCode(...new Uint8Array(randomBytes))) 8 | .replace(/\+/g, "-") 9 | .replace(/\//g, "_") 10 | .replace(/=/g, ""); 11 | return prefix ? `${prefix}_${key}` : key; 12 | } 13 | -------------------------------------------------------------------------------- /keyflow-api/src/config/schema-validation.ts: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | // Schema for validating the request body 4 | export const createApiKeySchema = z.object({ 5 | apiId: z.string().optional(), 6 | prefix: z.string().optional(), 7 | byteLength: z.number().int().min(16).max(32).optional(), 8 | ownerId: z.string().optional(), 9 | name: z.string(), 10 | meta: z 11 | .object({ 12 | plan: z.string().optional(), 13 | createdBy: z.string().optional(), 14 | }) 15 | .optional(), 16 | expires: z.number().optional(), 17 | ratelimit: z 18 | .object({ 19 | type: z.enum(["consistent", "fast"]).optional(), 20 | limit: z.number().int().positive().optional(), 21 | refillRate: z.number().int().positive().optional(), 22 | refillInterval: z.number().int().positive().optional(), 23 | }) 24 | .optional(), 25 | remaining: z.number().int().nonnegative().optional(), 26 | refill: z 27 | .object({ 28 | amount: z.number().int().positive().optional(), 29 | interval: z.enum(["daily", "monthly"]).optional(), 30 | }) 31 | .optional(), 32 | enabled: z.boolean().optional(), 33 | }); 34 | 35 | // Schema for validating the request body 36 | export const verifyApiKeySchema = z.object({ 37 | apiKey: z.string(), 38 | }); 39 | -------------------------------------------------------------------------------- /keyflow-api/src/index.ts: -------------------------------------------------------------------------------- 1 | import { Hono } from "hono"; 2 | import { rateLimitMiddleware } from "./lib/ratelimit"; 3 | import create from "./routes/create"; 4 | import verify from "./routes/verify"; 5 | import type { Bindings } from "./types/api"; 6 | 7 | const app = new Hono<{ 8 | Bindings: Bindings; 9 | }>().basePath("/keys"); 10 | 11 | app.use("*", rateLimitMiddleware); 12 | 13 | app.route("/", create); 14 | app.route("/", verify); 15 | 16 | export default app; 17 | -------------------------------------------------------------------------------- /keyflow-api/src/lib/ratelimit.ts: -------------------------------------------------------------------------------- 1 | import { Ratelimit } from "@upstash/ratelimit"; 2 | import { Redis } from "@upstash/redis/cloudflare"; 3 | import { env } from "hono/adapter"; 4 | import type { Context, Next } from "hono"; 5 | import type { Bindings } from "../types/api"; 6 | 7 | // Middleware for rate limiting 8 | export async function rateLimitMiddleware(c: Context, next: Next) { 9 | const { UPSTASH_REDIS_REST_TOKEN, UPSTASH_REDIS_REST_URL } = env(c); 10 | 11 | const ratelimit = new Ratelimit({ 12 | redis: new Redis({ 13 | url: UPSTASH_REDIS_REST_URL, 14 | token: UPSTASH_REDIS_REST_TOKEN, 15 | }), 16 | limiter: Ratelimit.slidingWindow(5, "30 s"), 17 | analytics: true, 18 | }); 19 | 20 | const ip = c.req.header("CF-Connecting-IP") || "127.0.0.1"; 21 | const { success, limit, remaining, reset } = await ratelimit.limit(ip); 22 | 23 | if (!success) { 24 | return c.json({ error: "Rate limit exceeded" }, 429); 25 | } 26 | 27 | c.header("X-RateLimit-Limit", limit.toString()); 28 | c.header("X-RateLimit-Remaining", remaining.toString()); 29 | c.header("X-RateLimit-Reset", reset.toString()); 30 | 31 | await next(); 32 | } 33 | -------------------------------------------------------------------------------- /keyflow-api/src/routes/create.ts: -------------------------------------------------------------------------------- 1 | import { zValidator } from "@hono/zod-validator"; 2 | import { Redis } from "@upstash/redis/cloudflare"; 3 | import { Hono } from "hono"; 4 | import { generateApiKey } from "../config/generateApiKey"; 5 | import { createApiKeySchema } from "../config/schema-validation"; 6 | import type { 7 | CreateKeyRequest, 8 | CreateKeyResponse, 9 | Bindings, 10 | } from "../types/api"; 11 | 12 | const create = new Hono<{ 13 | Bindings: Bindings; 14 | }>(); 15 | 16 | // Create API Key endpoint with proper JSON stringification 17 | create.post( 18 | "/create", 19 | zValidator("json", createApiKeySchema, (result, c) => { 20 | if (!result.success) { 21 | return c.text("Invalid!", 400); 22 | } 23 | }), 24 | async (c) => { 25 | const { UPSTASH_REDIS_REST_TOKEN, UPSTASH_REDIS_REST_URL } = c.env; 26 | const redis = new Redis({ 27 | url: UPSTASH_REDIS_REST_URL, 28 | token: UPSTASH_REDIS_REST_TOKEN, 29 | }); 30 | const body = await c.req.json(); 31 | 32 | const keyId = crypto.randomUUID(); 33 | const key = generateApiKey(body.prefix, body.byteLength || 16); 34 | 35 | const keyData = { 36 | ...body, 37 | key, 38 | keyId, 39 | createdAt: Date.now(), 40 | }; 41 | 42 | // Make sure to stringify the keyData before storing 43 | const encodedKey = encodeURIComponent(key); 44 | 45 | try { 46 | await redis.set(`key:${keyId}`, JSON.stringify(keyData)); 47 | await redis.set(`lookup:${encodedKey}`, keyId); 48 | 49 | return c.json({ key, keyId }); 50 | } catch (error) { 51 | console.error("Error in /keys/create:", error); 52 | return c.json({ error: "Internal Server Error" }, 500); 53 | } 54 | } 55 | ); 56 | 57 | export default create; 58 | -------------------------------------------------------------------------------- /keyflow-api/src/routes/verify.ts: -------------------------------------------------------------------------------- 1 | import { zValidator } from "@hono/zod-validator"; 2 | import { Redis } from "@upstash/redis/cloudflare"; 3 | import { Hono } from "hono"; 4 | import { verifyApiKeySchema } from "../config/schema-validation"; 5 | import type { 6 | CreateKeyRequest, 7 | Bindings, 8 | VerifyKeyRequest, 9 | VerifyKeyResponse, 10 | } from "../types/api"; 11 | 12 | const verify = new Hono<{ 13 | Bindings: Bindings; 14 | }>(); 15 | 16 | verify.post( 17 | "/verify", 18 | zValidator("json", verifyApiKeySchema, (result, c) => { 19 | if (!result.success) { 20 | return c.text("Invalid!", 400); 21 | } 22 | }), 23 | async (c) => { 24 | const { UPSTASH_REDIS_REST_TOKEN, UPSTASH_REDIS_REST_URL } = c.env; 25 | const redis = new Redis({ 26 | url: UPSTASH_REDIS_REST_URL, 27 | token: UPSTASH_REDIS_REST_TOKEN, 28 | }); 29 | const body = await c.req.json(); 30 | try { 31 | console.log("Parsed request body:", body); 32 | 33 | if (!body.key) { 34 | return c.json({ error: "key is required" }, 400); 35 | } 36 | 37 | const encodedKey = encodeURIComponent(body.key); 38 | const keyId = await redis.get(`lookup:${encodedKey}`); 39 | 40 | console.log("Key ID:", keyId); 41 | 42 | if (keyId) { 43 | return c.json({ valid: true }); 44 | } else if (!keyId) { 45 | return c.json({ valid: false }); 46 | } 47 | 48 | // Get the raw string data from Redis 49 | const keyDataString = await redis.get(`key:${keyId}`); 50 | console.log("Raw key data from Redis:", keyDataString); 51 | 52 | if (!keyDataString || typeof keyDataString !== "string") { 53 | return c.json({ valid: false }); 54 | } 55 | 56 | // Parse the string data 57 | let keyData: CreateKeyRequest & { 58 | key: string; 59 | keyId: string; 60 | createdAt: number; 61 | }; 62 | 63 | try { 64 | // Handle case where Redis might return an object instead of a string 65 | if (typeof keyDataString === "object") { 66 | // biome-ignore lint/suspicious/noExplicitAny: 67 | keyData = keyDataString as any; 68 | } else { 69 | keyData = JSON.parse(keyDataString); 70 | } 71 | } catch (parseError) { 72 | console.error("Key data parse error:", parseError); 73 | // If the data in Redis is corrupt, clean it up 74 | await Promise.all([ 75 | redis.del(`key:${keyId}`), 76 | redis.del(`lookup:${encodedKey}`), 77 | ]); 78 | return c.json( 79 | { 80 | error: "Invalid key data in storage", 81 | details: 82 | parseError instanceof Error 83 | ? parseError.message 84 | : "Unknown parse error", 85 | valid: false, 86 | }, 87 | 500 88 | ); 89 | } 90 | 91 | if (keyData.expires && keyData.expires < Date.now()) { 92 | await Promise.all([ 93 | redis.del(`key:${keyId}`), 94 | redis.del(`lookup:${encodedKey}`), 95 | ]); 96 | return c.json({ valid: false }); 97 | } 98 | 99 | const response: VerifyKeyResponse = { 100 | valid: true, 101 | ownerId: keyData.ownerId, 102 | meta: keyData.meta, 103 | expires: keyData.expires, 104 | }; 105 | 106 | if (keyData.ratelimit) { 107 | response.ratelimit = { 108 | limit: keyData.ratelimit.limit, 109 | remaining: keyData.ratelimit.limit, 110 | reset: Date.now() + keyData.ratelimit.refillInterval, 111 | }; 112 | } 113 | 114 | return c.json(response); 115 | } catch (error) { 116 | console.error("Error in /keys/verify:", error); 117 | 118 | return c.json( 119 | { 120 | error: "Internal Server Error", 121 | details: error instanceof Error ? error.message : "Unknown error", 122 | valid: false, 123 | }, 124 | 500 125 | ); 126 | } 127 | } 128 | ); 129 | 130 | export default verify; 131 | -------------------------------------------------------------------------------- /keyflow-api/src/types/api.ts: -------------------------------------------------------------------------------- 1 | export type Bindings = { 2 | UPSTASH_REDIS_REST_URL: string; 3 | UPSTASH_REDIS_REST_TOKEN: string; 4 | }; 5 | 6 | // Types for our API key schema 7 | export type CreateKeyRequest = { 8 | apiId: string; 9 | prefix?: string; 10 | byteLength?: number; 11 | ownerId?: string; 12 | name: string; 13 | meta?: Record; 14 | expires?: number; 15 | ratelimit?: { 16 | type: "fast" | "consistent"; 17 | limit: number; 18 | refillRate: number; 19 | refillInterval: number; 20 | }; 21 | remaining?: number; 22 | refill?: { 23 | amount: number; 24 | interval: "daily" | "monthly"; 25 | }; 26 | enabled?: boolean; 27 | }; 28 | 29 | export type CreateKeyResponse = { 30 | key: string; 31 | keyId: string; 32 | }; 33 | 34 | export type VerifyKeyRequest = { 35 | key: string; 36 | }; 37 | 38 | export type VerifyKeyResponse = { 39 | valid: boolean; 40 | ownerId?: string; 41 | meta?: Record; 42 | expires?: number; 43 | ratelimit?: { 44 | limit: number; 45 | remaining: number; 46 | reset: number; 47 | }; 48 | }; 49 | -------------------------------------------------------------------------------- /keyflow-api/wrangler.example.toml: -------------------------------------------------------------------------------- 1 | name = "keyflow-api" 2 | main = "src/index.ts" 3 | compatibility_date = "2023-05-18" 4 | 5 | [vars] 6 | 7 | UPSTASH_REDIS_REST_URL = "" 8 | UPSTASH_REDIS_REST_TOKEN = "" 9 | -------------------------------------------------------------------------------- /www/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.* 7 | .yarn/* 8 | !.yarn/patches 9 | !.yarn/plugins 10 | !.yarn/releases 11 | !.yarn/versions 12 | 13 | # testing 14 | /coverage 15 | 16 | # next.js 17 | /.next/ 18 | /out/ 19 | 20 | # production 21 | /build 22 | 23 | # misc 24 | .DS_Store 25 | *.pem 26 | 27 | # debug 28 | npm-debug.log* 29 | yarn-debug.log* 30 | yarn-error.log* 31 | 32 | # env files (can opt-in for commiting if needed) 33 | .env* 34 | 35 | # vercel 36 | .vercel 37 | 38 | # typescript 39 | *.tsbuildinfo 40 | next-env.d.ts 41 | 42 | # Sentry Config File 43 | .env.sentry-build-plugin 44 | -------------------------------------------------------------------------------- /www/bun.lockb: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evanssmaina/keyflow/1dd8349c17075bd9c99d9aa5954ec5d033a7ea9f/www/bun.lockb -------------------------------------------------------------------------------- /www/components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "src/app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | } 20 | } -------------------------------------------------------------------------------- /www/convex/_generated/api.d.ts: -------------------------------------------------------------------------------- 1 | /* prettier-ignore-start */ 2 | 3 | /* eslint-disable */ 4 | /** 5 | * Generated `api` utility. 6 | * 7 | * THIS CODE IS AUTOMATICALLY GENERATED. 8 | * 9 | * To regenerate, run `npx convex dev`. 10 | * @module 11 | */ 12 | 13 | import type { 14 | ApiFromModules, 15 | FilterApi, 16 | FunctionReference, 17 | } from "convex/server"; 18 | import type * as apiRequests from "../apiRequests.js"; 19 | 20 | /** 21 | * A utility for referencing Convex functions in your app's API. 22 | * 23 | * Usage: 24 | * ```js 25 | * const myFunctionReference = api.myModule.myFunction; 26 | * ``` 27 | */ 28 | declare const fullApi: ApiFromModules<{ 29 | apiRequests: typeof apiRequests; 30 | }>; 31 | export declare const api: FilterApi< 32 | typeof fullApi, 33 | FunctionReference 34 | >; 35 | export declare const internal: FilterApi< 36 | typeof fullApi, 37 | FunctionReference 38 | >; 39 | 40 | /* prettier-ignore-end */ 41 | -------------------------------------------------------------------------------- /www/convex/_generated/api.js: -------------------------------------------------------------------------------- 1 | /* prettier-ignore-start */ 2 | 3 | /* eslint-disable */ 4 | /** 5 | * Generated `api` utility. 6 | * 7 | * THIS CODE IS AUTOMATICALLY GENERATED. 8 | * 9 | * To regenerate, run `npx convex dev`. 10 | * @module 11 | */ 12 | 13 | import { anyApi } from "convex/server"; 14 | 15 | /** 16 | * A utility for referencing Convex functions in your app's API. 17 | * 18 | * Usage: 19 | * ```js 20 | * const myFunctionReference = api.myModule.myFunction; 21 | * ``` 22 | */ 23 | export const api = anyApi; 24 | export const internal = anyApi; 25 | 26 | /* prettier-ignore-end */ 27 | -------------------------------------------------------------------------------- /www/convex/_generated/dataModel.d.ts: -------------------------------------------------------------------------------- 1 | /* prettier-ignore-start */ 2 | 3 | /* eslint-disable */ 4 | /** 5 | * Generated data model types. 6 | * 7 | * THIS CODE IS AUTOMATICALLY GENERATED. 8 | * 9 | * To regenerate, run `npx convex dev`. 10 | * @module 11 | */ 12 | 13 | import type { 14 | DataModelFromSchemaDefinition, 15 | DocumentByName, 16 | TableNamesInDataModel, 17 | SystemTableNames, 18 | } from "convex/server"; 19 | import type { GenericId } from "convex/values"; 20 | import schema from "../schema.js"; 21 | 22 | /** 23 | * The names of all of your Convex tables. 24 | */ 25 | export type TableNames = TableNamesInDataModel; 26 | 27 | /** 28 | * The type of a document stored in Convex. 29 | * 30 | * @typeParam TableName - A string literal type of the table name (like "users"). 31 | */ 32 | export type Doc = DocumentByName< 33 | DataModel, 34 | TableName 35 | >; 36 | 37 | /** 38 | * An identifier for a document in Convex. 39 | * 40 | * Convex documents are uniquely identified by their `Id`, which is accessible 41 | * on the `_id` field. To learn more, see [Document IDs](https://docs.convex.dev/using/document-ids). 42 | * 43 | * Documents can be loaded using `db.get(id)` in query and mutation functions. 44 | * 45 | * IDs are just strings at runtime, but this type can be used to distinguish them from other 46 | * strings when type checking. 47 | * 48 | * @typeParam TableName - A string literal type of the table name (like "users"). 49 | */ 50 | export type Id = 51 | GenericId; 52 | 53 | /** 54 | * A type describing your Convex data model. 55 | * 56 | * This type includes information about what tables you have, the type of 57 | * documents stored in those tables, and the indexes defined on them. 58 | * 59 | * This type is used to parameterize methods like `queryGeneric` and 60 | * `mutationGeneric` to make them type-safe. 61 | */ 62 | export type DataModel = DataModelFromSchemaDefinition; 63 | 64 | /* prettier-ignore-end */ 65 | -------------------------------------------------------------------------------- /www/convex/_generated/server.d.ts: -------------------------------------------------------------------------------- 1 | /* prettier-ignore-start */ 2 | 3 | /* eslint-disable */ 4 | /** 5 | * Generated utilities for implementing server-side Convex query and mutation functions. 6 | * 7 | * THIS CODE IS AUTOMATICALLY GENERATED. 8 | * 9 | * To regenerate, run `npx convex dev`. 10 | * @module 11 | */ 12 | 13 | import { 14 | ActionBuilder, 15 | HttpActionBuilder, 16 | MutationBuilder, 17 | QueryBuilder, 18 | GenericActionCtx, 19 | GenericMutationCtx, 20 | GenericQueryCtx, 21 | GenericDatabaseReader, 22 | GenericDatabaseWriter, 23 | } from "convex/server"; 24 | import type { DataModel } from "./dataModel.js"; 25 | 26 | /** 27 | * Define a query in this Convex app's public API. 28 | * 29 | * This function will be allowed to read your Convex database and will be accessible from the client. 30 | * 31 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 32 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 33 | */ 34 | export declare const query: QueryBuilder; 35 | 36 | /** 37 | * Define a query that is only accessible from other Convex functions (but not from the client). 38 | * 39 | * This function will be allowed to read from your Convex database. It will not be accessible from the client. 40 | * 41 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 42 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 43 | */ 44 | export declare const internalQuery: QueryBuilder; 45 | 46 | /** 47 | * Define a mutation in this Convex app's public API. 48 | * 49 | * This function will be allowed to modify your Convex database and will be accessible from the client. 50 | * 51 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 52 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 53 | */ 54 | export declare const mutation: MutationBuilder; 55 | 56 | /** 57 | * Define a mutation that is only accessible from other Convex functions (but not from the client). 58 | * 59 | * This function will be allowed to modify your Convex database. It will not be accessible from the client. 60 | * 61 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 62 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 63 | */ 64 | export declare const internalMutation: MutationBuilder; 65 | 66 | /** 67 | * Define an action in this Convex app's public API. 68 | * 69 | * An action is a function which can execute any JavaScript code, including non-deterministic 70 | * code and code with side-effects, like calling third-party services. 71 | * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive. 72 | * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}. 73 | * 74 | * @param func - The action. It receives an {@link ActionCtx} as its first argument. 75 | * @returns The wrapped action. Include this as an `export` to name it and make it accessible. 76 | */ 77 | export declare const action: ActionBuilder; 78 | 79 | /** 80 | * Define an action that is only accessible from other Convex functions (but not from the client). 81 | * 82 | * @param func - The function. It receives an {@link ActionCtx} as its first argument. 83 | * @returns The wrapped function. Include this as an `export` to name it and make it accessible. 84 | */ 85 | export declare const internalAction: ActionBuilder; 86 | 87 | /** 88 | * Define an HTTP action. 89 | * 90 | * This function will be used to respond to HTTP requests received by a Convex 91 | * deployment if the requests matches the path and method where this action 92 | * is routed. Be sure to route your action in `convex/http.js`. 93 | * 94 | * @param func - The function. It receives an {@link ActionCtx} as its first argument. 95 | * @returns The wrapped function. Import this function from `convex/http.js` and route it to hook it up. 96 | */ 97 | export declare const httpAction: HttpActionBuilder; 98 | 99 | /** 100 | * A set of services for use within Convex query functions. 101 | * 102 | * The query context is passed as the first argument to any Convex query 103 | * function run on the server. 104 | * 105 | * This differs from the {@link MutationCtx} because all of the services are 106 | * read-only. 107 | */ 108 | export type QueryCtx = GenericQueryCtx; 109 | 110 | /** 111 | * A set of services for use within Convex mutation functions. 112 | * 113 | * The mutation context is passed as the first argument to any Convex mutation 114 | * function run on the server. 115 | */ 116 | export type MutationCtx = GenericMutationCtx; 117 | 118 | /** 119 | * A set of services for use within Convex action functions. 120 | * 121 | * The action context is passed as the first argument to any Convex action 122 | * function run on the server. 123 | */ 124 | export type ActionCtx = GenericActionCtx; 125 | 126 | /** 127 | * An interface to read from the database within Convex query functions. 128 | * 129 | * The two entry points are {@link DatabaseReader.get}, which fetches a single 130 | * document by its {@link Id}, or {@link DatabaseReader.query}, which starts 131 | * building a query. 132 | */ 133 | export type DatabaseReader = GenericDatabaseReader; 134 | 135 | /** 136 | * An interface to read from and write to the database within Convex mutation 137 | * functions. 138 | * 139 | * Convex guarantees that all writes within a single mutation are 140 | * executed atomically, so you never have to worry about partial writes leaving 141 | * your data in an inconsistent state. See [the Convex Guide](https://docs.convex.dev/understanding/convex-fundamentals/functions#atomicity-and-optimistic-concurrency-control) 142 | * for the guarantees Convex provides your functions. 143 | */ 144 | export type DatabaseWriter = GenericDatabaseWriter; 145 | 146 | /* prettier-ignore-end */ 147 | -------------------------------------------------------------------------------- /www/convex/_generated/server.js: -------------------------------------------------------------------------------- 1 | /* prettier-ignore-start */ 2 | 3 | /* eslint-disable */ 4 | /** 5 | * Generated utilities for implementing server-side Convex query and mutation functions. 6 | * 7 | * THIS CODE IS AUTOMATICALLY GENERATED. 8 | * 9 | * To regenerate, run `npx convex dev`. 10 | * @module 11 | */ 12 | 13 | import { 14 | actionGeneric, 15 | httpActionGeneric, 16 | queryGeneric, 17 | mutationGeneric, 18 | internalActionGeneric, 19 | internalMutationGeneric, 20 | internalQueryGeneric, 21 | } from "convex/server"; 22 | 23 | /** 24 | * Define a query in this Convex app's public API. 25 | * 26 | * This function will be allowed to read your Convex database and will be accessible from the client. 27 | * 28 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 29 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 30 | */ 31 | export const query = queryGeneric; 32 | 33 | /** 34 | * Define a query that is only accessible from other Convex functions (but not from the client). 35 | * 36 | * This function will be allowed to read from your Convex database. It will not be accessible from the client. 37 | * 38 | * @param func - The query function. It receives a {@link QueryCtx} as its first argument. 39 | * @returns The wrapped query. Include this as an `export` to name it and make it accessible. 40 | */ 41 | export const internalQuery = internalQueryGeneric; 42 | 43 | /** 44 | * Define a mutation in this Convex app's public API. 45 | * 46 | * This function will be allowed to modify your Convex database and will be accessible from the client. 47 | * 48 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 49 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 50 | */ 51 | export const mutation = mutationGeneric; 52 | 53 | /** 54 | * Define a mutation that is only accessible from other Convex functions (but not from the client). 55 | * 56 | * This function will be allowed to modify your Convex database. It will not be accessible from the client. 57 | * 58 | * @param func - The mutation function. It receives a {@link MutationCtx} as its first argument. 59 | * @returns The wrapped mutation. Include this as an `export` to name it and make it accessible. 60 | */ 61 | export const internalMutation = internalMutationGeneric; 62 | 63 | /** 64 | * Define an action in this Convex app's public API. 65 | * 66 | * An action is a function which can execute any JavaScript code, including non-deterministic 67 | * code and code with side-effects, like calling third-party services. 68 | * They can be run in Convex's JavaScript environment or in Node.js using the "use node" directive. 69 | * They can interact with the database indirectly by calling queries and mutations using the {@link ActionCtx}. 70 | * 71 | * @param func - The action. It receives an {@link ActionCtx} as its first argument. 72 | * @returns The wrapped action. Include this as an `export` to name it and make it accessible. 73 | */ 74 | export const action = actionGeneric; 75 | 76 | /** 77 | * Define an action that is only accessible from other Convex functions (but not from the client). 78 | * 79 | * @param func - The function. It receives an {@link ActionCtx} as its first argument. 80 | * @returns The wrapped function. Include this as an `export` to name it and make it accessible. 81 | */ 82 | export const internalAction = internalActionGeneric; 83 | 84 | /** 85 | * Define a Convex HTTP action. 86 | * 87 | * @param func - The function. It receives an {@link ActionCtx} as its first argument, and a `Request` object 88 | * as its second. 89 | * @returns The wrapped endpoint function. Route a URL path to this function in `convex/http.js`. 90 | */ 91 | export const httpAction = httpActionGeneric; 92 | 93 | /* prettier-ignore-end */ 94 | -------------------------------------------------------------------------------- /www/convex/apiRequests.ts: -------------------------------------------------------------------------------- 1 | import { mutation, query } from "./_generated/server"; 2 | import { v } from "convex/values"; 3 | 4 | export const create = mutation({ 5 | args: { 6 | method: v.string(), 7 | url: v.string(), 8 | status_code: v.number(), 9 | request_body: v.optional( 10 | v.object({ 11 | name: v.optional(v.string()), 12 | apiId: v.optional(v.string()), 13 | prefix: v.optional(v.string()), 14 | byteLength: v.optional(v.number()), 15 | ownerId: v.optional(v.string()), 16 | meta: v.optional( 17 | v.object({ 18 | plan: v.optional(v.string()), 19 | createdBy: v.optional(v.string()), 20 | }) 21 | ), 22 | expires: v.optional(v.number()), 23 | ratelimit: v.optional( 24 | v.object({ 25 | type: v.optional(v.string()), 26 | limit: v.optional(v.number()), 27 | refillRate: v.optional(v.number()), 28 | refillInterval: v.optional(v.number()), 29 | }) 30 | ), 31 | remaining: v.optional(v.number()), 32 | refill: v.optional( 33 | v.object({ 34 | amount: v.optional(v.number()), 35 | interval: v.optional(v.string()), 36 | }) 37 | ), 38 | enabled: v.optional(v.boolean()), 39 | key: v.optional(v.string()), 40 | }) 41 | ), 42 | result_body: v.optional( 43 | v.object({ 44 | key: v.optional(v.string()), 45 | keyId: v.optional(v.string()), 46 | valid: v.optional(v.boolean()), 47 | error: v.optional(v.string()), 48 | }) 49 | ), 50 | }, 51 | handler: async (ctx, args) => { 52 | await ctx.db.insert("api_requests", args); 53 | }, 54 | }); 55 | 56 | export const get = query({ 57 | handler: async (ctx) => { 58 | const data = await ctx.db.query("api_requests").order("desc").collect(); 59 | 60 | return data.map((item) => ({ 61 | id: item._id, 62 | method: item.method, 63 | statusCode: item.status_code, 64 | path: item.url, 65 | createdAt: item._creationTime, 66 | request_body: item.request_body, 67 | })); 68 | }, 69 | }); 70 | 71 | export const getByPath = query({ 72 | args: { 73 | path: v.string(), 74 | }, 75 | handler: async (ctx, args) => { 76 | const data = await ctx.db 77 | .query("api_requests") 78 | .filter((q) => q.eq(q.field("url"), args.path)) 79 | .collect(); 80 | return data.map((item) => ({ 81 | id: item._id, 82 | method: item.method, 83 | statusCode: item.status_code, 84 | path: item.url, 85 | createdAt: item._creationTime, 86 | request_body: item.request_body, 87 | response_body: item.result_body, 88 | })); 89 | }, 90 | }); 91 | 92 | export const getById = query({ 93 | args: { 94 | id: v.id("api_requests"), 95 | }, 96 | handler: async (ctx, args) => { 97 | const data = await ctx.db.get(args.id); 98 | return { 99 | requestData: data?.request_body, 100 | responseData: data?.result_body, 101 | path: data?.url, 102 | statusCode: data?.status_code, 103 | }; 104 | }, 105 | }); 106 | -------------------------------------------------------------------------------- /www/next.config.ts: -------------------------------------------------------------------------------- 1 | import { withSentryConfig } from "@sentry/nextjs"; 2 | import type { NextConfig } from "next"; 3 | 4 | const nextConfig: NextConfig = { 5 | /* config options here */ 6 | }; 7 | 8 | export default withSentryConfig(withSentryConfig(nextConfig, { 9 | // For all available options, see: 10 | // https://github.com/getsentry/sentry-webpack-plugin#options 11 | 12 | org: "reelhype", 13 | project: "keyflow", 14 | 15 | // Only print logs for uploading source maps in CI 16 | silent: !process.env.CI, 17 | 18 | // For all available options, see: 19 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/ 20 | 21 | // Upload a larger set of source maps for prettier stack traces (increases build time) 22 | widenClientFileUpload: true, 23 | 24 | // Automatically annotate React components to show their full name in breadcrumbs and session replay 25 | reactComponentAnnotation: { 26 | enabled: true, 27 | }, 28 | 29 | // Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers. 30 | // This can increase your server load as well as your hosting bill. 31 | // Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client- 32 | // side errors will fail. 33 | tunnelRoute: "/monitoring", 34 | 35 | // Hides source maps from generated client bundles 36 | hideSourceMaps: true, 37 | 38 | // Automatically tree-shake Sentry logger statements to reduce bundle size 39 | disableLogger: true, 40 | 41 | // Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.) 42 | // See the following for more information: 43 | // https://docs.sentry.io/product/crons/ 44 | // https://vercel.com/docs/cron-jobs 45 | automaticVercelMonitors: true, 46 | }), { 47 | // For all available options, see: 48 | // https://github.com/getsentry/sentry-webpack-plugin#options 49 | 50 | org: "reelhype", 51 | project: "keyflow", 52 | 53 | // Only print logs for uploading source maps in CI 54 | silent: !process.env.CI, 55 | 56 | // For all available options, see: 57 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/manual-setup/ 58 | 59 | // Upload a larger set of source maps for prettier stack traces (increases build time) 60 | widenClientFileUpload: true, 61 | 62 | // Automatically annotate React components to show their full name in breadcrumbs and session replay 63 | reactComponentAnnotation: { 64 | enabled: true, 65 | }, 66 | 67 | // Route browser requests to Sentry through a Next.js rewrite to circumvent ad-blockers. 68 | // This can increase your server load as well as your hosting bill. 69 | // Note: Check that the configured route will not match with your Next.js middleware, otherwise reporting of client- 70 | // side errors will fail. 71 | tunnelRoute: "/monitoring", 72 | 73 | // Hides source maps from generated client bundles 74 | hideSourceMaps: true, 75 | 76 | // Automatically tree-shake Sentry logger statements to reduce bundle size 77 | disableLogger: true, 78 | 79 | // Enables automatic instrumentation of Vercel Cron Monitors. (Does not yet work with App Router route handlers.) 80 | // See the following for more information: 81 | // https://docs.sentry.io/product/crons/ 82 | // https://vercel.com/docs/cron-jobs 83 | automaticVercelMonitors: true, 84 | }); -------------------------------------------------------------------------------- /www/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "www", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev --turbo", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@fluid-tailwind/tailwind-merge": "^0.0.3", 13 | "@number-flow/react": "^0.2.6", 14 | "@openpanel/nextjs": "^1.0.5", 15 | "@radix-ui/react-dialog": "^1.1.2", 16 | "@radix-ui/react-dropdown-menu": "^2.1.2", 17 | "@radix-ui/react-label": "^2.1.0", 18 | "@radix-ui/react-separator": "^1.1.0", 19 | "@radix-ui/react-slot": "^1.1.0", 20 | "@radix-ui/react-switch": "^1.1.1", 21 | "@radix-ui/react-tabs": "^1.1.1", 22 | "@sentry/nextjs": "^8", 23 | "@tanstack/react-table": "^8.20.5", 24 | "class-variance-authority": "^0.7.0", 25 | "clsx": "^2.1.1", 26 | "convex": "^1.16.6", 27 | "date-fns": "^4.1.0", 28 | "framer-motion": "^11.11.10", 29 | "lucide-react": "^0.453.0", 30 | "next": "15.0.0-rc.1", 31 | "next-safe-action": "^7.9.7", 32 | "next-themes": "^0.3.0", 33 | "react": "19.0.0-rc-cd22717c-20241013", 34 | "react-dom": "19.0.0-rc-cd22717c-20241013", 35 | "sonner": "^1.5.0", 36 | "tailwind-merge": "^2.5.4", 37 | "tailwindcss-animate": "^1.0.7", 38 | "vaul": "^1.1.0", 39 | "zod": "^3.23.8", 40 | "zod-form-data": "^2.0.2" 41 | }, 42 | "devDependencies": { 43 | "@types/node": "^20", 44 | "@types/react": "^18", 45 | "@types/react-dom": "^18", 46 | "fluid-tailwind": "^1.0.3", 47 | "postcss": "^8", 48 | "tailwindcss": "^3.4.1", 49 | "typescript": "^5" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /www/postcss.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('postcss-load-config').Config} */ 2 | const config = { 3 | plugins: { 4 | tailwindcss: {}, 5 | }, 6 | }; 7 | 8 | export default config; 9 | -------------------------------------------------------------------------------- /www/public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evanssmaina/keyflow/1dd8349c17075bd9c99d9aa5954ec5d033a7ea9f/www/public/apple-touch-icon.png -------------------------------------------------------------------------------- /www/public/favicon-48x48.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evanssmaina/keyflow/1dd8349c17075bd9c99d9aa5954ec5d033a7ea9f/www/public/favicon-48x48.png -------------------------------------------------------------------------------- /www/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evanssmaina/keyflow/1dd8349c17075bd9c99d9aa5954ec5d033a7ea9f/www/public/favicon.ico -------------------------------------------------------------------------------- /www/public/favicon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /www/public/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "KeyFlow", 3 | "short_name": "KeyFlow", 4 | "icons": [ 5 | { 6 | "src": "/web-app-manifest-192x192.png", 7 | "sizes": "192x192", 8 | "type": "image/png", 9 | "purpose": "maskable" 10 | }, 11 | { 12 | "src": "/web-app-manifest-512x512.png", 13 | "sizes": "512x512", 14 | "type": "image/png", 15 | "purpose": "maskable" 16 | } 17 | ], 18 | "theme_color": "#ffffff", 19 | "background_color": "#ffffff", 20 | "display": "standalone" 21 | } 22 | -------------------------------------------------------------------------------- /www/public/web-app-manifest-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evanssmaina/keyflow/1dd8349c17075bd9c99d9aa5954ec5d033a7ea9f/www/public/web-app-manifest-192x192.png -------------------------------------------------------------------------------- /www/public/web-app-manifest-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/evanssmaina/keyflow/1dd8349c17075bd9c99d9aa5954ec5d033a7ea9f/www/public/web-app-manifest-512x512.png -------------------------------------------------------------------------------- /www/sentry.client.config.ts: -------------------------------------------------------------------------------- 1 | // This file configures the initialization of Sentry on the client. 2 | // The config you add here will be used whenever a users loads a page in their browser. 3 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/ 4 | 5 | import * as Sentry from "@sentry/nextjs"; 6 | 7 | Sentry.init({ 8 | dsn: "https://69853e93ea3acac9f407d47639b45769@o4507520909901824.ingest.de.sentry.io/4508178087805008", 9 | 10 | // Add optional integrations for additional features 11 | integrations: [ 12 | Sentry.replayIntegration(), 13 | ], 14 | 15 | // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control. 16 | tracesSampleRate: 1, 17 | 18 | // Define how likely Replay events are sampled. 19 | // This sets the sample rate to be 10%. You may want this to be 100% while 20 | // in development and sample at a lower rate in production 21 | replaysSessionSampleRate: 0.1, 22 | 23 | // Define how likely Replay events are sampled when an error occurs. 24 | replaysOnErrorSampleRate: 1.0, 25 | 26 | // Setting this option to true will print useful information to the console while you're setting up Sentry. 27 | debug: false, 28 | }); 29 | -------------------------------------------------------------------------------- /www/sentry.edge.config.ts: -------------------------------------------------------------------------------- 1 | // This file configures the initialization of Sentry for edge features (middleware, edge routes, and so on). 2 | // The config you add here will be used whenever one of the edge features is loaded. 3 | // Note that this config is unrelated to the Vercel Edge Runtime and is also required when running locally. 4 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/ 5 | 6 | import * as Sentry from "@sentry/nextjs"; 7 | 8 | Sentry.init({ 9 | dsn: "https://69853e93ea3acac9f407d47639b45769@o4507520909901824.ingest.de.sentry.io/4508178087805008", 10 | 11 | // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control. 12 | tracesSampleRate: 1, 13 | 14 | // Setting this option to true will print useful information to the console while you're setting up Sentry. 15 | debug: false, 16 | }); 17 | -------------------------------------------------------------------------------- /www/sentry.server.config.ts: -------------------------------------------------------------------------------- 1 | // This file configures the initialization of Sentry on the server. 2 | // The config you add here will be used whenever the server handles a request. 3 | // https://docs.sentry.io/platforms/javascript/guides/nextjs/ 4 | 5 | import * as Sentry from "@sentry/nextjs"; 6 | 7 | Sentry.init({ 8 | dsn: "https://69853e93ea3acac9f407d47639b45769@o4507520909901824.ingest.de.sentry.io/4508178087805008", 9 | 10 | // Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control. 11 | tracesSampleRate: 1, 12 | 13 | // Setting this option to true will print useful information to the console while you're setting up Sentry. 14 | debug: false, 15 | }); 16 | -------------------------------------------------------------------------------- /www/src/actions/create-apiKey.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { actionClient } from "@/config/safe-action"; 4 | import { z } from "zod"; 5 | import { zfd } from "zod-form-data"; 6 | import type { ApiKeyData } from "@/types/action-types"; 7 | import { exampleData } from "@/config/custom-data"; 8 | 9 | // Validation schema 10 | const schema = zfd.formData({ 11 | customData: zfd.text(z.string().optional()), 12 | exampleData: zfd.text(z.string().optional()), 13 | name: zfd.text(z.string().min(1).optional()), 14 | prefix: zfd.text(z.string().optional()), 15 | expiration: zfd.text(z.string().optional()), 16 | rateLimit: zfd.text(z.string().optional()), 17 | byteLength: zfd.text(z.number().optional()), 18 | }); 19 | 20 | // Action definition 21 | export const createApiKeyAction = actionClient.schema(schema).stateAction<{ 22 | data?: any; 23 | message?: string; 24 | error?: string; 25 | }>(async ({ parsedInput }, { prevResult }) => { 26 | let data: ApiKeyData; 27 | 28 | if (parsedInput.exampleData) { 29 | // Use example data 30 | data = exampleData; 31 | } else if (parsedInput.customData) { 32 | // Use custom data if provided 33 | data = JSON.parse(parsedInput.customData); 34 | } else { 35 | // Use structured data 36 | data = { 37 | name: parsedInput.name!, 38 | prefix: parsedInput.prefix, 39 | expires: 40 | parsedInput.expiration ? 41 | new Date(parsedInput.expiration).getTime() 42 | : undefined, 43 | ratelimit: 44 | parsedInput.rateLimit ? 45 | { 46 | type: "consistent", 47 | limit: parseInt(parsedInput.rateLimit, 10), 48 | refillRate: parseInt(parsedInput.rateLimit, 10), 49 | refillInterval: 60000, // 1 minute in milliseconds 50 | } 51 | : undefined, 52 | }; 53 | } 54 | 55 | const postData = await fetch( 56 | "https://keyflow-api.mpesaflow.com/keys/create", 57 | { 58 | method: "POST", 59 | headers: { 60 | "Content-Type": "application/json", 61 | }, 62 | body: JSON.stringify(data), 63 | } 64 | ).then((res) => res.json()); 65 | 66 | return { 67 | data: postData, 68 | message: "API key created successfully", 69 | }; 70 | }); 71 | -------------------------------------------------------------------------------- /www/src/actions/verify-apiKey.ts: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import { actionClient } from "@/config/safe-action"; 4 | import { z } from "zod"; 5 | import { zfd } from "zod-form-data"; 6 | 7 | const schema = zfd.formData({ 8 | key: zfd.text(z.string()), 9 | }); 10 | 11 | export const verifApikeyAction = actionClient 12 | .schema(schema) 13 | .stateAction(async ({ parsedInput }) => { 14 | const response = await fetch( 15 | "https://keyflow-api.mpesaflow.com/keys/verify", 16 | { 17 | method: "POST", 18 | headers: { 19 | "Content-Type": "application/json", 20 | }, 21 | body: JSON.stringify({ key: parsedInput.key }), // Send as JSON object with key property 22 | } 23 | ).then((res) => res.json()); 24 | 25 | return { 26 | data: response, 27 | message: response.valid === false ? "Invalid API key" : "Valid API key", 28 | }; 29 | }); 30 | -------------------------------------------------------------------------------- /www/src/app/_components/create-apiKey.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from "@/components/ui/card" 4 | import { Button } from "@/components/ui/button" 5 | import { Input } from "@/components/ui/input" 6 | import { Label } from "@/components/ui/label" 7 | import { Switch } from "@/components/ui/switch" 8 | import { toast } from "sonner"; 9 | import { useEffect, useState } from "react"; 10 | import { Textarea } from "@/components/ui/textarea"; 11 | import { Separator } from "@/components/ui/separator"; 12 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"; 13 | import { useStateAction } from "next-safe-action/stateful-hooks"; 14 | import { createApiKeyAction } from "@/actions/create-apiKey"; 15 | import { AlertCircle } from "lucide-react"; 16 | import { Alert, AlertDescription } from "@/components/ui/alert"; 17 | import { validateJson, formatCustomData } from "@/config/validateJson"; 18 | import { exampleData } from "@/config/custom-data"; 19 | 20 | export function CreateApiKey() { 21 | const { execute, result, isPending } = useStateAction(createApiKeyAction); 22 | const [name, setName] = useState(''); 23 | const [prefix, setPrefix] = useState(''); 24 | const [expiration, setExpiration] = useState(''); 25 | const [rateLimit, setRateLimit] = useState(''); 26 | const [enableRateLimit, setEnableRateLimit] = useState(false); 27 | const [customData, setCustomData] = useState(''); 28 | const [currentTab, setCurrentTab] = useState('structured'); 29 | const [jsonError, setJsonError] = useState(null); 30 | 31 | useEffect(() => { 32 | if (!result?.data) return; 33 | if (result.data?.message) { 34 | toast.success(result.data.message); 35 | } else if (result.serverError) { 36 | toast.error(result.serverError); 37 | } 38 | }, [result]); 39 | 40 | 41 | 42 | const handleSubmit = async (formData: FormData) => { 43 | if (currentTab === 'custom' && customData.trim()) { 44 | if (!validateJson(customData, setJsonError)) { 45 | return; // Prevent form submission if JSON is invalid 46 | } 47 | } 48 | execute(formData); 49 | }; 50 | 51 | const handleTabChange = (value: string) => { 52 | setCurrentTab(value); 53 | setJsonError(null); // Clear any existing JSON errors when switching tabs 54 | }; 55 | 56 | return ( 57 | 58 | 59 | Create API Key 60 | 61 | Create a new API key to use with the Keyflow API. 62 | 63 | 64 | 65 | 66 |
67 | 68 |
69 |
70 | POST 71 |
72 | 73 |

74 | https://keyflow-api.mpesaflow.com/keys/create 75 |

76 |
77 | 78 | 79 | 80 | 81 |

Structured Input

82 |
83 | 84 |

Custom Data

85 |
86 | 87 |

Example Data

88 |
89 |
90 | 91 |
92 |
93 | 94 | setName(e.target.value)} 99 | placeholder="Enter API key name" 100 | required 101 | /> 102 | {result?.validationErrors?.name?._errors} 103 |
104 |
105 | 106 | setPrefix(e.target.value)} 111 | placeholder="key_live or key_test" 112 | /> 113 | {result?.validationErrors?.prefix?._errors} 114 |
115 |
116 | 117 | setExpiration(e.target.value)} 123 | /> 124 | {result?.validationErrors?.expiration?._errors} 125 |
126 |
127 | 132 | 133 |
134 | {enableRateLimit && ( 135 |
136 | 137 | setRateLimit(e.target.value)} 143 | placeholder="Enter rate limit" 144 | min="1" 145 | /> 146 | {result?.validationErrors?.rateLimit?._errors} 147 |
148 | )} 149 |
150 |
151 | 152 |
153 | 154 |
155 |