├── .env.sample ├── .eslintrc.json ├── .gitignore ├── README.md ├── next.config.js ├── package-lock.json ├── package.json ├── postcss.config.js ├── prisma └── schema.prisma ├── public ├── next.svg └── vercel.svg ├── src ├── app │ ├── dashboard │ │ ├── layout.tsx │ │ └── page.tsx │ ├── favicon.ico │ ├── globals.css │ ├── layout.tsx │ └── page.tsx ├── components │ └── header.tsx ├── lib │ ├── auth.ts │ └── stripe.ts └── pages │ └── api │ ├── auth │ └── [...nextauth].ts │ └── endpoint.ts ├── tailwind.config.js └── tsconfig.json /.env.sample: -------------------------------------------------------------------------------- 1 | DATABASE_URL='' 2 | 3 | DISCORD_CLIENT_ID="" 4 | DISCORD_CLIENT_SECRET="" 5 | 6 | STRIPE_SECRET="" -------------------------------------------------------------------------------- /.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 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | 27 | # local env files 28 | .env*.local 29 | 30 | # vercel 31 | .vercel 32 | 33 | # typescript 34 | *.tsbuildinfo 35 | next-env.d.ts 36 | -------------------------------------------------------------------------------- /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 | ``` 14 | 15 | Open [http://localhost:3000](http://localhost:3000) with your browser to see the result. 16 | 17 | You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file. 18 | 19 | This project uses [`next/font`](https://nextjs.org/docs/basic-features/font-optimization) to automatically optimize and load Inter, a custom Google Font. 20 | 21 | ## Learn More 22 | 23 | To learn more about Next.js, take a look at the following resources: 24 | 25 | - [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API. 26 | - [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial. 27 | 28 | You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js/) - your feedback and contributions are welcome! 29 | 30 | ## Deploy on Vercel 31 | 32 | 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. 33 | 34 | Check out our [Next.js deployment documentation](https://nextjs.org/docs/deployment) for more details. 35 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | experimental: { 4 | serverActions: true 5 | } 6 | } 7 | 8 | module.exports = nextConfig 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "cool-api", 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 | "@auth/prisma-adapter": "1.0.1", 13 | "@prisma/client": "5.1.0", 14 | "@types/node": "20.4.6", 15 | "@types/react": "18.2.18", 16 | "@types/react-dom": "18.2.7", 17 | "autoprefixer": "10.4.14", 18 | "eslint": "8.46.0", 19 | "eslint-config-next": "13.4.12", 20 | "next": "13.4.12", 21 | "next-auth": "4.22.3", 22 | "postcss": "8.4.27", 23 | "react": "18.2.0", 24 | "react-dom": "18.2.0", 25 | "stripe": "12.16.0", 26 | "tailwindcss": "3.3.3", 27 | "typescript": "5.1.6" 28 | }, 29 | "devDependencies": { 30 | "prisma": "5.1.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | generator client { 2 | provider = "prisma-client-js" 3 | } 4 | 5 | datasource db { 6 | provider = "mysql" 7 | url = env("DATABASE_URL") 8 | relationMode = "prisma" 9 | } 10 | 11 | model Account { 12 | id String @id @default(cuid()) 13 | userId String 14 | type String 15 | provider String 16 | providerAccountId String 17 | refresh_token String? @db.Text 18 | access_token String? @db.Text 19 | expires_at Int? 20 | token_type String? 21 | scope String? 22 | id_token String? @db.Text 23 | session_state String? 24 | 25 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 26 | 27 | @@unique([provider, providerAccountId]) 28 | } 29 | 30 | model Session { 31 | id String @id @default(cuid()) 32 | sessionToken String @unique 33 | userId String 34 | expires DateTime 35 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 36 | } 37 | 38 | model User { 39 | id String @id @default(cuid()) 40 | name String? 41 | email String? @unique 42 | emailVerified DateTime? 43 | image String? 44 | accounts Account[] 45 | sessions Session[] 46 | stripe_customer_id String? 47 | stripe_subscription_item String? 48 | api_key String? 49 | } 50 | 51 | model Log { 52 | id String @id @default(cuid()) 53 | userId String 54 | method String 55 | status Int 56 | created DateTime @default(now()) 57 | } 58 | 59 | model VerificationToken { 60 | identifier String 61 | token String @unique 62 | expires DateTime 63 | 64 | @@unique([identifier, token]) 65 | } 66 | -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/app/dashboard/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Header } from "@/components/header"; 2 | import { mustBeLoggedIn } from "@/lib/auth"; 3 | import { createCheckoutLink, createCustomerIfNull, hasSubscription } from "@/lib/stripe"; 4 | 5 | export default async function DashboardLayout({ 6 | children, 7 | }: { 8 | children: React.ReactNode; 9 | }) { 10 | await mustBeLoggedIn(); 11 | const customer = await createCustomerIfNull(); 12 | 13 | return ( 14 |
15 |
16 |
{children}
17 |
18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/app/dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | createCheckoutLink, 3 | createCustomerIfNull, 4 | hasSubscription, 5 | stripe, 6 | } from "@/lib/stripe"; 7 | import Link from "next/link"; 8 | 9 | import { PrismaClient } from "@prisma/client"; 10 | import { getServerSession } from "next-auth"; 11 | import { authOptions } from "@/pages/api/auth/[...nextauth]"; 12 | const prisma = new PrismaClient(); 13 | 14 | export default async function Page() { 15 | const session = await getServerSession(authOptions); 16 | const customer = await createCustomerIfNull(); 17 | const hasSub = await hasSubscription(); 18 | const checkoutLink = await createCheckoutLink(String(customer)); 19 | 20 | const user = await prisma.user.findFirst({ 21 | where: { 22 | email: session?.user?.email, 23 | }, 24 | }); 25 | 26 | const top10Recentlogs = await prisma.log.findMany({ 27 | where: { 28 | userId: user?.id, 29 | }, 30 | orderBy: { 31 | created: "desc", 32 | }, 33 | take: 10, 34 | }); 35 | 36 | let current_usage = 0; 37 | 38 | if (hasSub) { 39 | const subscriptions = await stripe.subscriptions.list({ 40 | customer: String(user?.stripe_customer_id), 41 | }); 42 | const invoice = await stripe.invoices.retrieveUpcoming({ 43 | subscription: subscriptions.data.at(0)?.id, 44 | }); 45 | 46 | current_usage = invoice.amount_due; 47 | } 48 | 49 | return ( 50 |
51 | {hasSub ? ( 52 | <> 53 |
54 |
55 | You have a subscription! 56 |
57 | 58 |
59 |

60 | Current Usage 61 |

62 |

63 | {current_usage/100} 64 |

65 |
66 | 67 |
68 |

69 | API Key 70 |

71 |

72 | {user?.api_key} 73 |

74 |
75 | 76 |
77 |

78 | Log Events 79 |

80 | {top10Recentlogs.map((item, index) => ( 81 |
82 |

83 | {item.method} 84 |

85 |

86 | {item.status} 87 |

88 |

89 | {item.created.toDateString()} 90 |

91 |
92 | ))} 93 |
94 |
95 | 96 | ) : ( 97 | <> 98 |
99 | 103 | You have no subscription, checkout now! 104 | 105 |
106 | 107 | )} 108 |
109 | ); 110 | } 111 | -------------------------------------------------------------------------------- /src/app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/romanwbruce/api-saas-project/8e57a221d4118dd25884ec0cdabc6d9d3c27593f/src/app/favicon.ico -------------------------------------------------------------------------------- /src/app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import "./globals.css"; 2 | import type { Metadata } from "next"; 3 | import { Poppins } from "next/font/google"; 4 | 5 | const poppins = Poppins({ 6 | subsets: ["latin"], 7 | weight: ["100", "200", "300", "400", "500", "600", "700", "800", "900"], 8 | }); 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 | }: { 18 | children: React.ReactNode; 19 | }) { 20 | return ( 21 | 22 | {children} 23 | 24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /src/app/page.tsx: -------------------------------------------------------------------------------- 1 | import { Header } from '@/components/header' 2 | import Image from 'next/image' 3 | 4 | export default function Home() { 5 | return ( 6 |
7 |
8 |
9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /src/components/header.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link"; 2 | 3 | export function Header() { 4 | return ( 5 | 35 | ); 36 | } 37 | -------------------------------------------------------------------------------- /src/lib/auth.ts: -------------------------------------------------------------------------------- 1 | import { authOptions } from "@/pages/api/auth/[...nextauth]"; 2 | import { getServerSession } from "next-auth"; 3 | import { redirect } from "next/navigation"; 4 | 5 | 6 | export async function mustBeLoggedIn() { 7 | const session = await getServerSession(authOptions); 8 | 9 | if (!session) { 10 | redirect('/api/auth/signin'); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/lib/stripe.ts: -------------------------------------------------------------------------------- 1 | import { authOptions } from '@/pages/api/auth/[...nextauth]'; 2 | import { getServerSession } from 'next-auth'; 3 | import Stripe from 'stripe'; 4 | 5 | import { PrismaClient } from "@prisma/client"; 6 | import { randomUUID } from 'crypto'; 7 | const prisma = new PrismaClient(); 8 | 9 | //price_1NarR3APMZcBliJSoefCKTi5 10 | 11 | export const stripe = new Stripe(String(process.env.STRIPE_SECRET), { 12 | apiVersion: '2022-11-15', 13 | }); 14 | 15 | export async function hasSubscription() { 16 | const session = await getServerSession(authOptions); 17 | 18 | if (session) { 19 | const user = await prisma.user.findFirst({ where: { email: session.user?.email } }); 20 | 21 | const subscriptions = await stripe.subscriptions.list({ 22 | customer: String(user?.stripe_customer_id) 23 | }) 24 | 25 | return subscriptions.data.length > 0; 26 | } 27 | 28 | return false; 29 | } 30 | 31 | export async function createCheckoutLink(customer: string) { 32 | const checkout = await stripe.checkout.sessions.create({ 33 | success_url: "http://localhost:3000/dashboard/billing?success=true", 34 | cancel_url: "http://localhost:3000/dashboard/billing?success=true", 35 | customer: customer, 36 | line_items: [ 37 | { 38 | price: 'price_1NarR3APMZcBliJSoefCKTi5' 39 | } 40 | ], 41 | mode: "subscription" 42 | }) 43 | 44 | return checkout.url; 45 | } 46 | 47 | export async function createCustomerIfNull() { 48 | const session = await getServerSession(authOptions); 49 | 50 | if (session) { 51 | const user = await prisma.user.findFirst({ where: { email: session.user?.email } }); 52 | 53 | if (!user?.api_key) { 54 | await prisma.user.update({ 55 | where: { 56 | id: user?.id 57 | }, 58 | data: { 59 | api_key: "secret_" + randomUUID() 60 | } 61 | }) 62 | } 63 | if (!user?.stripe_customer_id) { 64 | const customer = await stripe.customers.create({ 65 | email: String(user?.email) 66 | }) 67 | 68 | await prisma.user.update({ 69 | where: { 70 | id: user?.id 71 | }, 72 | data: { 73 | stripe_customer_id: customer.id 74 | } 75 | }) 76 | } 77 | const user2 = await prisma.user.findFirst({ where: { email: session.user?.email } }); 78 | return user2?.stripe_customer_id; 79 | } 80 | 81 | } -------------------------------------------------------------------------------- /src/pages/api/auth/[...nextauth].ts: -------------------------------------------------------------------------------- 1 | import NextAuth, { AuthOptions } from "next-auth" 2 | import DiscordProvider from "next-auth/providers/discord" 3 | import { PrismaAdapter } from "@auth/prisma-adapter"; 4 | import { PrismaClient } from "@prisma/client"; 5 | const prisma = new PrismaClient(); 6 | 7 | export const authOptions = { 8 | adapter: PrismaAdapter(prisma), 9 | providers: [ 10 | DiscordProvider({ 11 | clientId: String(process.env.DISCORD_CLIENT_ID), 12 | clientSecret: String(process.env.DISCORD_CLIENT_SECRET), 13 | }), 14 | ], 15 | } as AuthOptions 16 | 17 | export default NextAuth(authOptions) -------------------------------------------------------------------------------- /src/pages/api/endpoint.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest, NextApiResponse } from "next"; 2 | 3 | import { PrismaClient } from "@prisma/client"; 4 | import { stripe } from "@/lib/stripe"; 5 | import { randomUUID } from "crypto"; 6 | const prisma = new PrismaClient(); 7 | 8 | export default async function handler(req: NextApiRequest, res: NextApiResponse) { 9 | 10 | const { api_key } = req.query; 11 | 12 | if (!api_key) { 13 | res.status(401).json({ 14 | error: "Must have a valid api key!" 15 | }) 16 | } 17 | 18 | const user = await prisma.user.findFirst({ 19 | where: { 20 | api_key: String(api_key) 21 | } 22 | }) 23 | 24 | if (!user) { 25 | res.status(401).json({ 26 | error: "There is no user with such api key!" 27 | }) 28 | } 29 | 30 | const customer = await stripe.customers.retrieve(String(user?.stripe_customer_id)); 31 | 32 | const subscriptions = await stripe.subscriptions.list({ 33 | customer: String(user?.stripe_customer_id) 34 | }) 35 | 36 | const item = subscriptions.data.at(0)?.items.data.at(0); 37 | 38 | if (!item) { 39 | res.status(403).json({ 40 | error: "You have no subscription." 41 | }) 42 | } 43 | 44 | await stripe.subscriptionItems.createUsageRecord(String(item?.id), 45 | { 46 | quantity: 1 47 | }) 48 | 49 | 50 | const data = randomUUID(); 51 | 52 | const log = await prisma.log.create({ 53 | data: { 54 | userId: String(user?.id), 55 | status: 200, 56 | method: "GET", 57 | } 58 | }) 59 | 60 | res.status(200).json({ 61 | status: true, 62 | special_key: data, 63 | log: log 64 | }) 65 | } -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | './src/pages/**/*.{js,ts,jsx,tsx,mdx}', 5 | './src/components/**/*.{js,ts,jsx,tsx,mdx}', 6 | './src/app/**/*.{js,ts,jsx,tsx,mdx}', 7 | ], 8 | theme: { 9 | extend: { 10 | backgroundImage: { 11 | 'gradient-radial': 'radial-gradient(var(--tw-gradient-stops))', 12 | 'gradient-conic': 13 | 'conic-gradient(from 180deg at 50% 50%, var(--tw-gradient-stops))', 14 | }, 15 | }, 16 | }, 17 | plugins: [], 18 | } 19 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "bundler", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "plugins": [ 18 | { 19 | "name": "next" 20 | } 21 | ], 22 | "paths": { 23 | "@/*": ["./src/*"] 24 | } 25 | }, 26 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 27 | "exclude": ["node_modules"] 28 | } 29 | --------------------------------------------------------------------------------