├── .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 |
--------------------------------------------------------------------------------