├── .env.example ├── .eslintrc.json ├── .gitignore ├── README.md ├── app ├── api │ └── user-encryption │ │ └── route.ts ├── favicon.ico ├── globals.css ├── layout.tsx ├── page.tsx ├── sign-in │ └── [[...sign-in]] │ │ └── page.tsx └── sign-up │ └── [[...sign-up]] │ └── page.tsx ├── components ├── EncryptionContainer.tsx └── Navbar.tsx ├── lib ├── crypto-utils.ts ├── openai.ts └── prismadb.ts ├── middleware.ts ├── next.config.mjs ├── package-lock.json ├── package.json ├── postcss.config.js ├── prisma └── schema.prisma ├── public ├── next.svg └── vercel.svg ├── tailwind.config.ts └── tsconfig.json /.env.example: -------------------------------------------------------------------------------- 1 | DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public" 2 | 3 | NEXT_PUBLIC_CLERK_PUBLISHABLE_KEY=123 4 | CLERK_SECRET_KEY=123 5 | NEXT_PUBLIC_CLERK_SIGN_IN_URL=/sign-in 6 | NEXT_PUBLIC_CLERK_SIGN_UP_URL=/sign-up 7 | NEXT_PUBLIC_CLERK_AFTER_SIGN_IN_URL=/ 8 | NEXT_PUBLIC_CLERK_AFTER_SIGN_UP_URL=/ -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | .yarn/install-state.gz 8 | 9 | # testing 10 | /coverage 11 | 12 | # next.js 13 | /.next/ 14 | /out/ 15 | 16 | # production 17 | /build 18 | 19 | # misc 20 | .DS_Store 21 | *.pem 22 | 23 | # debug 24 | npm-debug.log* 25 | yarn-debug.log* 26 | yarn-error.log* 27 | 28 | # local env files 29 | .env*.local 30 | .env 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | This is a [Next.js](https://nextjs.org/) project bootstrapped with [`create-next-app`](https://github.com/vercel/next.js/tree/canary/packages/create-next-app). 2 | 3 | ## Getting Started 4 | 5 | First, run the development server: 6 | 7 | ```bash 8 | npm run dev 9 | # or 10 | yarn dev 11 | # or 12 | pnpm dev 13 | # or 14 | bun dev 15 | ``` 16 | 17 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 18 | 19 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 20 | 21 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 22 | 23 | ## Learn More 24 | 25 | To learn more about Next.js, take a look at the following resources: 26 | 27 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 28 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 29 | 30 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 31 | 32 | ## Deploy on Vercel 33 | 34 | The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js. 35 | 36 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 37 | # securely-store-openai-keys 38 | -------------------------------------------------------------------------------- /app/api/user-encryption/route.ts: -------------------------------------------------------------------------------- 1 | import { prismadb } from "@/lib/prismadb"; 2 | import { currentUser } from "@clerk/nextjs"; 3 | import { NextResponse } from "next/server"; 4 | 5 | export async function POST(request: Request) { 6 | const user = await currentUser(); 7 | if (!user) { 8 | return NextResponse.json({ error: "Unauthenticated" }, { status: 401 }); 9 | } 10 | 11 | const { salt, passphrase } = await request.json(); 12 | 13 | console.log("salt, passphrase", salt, passphrase); 14 | 15 | if (!salt || !passphrase) { 16 | return NextResponse.json({ error: "Invalid body" }, { status: 400 }); 17 | } 18 | 19 | try { 20 | const userEncryptions = await prismadb.userEncryption.findMany({ 21 | where: { userId: user.id }, 22 | }); 23 | 24 | if (userEncryptions.length > 0) { 25 | await prismadb.userEncryption.update({ 26 | where: { userId: user.id }, 27 | data: { salt, passphrase }, 28 | }); 29 | } else { 30 | await prismadb.userEncryption.create({ 31 | data: { salt, passphrase, userId: user.id }, 32 | }); 33 | } 34 | 35 | return NextResponse.json({ success: true }); 36 | } catch (e) { 37 | console.error(e); 38 | return NextResponse.json({ message: "Database error" }, { status: 500 }); 39 | } 40 | } 41 | 42 | export async function GET() { 43 | const user = await currentUser(); 44 | if (!user) { 45 | return NextResponse.json({ error: "Unauthenticated" }, { status: 401 }); 46 | } 47 | 48 | try { 49 | const userEncryption = await prismadb.userEncryption.findFirst({ 50 | where: { userId: user.id }, 51 | }); 52 | 53 | if (!userEncryption) { 54 | return NextResponse.json({ salt: null, passphrase: null }); 55 | } 56 | 57 | return NextResponse.json({ 58 | salt: userEncryption.salt, 59 | passphrase: userEncryption.passphrase, 60 | }); 61 | } catch (e) { 62 | console.error(e); 63 | return NextResponse.json({ message: "Database error" }, { status: 500 }); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bhancockio/securely-store-openai-keys/428182821399898c849c2ae7b599d66e2d6aaf8c/app/favicon.ico -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | :root { 6 | --foreground-rgb: 0, 0, 0; 7 | --background-start-rgb: 214, 219, 220; 8 | --background-end-rgb: 255, 255, 255; 9 | } 10 | 11 | @media (prefers-color-scheme: dark) { 12 | :root { 13 | --foreground-rgb: 255, 255, 255; 14 | --background-start-rgb: 0, 0, 0; 15 | --background-end-rgb: 0, 0, 0; 16 | } 17 | } 18 | 19 | @layer utilities { 20 | .text-balance { 21 | text-wrap: balance; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "./globals.css"; 4 | import { ClerkProvider } from "@clerk/nextjs"; 5 | import Navbar from "@/components/Navbar"; 6 | import { Toaster } from "react-hot-toast"; 7 | 8 | const inter = Inter({ subsets: ["latin"] }); 9 | 10 | export const metadata: Metadata = { 11 | title: "Create Next App", 12 | description: "Generated by create next app", 13 | }; 14 | 15 | export default function RootLayout({ 16 | children, 17 | }: Readonly<{ 18 | children: React.ReactNode; 19 | }>) { 20 | return ( 21 | 22 | 23 | 24 | 25 | {children} 26 | 27 | 28 | 29 | 30 | ); 31 | } 32 | -------------------------------------------------------------------------------- /app/page.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import EncryptionContainer from "@/components/EncryptionContainer"; 4 | 5 | export default function Home() { 6 | return ( 7 |
8 | 9 |
10 | ); 11 | } 12 | -------------------------------------------------------------------------------- /app/sign-in/[[...sign-in]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignIn } from "@clerk/nextjs"; 2 | 3 | export default function Page() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /app/sign-up/[[...sign-up]]/page.tsx: -------------------------------------------------------------------------------- 1 | import { SignUp } from "@clerk/nextjs"; 2 | 3 | export default function Page() { 4 | return ( 5 |
6 | 7 |
8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /components/EncryptionContainer.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | decryptKey, 3 | encryptKey, 4 | generatePassphrase, 5 | generateSalt, 6 | } from "@/lib/crypto-utils"; 7 | import React, { useEffect, useState } from "react"; 8 | import toast from "react-hot-toast"; 9 | import axios from "axios"; 10 | import { generateAIDadJoke } from "@/lib/openai"; 11 | 12 | // Button styling 13 | const baseButtonStyle = 14 | "rounded-md px-4 py-2 text-green-500 border border-green-500 hover:bg-green-500 hover:text-white focus:outline-none focus:ring"; 15 | const disabledButtonStyle = "hidden"; 16 | 17 | function EncryptionContainer() { 18 | // State 19 | const [saving, setSaving] = useState(false); 20 | const [generatingJoke, setGeneratingJoke] = useState(false); 21 | const [openAIKeyInput, setOpenAIKeyInput] = useState(""); 22 | const [encryptedKey, setEncryptedKey] = useState(""); 23 | const [decryptedKey, setDecryptedKey] = useState(""); 24 | const [passphrase, setPassphrase] = useState(""); 25 | const [salt, setSalt] = useState(""); 26 | const [dadJoke, setDadJoke] = useState(""); 27 | 28 | // Use Effects 29 | useEffect(() => { 30 | const fetchEncryptionKey = async () => { 31 | const response = await axios.get<{ 32 | salt?: string | null; 33 | passphrase?: string | null; 34 | message?: string; 35 | }>("/api/user-encryption"); 36 | 37 | if (response.status !== 200) { 38 | toast.error(response.data.message ?? "Error fetching encrypted key"); 39 | setSalt(""); 40 | setPassphrase(""); 41 | return; 42 | } 43 | 44 | setSalt(response.data.salt ?? ""); 45 | setPassphrase(response.data.passphrase ?? ""); 46 | }; 47 | 48 | const loadLocalStorageKey = () => { 49 | const encryptedKey = localStorage.getItem("openai-key"); 50 | if (encryptedKey) { 51 | setEncryptedKey(encryptedKey); 52 | } 53 | }; 54 | 55 | fetchEncryptionKey(); 56 | loadLocalStorageKey(); 57 | }, []); 58 | 59 | // Handle encryption 60 | const generateRandomCredentials = () => { 61 | const newPassphrase = generatePassphrase(16); // Implement this function in your crypto-utils 62 | const newSalt = generateSalt(); // Implement this function in your crypto-utils 63 | setPassphrase(newPassphrase); 64 | setSalt(newSalt); 65 | }; 66 | 67 | // Function to generate a random passphrase and salt 68 | const handleEncrypt = () => { 69 | try { 70 | const combinedEncryptedData = encryptKey( 71 | openAIKeyInput, 72 | passphrase, 73 | salt 74 | ); 75 | setEncryptedKey(combinedEncryptedData); 76 | } catch (error) { 77 | console.error("Encryption error:", error); 78 | toast.error("Error encrypting key"); 79 | } 80 | }; 81 | 82 | // Handle decryption (for demonstration) 83 | const handleDecrypt = () => { 84 | try { 85 | const decryptedData = decryptKey(encryptedKey, passphrase, salt); 86 | setDecryptedKey(decryptedData); 87 | } catch (error) { 88 | console.error("Decryption error:", error); 89 | toast.error("Error decrypting key"); 90 | } 91 | }; 92 | 93 | const handleSave = async () => { 94 | setSaving(true); 95 | 96 | console.log("Saving key:", encryptedKey); 97 | try { 98 | const response = await axios.post("/api/user-encryption", { 99 | salt, 100 | passphrase, 101 | }); 102 | 103 | if (response.status === 200) { 104 | localStorage.setItem("openai-key", encryptedKey); 105 | toast.success("Saved successfully to database"); 106 | } else { 107 | localStorage.removeItem("openai-key"); 108 | toast.error("Error saving to database"); 109 | } 110 | } catch (error) { 111 | console.error("Save error:", error); 112 | toast.error("Error saving key"); 113 | } finally { 114 | setSaving(false); 115 | } 116 | }; 117 | 118 | const handleGenerateJoke = async () => { 119 | setGeneratingJoke(true); 120 | const newJoke = await generateAIDadJoke(decryptedKey); 121 | setGeneratingJoke(false); 122 | 123 | if (!newJoke) { 124 | toast.error("Error generating dad joke"); 125 | return; 126 | } 127 | 128 | setDadJoke(newJoke); 129 | }; 130 | 131 | return ( 132 |
133 |

API Keys

134 |
135 | 138 | 149 | 156 | 167 | 174 |
175 | 176 |
177 | 180 | setOpenAIKeyInput(e.target.value)} 186 | /> 187 |
188 | {/* Passphrase Display */} 189 |
190 | 191 |

192 | {passphrase || "No passphrase generated"} 193 |

194 |
195 | 196 | {/* Salt Display */} 197 |
198 | 199 |

200 | {salt || "No salt generated"} 201 |

202 |
203 |
204 | 205 |

206 | {encryptedKey || "No encrypted OpenAI Key"} 207 |

208 |
209 |
210 | 211 |

212 | {decryptedKey || "No decrypted OpenAI Key"} 213 |

214 |
215 |
216 | 217 |

218 | {dadJoke || "No dad joke generated."} 219 |

220 |
221 |
222 | ); 223 | } 224 | 225 | export default EncryptionContainer; 226 | -------------------------------------------------------------------------------- /components/Navbar.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { SignInButton, UserButton, useAuth } from "@clerk/nextjs"; 4 | import Link from "next/link"; 5 | import { usePathname } from "next/navigation"; 6 | import React from "react"; 7 | 8 | const routes = [ 9 | { 10 | name: "Secure Chat", 11 | path: "/", 12 | }, 13 | ]; 14 | 15 | function Navbar() { 16 | const pathname = usePathname(); 17 | const { isSignedIn } = useAuth(); 18 | 19 | return ( 20 |
21 | 22 |

Secure OpenAI Keys

23 | 24 |
25 | {routes.map((route, idx) => ( 26 | 33 | {route.name} 34 | 35 | ))} 36 | 37 |
38 | {isSignedIn ? ( 39 |
40 | 41 |
42 | ) : ( 43 |
44 | 45 |
46 | )} 47 |
48 |
49 |
50 | ); 51 | } 52 | 53 | export default Navbar; 54 | -------------------------------------------------------------------------------- /lib/crypto-utils.ts: -------------------------------------------------------------------------------- 1 | import forge from "node-forge"; 2 | 3 | /** 4 | * Generates a random passphrase of the desired length. 5 | * @param {number} length - The desired length of the passphrase. 6 | * @returns {string} The generated passphrase. 7 | */ 8 | export function generatePassphrase(length: number): string { 9 | // Generate random bytes and convert to a base64 string to use as a passphrase 10 | return forge.util.encode64(forge.random.getBytesSync(length)); 11 | } 12 | 13 | /** 14 | * Generates a random salt. 15 | * @returns {string} The generated salt. 16 | */ 17 | export function generateSalt(): string { 18 | // Salts are typically 16 bytes long, but you can adjust the size if needed. 19 | return forge.util.encode64(forge.random.getBytesSync(16)); 20 | } 21 | 22 | /** 23 | * Generates a random key for AES encryption. 24 | * @returns {string} The generated key. 25 | */ 26 | export function generateKey() { 27 | // 32 bytes for AES-256 encryption 28 | return forge.random.getBytesSync(32); 29 | } 30 | 31 | /** 32 | * Encrypts the API key using AES-GCM and returns a combined string of IV and encrypted data. 33 | * The IV is derived from the passphrase and salt, so it does not need to be stored separately. 34 | * @param {string} key - The API key to be encrypted. 35 | * @param {string} passphrase - The passphrase for encryption. 36 | * @param {string} salt - The salt for encryption. 37 | * @returns {string} A combined string of the encrypted data and the tag. 38 | */ 39 | export function encryptKey( 40 | key: string, 41 | passphrase: string, 42 | salt: string 43 | ): string { 44 | const iterations = 10000; // Recommended number of iterations 45 | const keySize = 16; // For AES-256, key size is 32 bytes but 16 bytes for derivedKey is enough 46 | const ivSize = 12; // 12 bytes IV for GCM 47 | 48 | // Derive a key and IV using PBKDF2 49 | // The derivedBytes will be twice as long as needed to get both key and IV 50 | const derivedBytes = forge.pkcs5.pbkdf2( 51 | passphrase, 52 | salt, 53 | iterations, 54 | keySize + ivSize 55 | ); 56 | const derivedKey = derivedBytes.substring(0, keySize); 57 | const iv = derivedBytes.substring(keySize, keySize + ivSize); 58 | 59 | const cipher = forge.cipher.createCipher("AES-GCM", derivedKey); 60 | cipher.start({ iv }); 61 | cipher.update(forge.util.createBuffer(key)); 62 | cipher.finish(); 63 | 64 | const encrypted = cipher.output.getBytes(); 65 | const tag = cipher.mode.tag.getBytes(); 66 | 67 | // Combine encrypted data and tag into a single string and base64 encode it 68 | // No need to store the IV separately as it's derived from the passphrase and salt 69 | return forge.util.encode64(encrypted + tag); 70 | } 71 | 72 | /** 73 | * Decrypts a combined string of encrypted API key and tag. 74 | * @param {string} combined - The combined encrypted API key and tag. 75 | * @param {string} passphrase - The passphrase used for encryption. 76 | * @param {string} salt - The salt used for encryption. 77 | * @returns {string} The decrypted API key. 78 | */ 79 | export function decryptKey( 80 | combined: string, 81 | passphrase: string, 82 | salt: string 83 | ): string { 84 | const combinedBytes = forge.util.decode64(combined); 85 | 86 | // Assume that the tag is the last 16 bytes of the combinedBytes 87 | const encrypted = combinedBytes.substring(0, combinedBytes.length - 16); 88 | const tag = combinedBytes.substring(combinedBytes.length - 16); 89 | 90 | // Derive the key and IV as in the encryption function 91 | const iterations = 10000; 92 | const keySize = 16; 93 | const ivSize = 12; 94 | const derivedBytes = forge.pkcs5.pbkdf2( 95 | passphrase, 96 | salt, 97 | iterations, 98 | keySize + ivSize 99 | ); 100 | const derivedKey = derivedBytes.substring(0, keySize); 101 | const iv = derivedBytes.substring(keySize, keySize + ivSize); 102 | 103 | const decipher = forge.cipher.createDecipher("AES-GCM", derivedKey); 104 | decipher.start({ iv, tag: forge.util.createBuffer(tag) }); 105 | decipher.update(forge.util.createBuffer(encrypted)); 106 | const result = decipher.finish(); // Check 'result' to make sure decryption was successful 107 | 108 | return decipher.output.getBytes(); 109 | } 110 | -------------------------------------------------------------------------------- /lib/openai.ts: -------------------------------------------------------------------------------- 1 | import OpenAI from "openai"; 2 | 3 | const params: OpenAI.Chat.ChatCompletionCreateParams = { 4 | messages: [ 5 | { 6 | role: "user", 7 | content: 8 | "Generate a single dad joke. Do not provide any additional commentary. Only respond with the dad joke. ", 9 | }, 10 | ], 11 | model: "gpt-3.5-turbo", 12 | }; 13 | 14 | export const generateAIDadJoke = async ( 15 | key: string 16 | ): Promise => { 17 | const openai = new OpenAI({ apiKey: key, dangerouslyAllowBrowser: true }); 18 | 19 | const chatCompletion = await openai.chat.completions.create(params); 20 | 21 | console.log(chatCompletion); 22 | 23 | const joke = chatCompletion.choices[0].message.content; 24 | 25 | return joke; 26 | }; 27 | -------------------------------------------------------------------------------- /lib/prismadb.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client"; 2 | 3 | const globalForPrisma = globalThis as unknown as { 4 | prisma: PrismaClient | undefined; 5 | }; 6 | 7 | export const prismadb = 8 | globalForPrisma.prisma ?? 9 | new PrismaClient({ 10 | log: 11 | process.env.NODE_ENV === "development" 12 | ? ["query", "error", "warn"] 13 | : ["error"], 14 | }); 15 | 16 | if (process.env.NODE_ENV !== "production") globalForPrisma.prisma = prismadb; 17 | -------------------------------------------------------------------------------- /middleware.ts: -------------------------------------------------------------------------------- 1 | import { authMiddleware } from "@clerk/nextjs"; 2 | 3 | // This example protects all routes including api/trpc routes 4 | // Please edit this to allow other routes to be public as needed. 5 | // See https://clerk.com/docs/references/nextjs/auth-middleware for more information about configuring your Middleware 6 | export default authMiddleware({ 7 | publicRoutes: ["/"], 8 | }); 9 | 10 | export const config = { 11 | matcher: ["/((?!.+\\.[\\w]+$|_next).*)", "/", "/(api|trpc)(.*)"], 12 | }; 13 | -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "securley-store-keys", 3 | "version": "0.1.0", 4 | "private": true, 5 | "scripts": { 6 | "dev": "next dev", 7 | "build": "next build", 8 | "start": "next start", 9 | "lint": "next lint" 10 | }, 11 | "dependencies": { 12 | "@clerk/nextjs": "^4.29.5", 13 | "ai": "^2.2.31", 14 | "axios": "^1.6.7", 15 | "next": "14.1.0", 16 | "node-forge": "^1.3.1", 17 | "openai": "^4.26.0", 18 | "react": "^18", 19 | "react-dom": "^18", 20 | "react-hot-toast": "^2.4.1" 21 | }, 22 | "devDependencies": { 23 | "@types/node": "^20", 24 | "@types/node-forge": "^1.3.11", 25 | "@types/react": "^18", 26 | "@types/react-dom": "^18", 27 | "autoprefixer": "^10.0.1", 28 | "eslint": "^8", 29 | "eslint-config-next": "14.1.0", 30 | "postcss": "^8", 31 | "tailwindcss": "^3.3.0", 32 | "typescript": "^5" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "cockroachdb" 10 | url = env("DATABASE_URL") 11 | } 12 | 13 | model UserEncryption { 14 | id String @id @default(uuid()) 15 | userId String @unique 16 | passphrase String 17 | salt String 18 | } 19 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss"; 2 | 3 | const config: Config = { 4 | content: [ 5 | "./pages/**/*.{js,ts,jsx,tsx,mdx}", 6 | "./components/**/*.{js,ts,jsx,tsx,mdx}", 7 | "./app/**/*.{js,ts,jsx,tsx,mdx}", 8 | ], 9 | theme: { 10 | extend: { 11 | backgroundImage: { 12 | "gradient-radial": "radial-gradient(var(--tw-gradient-stops))", 13 | "gradient-conic": 14 | "conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))", 15 | }, 16 | }, 17 | }, 18 | plugins: [], 19 | }; 20 | export default config; 21 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "lib": ["dom", "dom.iterable", "esnext"], 4 | "allowJs": true, 5 | "skipLibCheck": true, 6 | "strict": true, 7 | "noEmit": true, 8 | "esModuleInterop": true, 9 | "module": "esnext", 10 | "moduleResolution": "bundler", 11 | "resolveJsonModule": true, 12 | "isolatedModules": true, 13 | "jsx": "preserve", 14 | "incremental": true, 15 | "plugins": [ 16 | { 17 | "name": "next" 18 | } 19 | ], 20 | "paths": { 21 | "@/*": ["./*"] 22 | } 23 | }, 24 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 25 | "exclude": ["node_modules"] 26 | } 27 | --------------------------------------------------------------------------------