├── .eslintrc.json ├── app ├── favicon.ico ├── component │ └── user.tsx ├── api │ ├── auth │ │ ├── [...nextauth] │ │ │ └── route.tsx │ │ ├── reset │ │ │ └── route.tsx │ │ ├── user │ │ │ └── route.ts │ │ ├── verification │ │ │ └── route.tsx │ │ └── new-password │ │ │ └── route.tsx │ ├── resend │ │ └── route.ts │ ├── webhook │ │ ├── route.tsx │ │ └── stripe │ │ │ └── route.ts │ └── stripe │ │ └── route.tsx ├── dashboard │ ├── billing │ │ └── page.tsx │ ├── component │ │ ├── search.tsx │ │ ├── user-menu.tsx │ │ ├── manage-subscription.tsx │ │ └── mail.tsx │ └── page.tsx ├── signup │ └── page.tsx ├── auth │ ├── new-password │ │ └── page.tsx │ ├── new-verification │ │ └── page.tsx │ └── reset │ │ └── page.tsx ├── layout.tsx ├── (main) │ └── page.tsx └── globals.css ├── data ├── formstatus │ └── formStatusInterface.tsx ├── verificationToken │ ├── verificationTokenInterface.tsx │ └── verificationTokenImpl.tsx ├── passwordResetToken │ ├── passwordResetTokenInterface.tsx │ └── passwordResetTokenImpl.tsx ├── user │ ├── userInterface.ts │ └── userImpl.ts ├── subscription │ ├── subscriptionInterface.ts │ └── subscriptionImpl.ts └── mail │ ├── mailInterface.tsx │ └── mailImpl.tsx ├── next.config.mjs ├── postcss.config.js ├── enums └── UserRoleEnum.tsx ├── next.config.js ├── utils ├── stringUtils.ts ├── jsonUtils.ts ├── AllAPIRouteMapping.ts ├── emailTemplates.tsx └── subscriptionPlans.tsx ├── next-auth.d.ts ├── next-auth ├── auth-provider.tsx ├── next-auth.d.ts ├── utils.ts └── config.ts ├── lib ├── utils.ts ├── prisma.ts ├── subscription.tsx ├── resend.ts ├── axios.ts └── stripe.ts ├── components.json ├── schemas └── index.tsx ├── .env.example ├── .gitignore ├── public ├── vercel.svg └── next.svg ├── tsconfig.json ├── .vscode └── launch.json ├── components ├── ui │ ├── label.tsx │ ├── input.tsx │ ├── avatar.tsx │ ├── alert.tsx │ ├── button.tsx │ ├── card.tsx │ ├── form.tsx │ └── dropdown-menu.tsx └── internal │ ├── FormSuccess.tsx │ ├── FormError.tsx │ └── Forms │ ├── NewVerificationForm.tsx │ ├── NewPasswordForm.tsx │ ├── ResetForm.tsx │ ├── SignupForm.tsx │ └── LoginForm.tsx ├── routes.tsx ├── middleware.ts ├── package.json ├── helpers └── errorHandler.tsx ├── controllers ├── PasswordResetTokenController.tsx ├── VerficationTokenController.tsx ├── SubscriptionController.ts └── UserController.ts ├── auth.config.ts ├── config.ts ├── auth.ts ├── prisma └── schema.prisma ├── actions └── login.tsx ├── tailwind.config.ts ├── README.md └── contribution.md /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "next/core-web-vitals" 3 | } 4 | -------------------------------------------------------------------------------- /app/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/buildFast10x/Nextjs-Boilerplate/HEAD/app/favicon.ico -------------------------------------------------------------------------------- /data/formstatus/formStatusInterface.tsx: -------------------------------------------------------------------------------- 1 | interface formStatusInterface { 2 | message?: string 3 | } -------------------------------------------------------------------------------- /next.config.mjs: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = {}; 3 | 4 | export default nextConfig; 5 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | }; 7 | -------------------------------------------------------------------------------- /enums/UserRoleEnum.tsx: -------------------------------------------------------------------------------- 1 | enum UserRoleEnum { 2 | ADMIN = 'ADMIN', 3 | USER = 'USER' 4 | } 5 | 6 | 7 | export default UserRoleEnum; -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | eslint: { 4 | ignoreDuringBuilds: true, 5 | } 6 | } 7 | 8 | module.exports = nextConfig 9 | -------------------------------------------------------------------------------- /data/verificationToken/verificationTokenInterface.tsx: -------------------------------------------------------------------------------- 1 | export default interface verificationTokenInterface { 2 | id: string; 3 | email: string; 4 | token: string; 5 | expires: string 6 | } -------------------------------------------------------------------------------- /data/passwordResetToken/passwordResetTokenInterface.tsx: -------------------------------------------------------------------------------- 1 | export default interface passwordResetTokenInterface { 2 | id: string; 3 | email: string; 4 | token: string; 5 | expires: string 6 | } -------------------------------------------------------------------------------- /app/component/user.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useSession } from "next-auth/react"; 4 | 5 | 6 | export const User = () => { 7 | 8 | const { data: session } = useSession() 9 | return
{JSON.stringify(session)}
10 | 11 | } -------------------------------------------------------------------------------- /app/api/auth/[...nextauth]/route.tsx: -------------------------------------------------------------------------------- 1 | // import { authOptions } from "@/next-auth/config"; 2 | // import NextAuth from "next-auth"; 3 | 4 | // const handler = NextAuth(authOptions); 5 | 6 | // export { handler as GET, handler as POST }; 7 | 8 | 9 | export { GET, POST } from "@/auth" -------------------------------------------------------------------------------- /app/dashboard/billing/page.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from "react"; 2 | import Search from "../component/search"; 3 | 4 | type Props = {} 5 | 6 | export default function page({ }: Props) { 7 | return ( 8 | 9 | 10 | 11 | ) 12 | } -------------------------------------------------------------------------------- /utils/stringUtils.ts: -------------------------------------------------------------------------------- 1 | export default class stringUtils { 2 | constructor() { }; 3 | 4 | static isUndefinedEmptyorNull(data: any) { 5 | if (data === undefined || data === '' || data === null) { 6 | return true; 7 | } 8 | return false; 9 | } 10 | 11 | } -------------------------------------------------------------------------------- /app/signup/page.tsx: -------------------------------------------------------------------------------- 1 | import SignupForm from '@/components/internal/Forms/SignupForm' 2 | import React from 'react' 3 | 4 | export default function Signup() { 5 | return ( 6 |
7 | 8 |
9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /next-auth.d.ts: -------------------------------------------------------------------------------- 1 | import NextAuth, { type DefaultSession } from "next-auth" 2 | import UserRoleEnum from "./enums/UserRoleEnum" 3 | 4 | export type ExtendedUser = DefaultSession["user"] & { 5 | role: string 6 | } 7 | 8 | declare module "next-auth" { 9 | interface Session { 10 | user: ExtendedUser 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /next-auth/auth-provider.tsx: -------------------------------------------------------------------------------- 1 | // "use client"; 2 | 3 | // import { SessionProvider } from "next-auth/react"; 4 | // import React from "react"; 5 | 6 | // type Props = { 7 | // children?: React.ReactNode; 8 | // }; 9 | 10 | // export const AuthProvider = ({ children }: Props) => { 11 | // return {children}; 12 | // }; 13 | -------------------------------------------------------------------------------- /lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | import configEnv from "@/config" 4 | 5 | 6 | export function cn(...inputs: ClassValue[]) { 7 | return twMerge(clsx(inputs)) 8 | } 9 | 10 | export function absoluteUrl(path: string) { 11 | return `${configEnv.app.url || "http://localhost:3000" 12 | }${path}`; 13 | } -------------------------------------------------------------------------------- /data/user/userInterface.ts: -------------------------------------------------------------------------------- 1 | import UserRoleEnum from "@/enums/UserRoleEnum"; 2 | 3 | export default interface userInterface { 4 | name: string, 5 | email: string, 6 | emailVerified: boolean, 7 | password?: string 8 | role?: UserRoleEnum 9 | image?: string 10 | 11 | initFromDataObject(data: any): void; 12 | getEmail(): void; 13 | getId(): void; 14 | toJson(): any; 15 | } -------------------------------------------------------------------------------- /data/subscription/subscriptionInterface.ts: -------------------------------------------------------------------------------- 1 | import userInterface from "../user/userInterface"; 2 | 3 | export default interface subscriptionInterface { 4 | id: string 5 | user: userInterface 6 | stripeCustomerId? : string 7 | stripeSubscriptionId?: string 8 | stripePriceId?: string 9 | stripeCurrentPeriodEnd? : string 10 | isValid?: boolean; 11 | 12 | getStripePriceId(): string; 13 | } -------------------------------------------------------------------------------- /next-auth/next-auth.d.ts: -------------------------------------------------------------------------------- 1 | // import { User } from "next-auth"; 2 | // import { JWT } from "next-auth/jwt"; 3 | 4 | // type UserId = string; 5 | 6 | // declare module "next-auth/jwt" { 7 | // interface JWT { 8 | // id: UserId; 9 | // } 10 | // } 11 | 12 | // declare module "next-auth" { 13 | // interface Session { 14 | // user: User & { 15 | // id: UserId; 16 | // }; 17 | // } 18 | // } 19 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.ts", 8 | "css": "app/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /utils/jsonUtils.ts: -------------------------------------------------------------------------------- 1 | import stringUtils from "./stringUtils"; 2 | 3 | export default class jsonUtilsImpl { 4 | 5 | public static isEmpty(data: any) { 6 | if (!stringUtils.isUndefinedEmptyorNull(data) && Object.keys(data).length !== 0) { 7 | return false; 8 | } 9 | return true; 10 | } 11 | 12 | static toString(json: any) { 13 | return JSON.stringify(json); 14 | } 15 | 16 | } -------------------------------------------------------------------------------- /schemas/index.tsx: -------------------------------------------------------------------------------- 1 | import { z } from "zod"; 2 | 3 | const loginFormSchema = z.object({ 4 | email: z.string().email(), 5 | password: z.string(), 6 | }) 7 | 8 | const resetFormSchema = z.object({ 9 | email: z.string().email() 10 | }) 11 | 12 | const newPasswordSchema = z.object({ 13 | password: z.string() 14 | }) 15 | 16 | 17 | export { 18 | loginFormSchema, 19 | resetFormSchema, 20 | newPasswordSchema 21 | } -------------------------------------------------------------------------------- /app/auth/new-password/page.tsx: -------------------------------------------------------------------------------- 1 | import NewPasswordForm from "@/components/internal/Forms/NewPasswordForm"; 2 | import { Suspense } from "react"; 3 | 4 | const NewPasswordPage = () => { 5 | return ( 6 |
7 | 8 | 9 | 10 |
11 | ); 12 | } 13 | 14 | export default NewPasswordPage; -------------------------------------------------------------------------------- /next-auth/utils.ts: -------------------------------------------------------------------------------- 1 | import {auth} from "@/auth" 2 | 3 | // export type AuthSession = { 4 | // session: { 5 | // user: { 6 | // id: string; 7 | // name?: string; 8 | // email?: string; 9 | // image?: string; 10 | // }; 11 | // } | null; 12 | // }; 13 | 14 | export const getCurrentUser = async () => { 15 | const session = await auth(); 16 | // console.log(session); 17 | return session; 18 | }; 19 | -------------------------------------------------------------------------------- /app/auth/new-verification/page.tsx: -------------------------------------------------------------------------------- 1 | import NewVerificationForm from "@/components/internal/Forms/NewVerificationForm"; 2 | import { Suspense } from "react"; 3 | 4 | const NewVerificationPage = () => { 5 | return ( 6 |
7 | 8 | 9 | 10 |
11 | ); 12 | } 13 | 14 | export default NewVerificationPage; -------------------------------------------------------------------------------- /app/auth/reset/page.tsx: -------------------------------------------------------------------------------- 1 | import ResetForm from "@/components/internal/Forms/ResetForm"; 2 | import { Suspense } from "react"; 3 | 4 | export default async function ResetPage() { 5 | 6 | return ( 7 |
8 | {/* Login */} 9 | 10 | 11 | 12 |
13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /data/mail/mailInterface.tsx: -------------------------------------------------------------------------------- 1 | export default interface mailInterface { 2 | name: string; 3 | email: string; 4 | from: string; 5 | subject: string; 6 | text?: string; 7 | html?: string; 8 | 9 | getFrom(): string; 10 | 11 | setSubject(subject: string): void; 12 | getSubject(): string; 13 | 14 | setEmail(email: string): void; 15 | getEmail(): string; 16 | 17 | getName(): string; 18 | getText(): string; 19 | 20 | setHTML(html: string): void; 21 | getHTML(): string; 22 | 23 | } -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_ENV="dev" 2 | NEXT_PUBLIC_DEV_API_URL="http://localhost:3000" 3 | DEV_NEXTAUTH_SECRET="" 4 | NEXTAUTH_SECRET="" 5 | DEV_NEXTAUTH_URL="http://localhost:3000" 6 | DEV_DATABASE_URL="" 7 | 8 | DEV_GOOGLE_CLIENT_ID="" 9 | DEV_GOOGLE_CLIENT_SECRET="" 10 | 11 | DEV_STRIPE_SECRET_KEY="" 12 | DEV_STRIPE_WEBHOOK_SECRET="" 13 | DEV_NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY="" 14 | 15 | DEV_NEXT_PUBLIC_STRIPE_PRO_PRICE_ID="" 16 | DEV_NEXT_PUBLIC_STRIPE_MAX_PRICE_ID="" 17 | DEV_NEXT_PUBLIC_STRIPE_ULTRA_PRICE_ID="" 18 | DEV_RESEND_API_KEY="" -------------------------------------------------------------------------------- /.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 30 | .env*.local 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /lib/prisma.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client'; 2 | import configEnv from "@/config" 3 | 4 | declare global { 5 | var prisma: PrismaClient | undefined; 6 | } 7 | 8 | const prisma = globalThis.prisma || new PrismaClient(); 9 | 10 | if (configEnv.env !== 'production') globalThis.prisma = prisma; 11 | 12 | export default prisma; 13 | 14 | 15 | // import { PrismaClient } from "@prisma/client" 16 | 17 | // const globalForPrisma = global as unknown as { prisma: PrismaClient } 18 | 19 | // export const prisma = 20 | // globalForPrisma.prisma || 21 | // new PrismaClient({ 22 | // log: ['query'] 23 | // }) 24 | 25 | 26 | // if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma -------------------------------------------------------------------------------- /utils/AllAPIRouteMapping.ts: -------------------------------------------------------------------------------- 1 | const AllAPIRouteMapping = { 2 | users: { 3 | add: { 4 | apiPath: "/api/auth/user", 5 | method: "POST" 6 | }, 7 | verify: { 8 | apiPath: "/api/auth/verification", 9 | method: "POST" 10 | }, 11 | resetPasswordMail: { 12 | apiPath: "/api/auth/reset", 13 | method: "POST" 14 | }, 15 | updatePassword: { 16 | apiPath: "/api/auth/new-password", 17 | method: "POST" 18 | } 19 | }, 20 | mails: { 21 | send: { 22 | apiPath: "/api/resend", 23 | method: "POST" 24 | } 25 | } 26 | } 27 | 28 | export default AllAPIRouteMapping; -------------------------------------------------------------------------------- /app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next"; 2 | import { Inter } from "next/font/google"; 3 | import "./globals.css"; 4 | 5 | // import { AuthProvider } from "@/next-auth/auth-provider"; 6 | import { Toaster } from "sonner"; 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 | {children} 24 | {/* */} 25 | 26 | 27 | 28 | ); 29 | } 30 | -------------------------------------------------------------------------------- /app/dashboard/component/search.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { AlertCircle, CheckCircle } from "lucide-react"; 4 | import { useSearchParams } from "next/navigation" 5 | 6 | type Props = {} 7 | 8 | export default function Search({ }: Props) { 9 | const query = useSearchParams(); 10 | if (query.get('success')) { 11 | return (
12 | 13 |

Payment Success

14 |
) 15 | } 16 | return ( 17 | 18 |
19 | 20 |

Payment Failed

21 |
22 | ) 23 | } -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "name": "Next.js: debug server-side", 6 | "type": "node-terminal", 7 | "request": "launch", 8 | "command": "next dev" 9 | }, 10 | { 11 | "name": "Next.js: debug client-side", 12 | "type": "pwa-chrome", 13 | "request": "launch", 14 | "url": "http://localhost:3000" 15 | }, 16 | { 17 | "name": "Next.js: debug full stack", 18 | "type": "node-terminal", 19 | "request": "launch", 20 | "command": "next dev", 21 | "console": "integratedTerminal", 22 | "serverReadyAction": { 23 | "pattern": "started server on .+, url: (https?://.+)", 24 | "uriFormat": "%s", 25 | "action": "debugWithChrome" 26 | } 27 | } 28 | ] 29 | } -------------------------------------------------------------------------------- /app/(main)/page.tsx: -------------------------------------------------------------------------------- 1 | import LoginForm from "@/components/internal/Forms/LoginForm"; 2 | import { Button, buttonVariants } from "@/components/ui/button"; 3 | import { cn } from "@/lib/utils"; 4 | import { getCurrentUser } from "@/next-auth/utils"; 5 | import Link from "next/link"; 6 | import { redirect } from "next/navigation"; 7 | import { Suspense } from "react"; 8 | 9 | export default async function Home() { 10 | const session = await getCurrentUser(); 11 | 12 | if (session) { 13 | redirect('/dashboard') 14 | } 15 | 16 | 17 | return ( 18 |
19 | {/* Login */} 20 | 21 | 22 | 23 |
24 | ); 25 | } 26 | -------------------------------------------------------------------------------- /components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | import { cva, type VariantProps } from "class-variance-authority" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | const labelVariants = cva( 10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70" 11 | ) 12 | 13 | const Label = React.forwardRef< 14 | React.ElementRef, 15 | React.ComponentPropsWithoutRef & 16 | VariantProps 17 | >(({ className, ...props }, ref) => ( 18 | 23 | )) 24 | Label.displayName = LabelPrimitive.Root.displayName 25 | 26 | export { Label } 27 | -------------------------------------------------------------------------------- /components/internal/FormSuccess.tsx: -------------------------------------------------------------------------------- 1 | import { ExclamationTriangleIcon } from "@radix-ui/react-icons" 2 | 3 | import { 4 | Alert, 5 | AlertDescription, 6 | AlertTitle, 7 | } from "@/components/ui/alert" 8 | import stringUtils from "@/utils/stringUtils" 9 | 10 | export function FormSuccess(props: formStatusInterface) { 11 | return ( 12 | <> 13 | { 14 | !stringUtils.isUndefinedEmptyorNull(props.message) ? 15 | 16 | 17 | Success 18 | 19 | {props.message} 20 | 21 | 22 | : 23 | <> 24 | } 25 | 26 | 27 | ) 28 | } 29 | -------------------------------------------------------------------------------- /components/internal/FormError.tsx: -------------------------------------------------------------------------------- 1 | import { ExclamationTriangleIcon } from "@radix-ui/react-icons" 2 | 3 | import { 4 | Alert, 5 | AlertDescription, 6 | AlertTitle, 7 | } from "@/components/ui/alert" 8 | import stringUtils from "@/utils/stringUtils" 9 | 10 | export function FormError(props: formStatusInterface) { 11 | return ( 12 | <> 13 | { 14 | !stringUtils.isUndefinedEmptyorNull(props.message) ? 15 | 16 | 17 | Error 18 | 19 | {props.message} 20 | 21 | 22 | : 23 | <> 24 | } 25 | 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | export interface InputProps 6 | extends React.InputHTMLAttributes {} 7 | 8 | const Input = React.forwardRef( 9 | ({ className, type, ...props }, ref) => { 10 | return ( 11 | 20 | ) 21 | } 22 | ) 23 | Input.displayName = "Input" 24 | 25 | export { Input } 26 | -------------------------------------------------------------------------------- /routes.tsx: -------------------------------------------------------------------------------- 1 | /** 2 | * An array of routes that are accessible to the public 3 | * These routes do not require authentication 4 | * @type {string[]} 5 | */ 6 | export const publicRoutes = [ 7 | "/", 8 | "/signup", 9 | "/auth/new-verification" 10 | ]; 11 | 12 | /** 13 | * An array of routes that are used for authentication 14 | * These routes will redirect logged in users to /settings 15 | * @type {string[]} 16 | */ 17 | export const authRoutes = [ 18 | "/auth/login", 19 | "/api/user", 20 | "/auth/reset", 21 | "/auth/new-password" 22 | ]; 23 | 24 | /** 25 | * The prefix for API authentication routes 26 | * Routes that start with this prefix are used for API authentication purposes 27 | * @type {string} 28 | */ 29 | export const apiAuthPrefix = "/api/auth"; 30 | 31 | /** 32 | * The default redirect path after logging in 33 | * @type {string} 34 | */ 35 | export const DEFAULT_LOGIN_REDIRECT = "/dashboard"; -------------------------------------------------------------------------------- /lib/subscription.tsx: -------------------------------------------------------------------------------- 1 | import { getCurrentUser } from "@/next-auth/utils"; 2 | 3 | import SubscriptionController from "@/controllers/SubscriptionController"; 4 | import subscriptionImpl from "@/data/subscription/subscriptionImpl"; 5 | 6 | export const checkSubscription = async () => { 7 | const session = await getCurrentUser(); 8 | 9 | if (!session) { 10 | return false; 11 | } 12 | 13 | const subscriptionControllerHandler = new SubscriptionController(); 14 | const userSubscriptionDB = await subscriptionControllerHandler.getSubscription(session?.user?.id || ''); 15 | 16 | const userSubscription = new subscriptionImpl(); 17 | userSubscription.initFromDataObject(userSubscriptionDB, session); 18 | 19 | if (!userSubscription.getStripeCustomerId()) { 20 | return false; 21 | } 22 | 23 | userSubscription.isSubsciptionValid(); 24 | let returnJSON: any = userSubscription.toJson(); 25 | return returnJSON; 26 | }; 27 | 28 | export default checkSubscription; -------------------------------------------------------------------------------- /app/api/resend/route.ts: -------------------------------------------------------------------------------- 1 | import mailImpl from "@/data/mail/mailImpl"; 2 | import errorHandler from "@/helpers/errorHandler"; 3 | import resendInstance from "@/lib/resend"; 4 | import { NextResponse } from "next/server"; 5 | 6 | export async function POST(request: Request) { 7 | const body = await request.json(); 8 | 9 | const mail = new mailImpl(); 10 | mail.initFromDataObject(body); 11 | mail.setFrom("noreply "); 12 | mail.setSubject("Test Mail"); 13 | mail.setText("This is a test mail. Thanks for using Nextjs Boilerplate build by Buildfast."); 14 | 15 | const { name, email } = body; 16 | 17 | try { 18 | mail.setEmail(email); 19 | const resend = new resendInstance(); 20 | const { data, error } = await resend.sendMail(mail) 21 | if (error) { 22 | const errorHandlerInstance = new errorHandler(); 23 | errorHandlerInstance.internalServerError(error); 24 | return errorHandlerInstance.generateError(); 25 | } 26 | return NextResponse.json(data); 27 | } catch (e: any) { 28 | const error = new errorHandler(); 29 | error.internalServerError(e); 30 | return error.generateError(); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /middleware.ts: -------------------------------------------------------------------------------- 1 | import NextAuth from "next-auth"; 2 | 3 | import authConfig from "@/auth.config"; 4 | import { 5 | DEFAULT_LOGIN_REDIRECT, 6 | apiAuthPrefix, 7 | authRoutes, 8 | publicRoutes, 9 | } from "@/routes"; 10 | 11 | const { auth } = NextAuth(authConfig); 12 | 13 | export default auth((req: any): any => { 14 | const { nextUrl } = req; 15 | const isLoggedIn = !!req.auth; 16 | 17 | const isApiAuthRoute = nextUrl.pathname.startsWith(apiAuthPrefix); 18 | const isPublicRoute = publicRoutes.includes(nextUrl.pathname); 19 | const isAuthRoute = authRoutes.includes(nextUrl.pathname); 20 | 21 | if (isApiAuthRoute) { 22 | return null; 23 | } 24 | 25 | if (isAuthRoute) { 26 | if (isLoggedIn) { 27 | return Response.redirect(new URL(DEFAULT_LOGIN_REDIRECT, nextUrl)) 28 | } 29 | return null; 30 | } 31 | 32 | if (!isLoggedIn && !isPublicRoute) { 33 | return Response.redirect(new URL("/", nextUrl)); 34 | } 35 | 36 | return null; 37 | }) 38 | 39 | // Optionally, don't invoke Middleware on some paths 40 | export const config = { 41 | matcher: ['/((?!.+\\.[\\w]+$|_next).*)', '/', '/(api|trpc)(.*)'], 42 | } -------------------------------------------------------------------------------- /utils/emailTemplates.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react"; 2 | 3 | interface EmailTemplateProps { 4 | firstName: string; 5 | } 6 | 7 | export const EmailTemplate: React.FC> = ({ 8 | firstName, 9 | }) => ( 10 |
11 |

Welcome, {firstName}!

12 |

13 | Lorem ipsum dolor sit amet, officia excepteur ex fugiat reprehenderit enim 14 | labore culpa sint ad nisi Lorem pariatur mollit ex esse exercitation amet. 15 | Nisi anim cupidatat excepteur officia. Reprehenderit nostrud nostrud ipsum 16 | Lorem est aliquip amet voluptate voluptate dolor minim nulla est proident. 17 | Nostrud officia pariatur ut officia. Sit irure elit esse ea nulla sunt ex 18 | occaecat reprehenderit commodo officia dolor Lorem duis laboris cupidatat 19 | officia voluptate. Culpa proident adipisicing id nulla nisi laboris ex in 20 | Lorem sunt duis officia eiusmod. Aliqua reprehenderit commodo ex non 21 | excepteur duis sunt velit enim. Voluptate laboris sint cupidatat ullamco 22 | ut ea consectetur et est culpa et culpa duis. 23 |

24 |
25 |

Sent with help from Resend and Kirimase 😊

26 |
27 | ); 28 | -------------------------------------------------------------------------------- /app/dashboard/component/user-menu.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar' 3 | import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu' 4 | import { LogOut } from 'lucide-react' 5 | import { User } from 'next-auth' 6 | import { signOut } from 'next-auth/react' 7 | 8 | 9 | type Props = { 10 | user: User 11 | } 12 | 13 | export default function UserMenu({ user }: Props) { 14 | return ( 15 | 16 | 17 | 18 | 19 | {user.name?.slice(0, 2)} 20 | 21 | 22 | 23 | await signOut()}> 24 | Logout 25 | 26 | 27 | 28 | ) 29 | } -------------------------------------------------------------------------------- /public/next.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /utils/subscriptionPlans.tsx: -------------------------------------------------------------------------------- 1 | export type subscriptionPlanInterface = { 2 | id: string; 3 | name: string; 4 | description: string; 5 | stripePriceId: string; 6 | price: number; 7 | features: Array; 8 | } 9 | 10 | 11 | 12 | export const subscriptionPlansData: subscriptionPlanInterface[] = [ 13 | { 14 | id: "pro", 15 | name: "Pro", 16 | description: "Pro tier that offers x, y, and z features.", 17 | stripePriceId: process.env.NEXT_PUBLIC_STRIPE_PRO_PRICE_ID ?? "", 18 | price: 19, 19 | features: ["Feature 1", "Feature 2", "Feature 3"], 20 | }, 21 | { 22 | id: "max", 23 | name: "Max", 24 | description: "Super Pro tier that offers x, y, and z features.", 25 | stripePriceId: process.env.NEXT_PUBLIC_STRIPE_MAX_PRICE_ID ?? "", 26 | price: 39, 27 | features: ["Feature 1", "Feature 2", "Feature 3"], 28 | }, 29 | { 30 | id: "ultra", 31 | name: "Ultra", 32 | description: "Ultra Pro tier that offers x, y, and z features.", 33 | stripePriceId: process.env.NEXT_PUBLIC_STRIPE_ULTRA_PRICE_ID ?? "", 34 | price: 59, 35 | features: ["Feature 1", "Feature 2", "Feature 3"], 36 | }, 37 | ]; 38 | 39 | async function getPlanByStripePriceId(stripePriceId: string) { 40 | return await subscriptionPlansData.find( 41 | (plan) => plan.stripePriceId === stripePriceId) 42 | } -------------------------------------------------------------------------------- /components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | const Avatar = React.forwardRef< 9 | React.ElementRef, 10 | React.ComponentPropsWithoutRef 11 | >(({ className, ...props }, ref) => ( 12 | 20 | )) 21 | Avatar.displayName = AvatarPrimitive.Root.displayName 22 | 23 | const AvatarImage = React.forwardRef< 24 | React.ElementRef, 25 | React.ComponentPropsWithoutRef 26 | >(({ className, ...props }, ref) => ( 27 | 32 | )) 33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName 34 | 35 | const AvatarFallback = React.forwardRef< 36 | React.ElementRef, 37 | React.ComponentPropsWithoutRef 38 | >(({ className, ...props }, ref) => ( 39 | 47 | )) 48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName 49 | 50 | export { Avatar, AvatarImage, AvatarFallback } 51 | -------------------------------------------------------------------------------- /app/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | 5 | @layer base { 6 | :root { 7 | --background: 0 0% 100%; 8 | --foreground: 0 0% 3.9%; 9 | 10 | --card: 0 0% 100%; 11 | --card-foreground: 0 0% 3.9%; 12 | 13 | --popover: 0 0% 100%; 14 | --popover-foreground: 0 0% 3.9%; 15 | 16 | --primary: 0 0% 9%; 17 | --primary-foreground: 0 0% 98%; 18 | 19 | --secondary: 0 0% 96.1%; 20 | --secondary-foreground: 0 0% 9%; 21 | 22 | --muted: 0 0% 96.1%; 23 | --muted-foreground: 0 0% 45.1%; 24 | 25 | --accent: 0 0% 96.1%; 26 | --accent-foreground: 0 0% 9%; 27 | 28 | --destructive: 0 84.2% 60.2%; 29 | --destructive-foreground: 0 0% 98%; 30 | 31 | --border: 0 0% 89.8%; 32 | --input: 0 0% 89.8%; 33 | --ring: 0 0% 3.9%; 34 | 35 | --radius: 0.5rem; 36 | } 37 | 38 | .dark { 39 | --background: 0 0% 3.9%; 40 | --foreground: 0 0% 98%; 41 | 42 | --card: 0 0% 3.9%; 43 | --card-foreground: 0 0% 98%; 44 | 45 | --popover: 0 0% 3.9%; 46 | --popover-foreground: 0 0% 98%; 47 | 48 | --primary: 0 0% 98%; 49 | --primary-foreground: 0 0% 9%; 50 | 51 | --secondary: 0 0% 14.9%; 52 | --secondary-foreground: 0 0% 98%; 53 | 54 | --muted: 0 0% 14.9%; 55 | --muted-foreground: 0 0% 63.9%; 56 | 57 | --accent: 0 0% 14.9%; 58 | --accent-foreground: 0 0% 98%; 59 | 60 | --destructive: 0 62.8% 30.6%; 61 | --destructive-foreground: 0 0% 98%; 62 | 63 | --border: 0 0% 14.9%; 64 | --input: 0 0% 14.9%; 65 | --ring: 0 0% 83.1%; 66 | } 67 | } 68 | 69 | @layer base { 70 | * { 71 | @apply border-border; 72 | } 73 | body { 74 | @apply bg-background text-foreground; 75 | } 76 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-project", 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 | "push": "prisma db push", 11 | "gen": "prisma generate" 12 | }, 13 | "prisma": { 14 | "seed": "ts-node --compiler-options {\"module\":\"CommonJS\"} prisma/seed.ts" 15 | }, 16 | "dependencies": { 17 | "@auth/prisma-adapter": "^1.5.1", 18 | "@hookform/resolvers": "^3.3.4", 19 | "@prisma/client": "^5.9.1", 20 | "@radix-ui/react-avatar": "^1.0.4", 21 | "@radix-ui/react-dropdown-menu": "^2.0.6", 22 | "@radix-ui/react-icons": "^1.3.0", 23 | "@radix-ui/react-label": "^2.0.2", 24 | "@radix-ui/react-slot": "^1.0.2", 25 | "axios": "^1.6.8", 26 | "bcryptjs": "^2.4.3", 27 | "class-variance-authority": "^0.7.0", 28 | "clsx": "^2.1.0", 29 | "lucide-react": "^0.321.0", 30 | "next": "14.1.0", 31 | "next-auth": "^5.0.0-beta.16", 32 | "react": "^18", 33 | "react-dom": "^18", 34 | "react-hook-form": "^7.51.0", 35 | "resend": "^3.1.0", 36 | "sonner": "^1.4.0", 37 | "stripe": "^14.14.0", 38 | "tailwind-merge": "^2.2.1", 39 | "tailwindcss-animate": "^1.0.7", 40 | "uuid": "^9.0.1", 41 | "zod": "^3.22.4" 42 | }, 43 | "devDependencies": { 44 | "@types/bcrypt": "^5.0.2", 45 | "@types/bcryptjs": "^2.4.6", 46 | "@types/node": "^20.11.16", 47 | "@types/react": "^18", 48 | "@types/react-dom": "^18", 49 | "@types/uuid": "^9.0.8", 50 | "autoprefixer": "^10.0.1", 51 | "eslint": "^8", 52 | "eslint-config-next": "14.1.0", 53 | "postcss": "^8", 54 | "prisma": "^5.9.1", 55 | "tailwindcss": "^3.3.0", 56 | "ts-node": "^10.9.2", 57 | "typescript": "^5.3.3" 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /components/ui/alert.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { cva, type VariantProps } from "class-variance-authority" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | const alertVariants = cva( 7 | "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7", 8 | { 9 | variants: { 10 | variant: { 11 | default: "bg-background text-foreground", 12 | destructive: 13 | "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", 14 | }, 15 | }, 16 | defaultVariants: { 17 | variant: "default", 18 | }, 19 | } 20 | ) 21 | 22 | const Alert = React.forwardRef< 23 | HTMLDivElement, 24 | React.HTMLAttributes & VariantProps 25 | >(({ className, variant, ...props }, ref) => ( 26 |
32 | )) 33 | Alert.displayName = "Alert" 34 | 35 | const AlertTitle = React.forwardRef< 36 | HTMLParagraphElement, 37 | React.HTMLAttributes 38 | >(({ className, ...props }, ref) => ( 39 |
44 | )) 45 | AlertTitle.displayName = "AlertTitle" 46 | 47 | const AlertDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |
56 | )) 57 | AlertDescription.displayName = "AlertDescription" 58 | 59 | export { Alert, AlertTitle, AlertDescription } 60 | -------------------------------------------------------------------------------- /app/api/auth/reset/route.tsx: -------------------------------------------------------------------------------- 1 | import userController from "@/controllers/UserController"; 2 | import mailImpl from "@/data/mail/mailImpl"; 3 | import passwordResetTokenImpl from "@/data/passwordResetToken/passwordResetTokenImpl"; 4 | import userImpl from "@/data/user/userImpl"; 5 | import errorHandler from "@/helpers/errorHandler"; 6 | import resendInstance from "@/lib/resend"; 7 | import { NextRequest, NextResponse } from "next/server"; 8 | 9 | export async function POST(req: NextRequest) { 10 | try { 11 | const { email } = await req.json(); 12 | 13 | const userControllerHandler = new userController(); 14 | const existingUser = await userControllerHandler.getUserByEmail(email); 15 | 16 | if (!existingUser) { 17 | const error = new errorHandler(); 18 | error.notFoundWithMessage("User does not exist"); 19 | return error.generateError(); 20 | } 21 | 22 | const userForm = new userImpl(); 23 | userForm.initFromDataObject(existingUser); 24 | 25 | // TODO: Generate Token and send email 26 | const passwordResetTokenForm = new passwordResetTokenImpl(); 27 | await passwordResetTokenForm.generateVerificationToken(userForm.getEmail()); 28 | 29 | const mail = new mailImpl(); 30 | mail.populateCredentials() 31 | 32 | const resend = new resendInstance(); 33 | resend.sendPasswordResetMail(userForm.getEmail(), passwordResetTokenForm.getToken(), mail); 34 | 35 | const returnJson: any = { 36 | status: 200, 37 | message: "Reset Email sent", 38 | success: true 39 | } 40 | return NextResponse.json(returnJson); 41 | 42 | } catch (e: any) { 43 | const error = new errorHandler(); 44 | error.internalServerError(e); 45 | return error.generateError(); 46 | } 47 | } -------------------------------------------------------------------------------- /app/api/webhook/route.tsx: -------------------------------------------------------------------------------- 1 | import Stripe from "stripe" 2 | import { headers } from "next/headers" 3 | import { NextRequest, NextResponse } from "next/server" 4 | 5 | import prismadb from "@/lib/prisma" 6 | import stripeInstance from "@/lib/stripe" 7 | import errorHandler from "@/helpers/errorHandler" 8 | import SubscriptionController from "@/controllers/SubscriptionController" 9 | 10 | export async function POST(req: NextRequest) { 11 | const body = await req.text() 12 | const signature = headers().get("Stripe-Signature") as string 13 | 14 | let event: Stripe.Event 15 | 16 | const stripe = new stripeInstance(); 17 | 18 | try { 19 | event = await stripe.webhookEvent(body, signature) 20 | } catch (e: any) { 21 | const error = new errorHandler(); 22 | error.internalServerError(e); 23 | return error.generateError(); 24 | } 25 | 26 | const session = event.data.object as Stripe.Checkout.Session 27 | 28 | // ÇHECKOUT COMPLETED 29 | if (event.type === "checkout.session.completed") { 30 | const subscription = await stripe.retriveSubscription(session); 31 | 32 | if (!session?.metadata?.userId) { 33 | const error = new errorHandler(); 34 | error.missingItem("User Id is required"); 35 | return error.generateError(); 36 | } 37 | 38 | const subscriptionControllerHandler = new SubscriptionController(); 39 | await subscriptionControllerHandler.create(session?.metadata?.userId, subscription) 40 | } 41 | 42 | // PAYMENT SUCCEDDED 43 | if (event.type === "invoice.payment_succeeded") { 44 | const subscription = await stripe.retriveSubscription(session); 45 | 46 | const subscriptionControllerHandler = new SubscriptionController(); 47 | await subscriptionControllerHandler.update(subscription); 48 | } 49 | 50 | } -------------------------------------------------------------------------------- /helpers/errorHandler.tsx: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | 3 | interface errorInterfance { 4 | status: number; 5 | message: string; 6 | success: boolean; 7 | } 8 | 9 | 10 | export default class errorHandler implements errorInterfance { 11 | status: number = 500; 12 | message: string = ""; 13 | success: boolean = false; 14 | 15 | internalServerError(e: any) { 16 | this.status = 500; 17 | this.message = e.message; 18 | this.success = false; 19 | 20 | } 21 | 22 | cookieError() { 23 | this.status = 403; 24 | this.message = "Cookie Absent/Expired, Please Sign in Again"; 25 | this.success = false; 26 | } 27 | 28 | invalidAccessToken(msg: string) { 29 | this.status = 401; 30 | this.message = msg; 31 | this.success = false; 32 | } 33 | 34 | missingItem(msg: string) { 35 | this.status = 400; 36 | this.message = msg; 37 | this.success = false; 38 | } 39 | 40 | noAuthenticationTokenError(msg: string) { 41 | this.status = 201; 42 | this.message = msg; 43 | this.success = false; 44 | } 45 | 46 | unAuthorizedAccess() { 47 | this.status = 401; 48 | this.message = "Unauthorized Access"; 49 | this.success = false; 50 | } 51 | 52 | notFound() { 53 | this.status = 404; 54 | this.message = "Not Found"; 55 | this.success = false; 56 | } 57 | 58 | notFoundWithMessage(msg: string) { 59 | this.status = 404; 60 | this.message = msg; 61 | this.success = false; 62 | } 63 | 64 | conflict(msg: string) { 65 | this.status = 409; 66 | this.message = msg; 67 | this.success = false; 68 | } 69 | 70 | generateError() { 71 | return NextResponse.json({ error: this.message, "success": this.success }, { "status": this.status }); 72 | } 73 | 74 | } -------------------------------------------------------------------------------- /lib/resend.ts: -------------------------------------------------------------------------------- 1 | import { Resend } from "resend"; 2 | import { EmailTemplate } from "@/utils/emailTemplates"; 3 | import mailInterface from "@/data/mail/mailInterface"; 4 | import configEnv from "@/config" 5 | 6 | export default class resendInstance { 7 | resend: any; 8 | 9 | constructor() { 10 | this.resend = new Resend(configEnv.resend.apiKey); 11 | } 12 | 13 | async sendMail(mailObj: mailInterface) { 14 | const mail = await this.resend.emails.send({ 15 | from: mailObj.getFrom(), 16 | to: mailObj.getEmail(), 17 | subject: mailObj.getSubject(), 18 | text: mailObj.getText(), 19 | }); 20 | return mail; 21 | } 22 | 23 | async sendHTMLMail(mailObj: mailInterface) { 24 | const mail = await this.resend.emails.send({ 25 | from: mailObj.getFrom(), 26 | to: mailObj.getEmail(), 27 | subject: mailObj.getSubject(), 28 | html: mailObj.getHTML(), 29 | }); 30 | return mail; 31 | } 32 | 33 | async sendVerificationEmail(email: string, token: string, mailObj: mailInterface) { 34 | const confirmLink = `${configEnv.app.url}/auth/new-verification?token=${token}`; 35 | mailObj.setEmail(email); 36 | mailObj.setSubject("Confirm your email"); 37 | mailObj.setHTML(`

Click here to confirm email.

`); 38 | const result = await this.sendHTMLMail(mailObj); 39 | return result; 40 | } 41 | 42 | async sendPasswordResetMail(email: string, token: string, mailObj: mailInterface) { 43 | const confirmLink = `${configEnv.app.url}/auth/new-password?token=${token}`; 44 | mailObj.setEmail(email); 45 | mailObj.setSubject("Reset Your Password"); 46 | mailObj.setHTML(`

Click here to confirm email.

`); 47 | this.sendHTMLMail(mailObj); 48 | 49 | } 50 | 51 | } 52 | -------------------------------------------------------------------------------- /app/api/stripe/route.tsx: -------------------------------------------------------------------------------- 1 | import { NextRequest, NextResponse } from "next/server"; 2 | 3 | import prisma from "@/lib/prisma"; 4 | import stripeInstance from "@/lib/stripe"; 5 | import { absoluteUrl } from "@/lib/utils"; 6 | import errorHandler from "@/helpers/errorHandler"; 7 | import { getCurrentUser } from "@/next-auth/utils"; 8 | import SubscriptionController from "@/controllers/SubscriptionController"; 9 | import subscriptionImpl from "@/data/subscription/subscriptionImpl"; 10 | 11 | const dashboardUrl = absoluteUrl("/dashboard"); 12 | 13 | export async function POST(req: NextRequest) { 14 | try { 15 | const user = await getCurrentUser(); 16 | const { stripePriceId } = await req.json(); 17 | 18 | if (!user || !user.user) { 19 | const error = new errorHandler(); 20 | error.notFound(); 21 | return error.generateError(); 22 | } 23 | 24 | const subscriptionControllerHandler = new SubscriptionController(); 25 | const userSubscriptionDB = await subscriptionControllerHandler.findUnique(user?.user?.id || ''); 26 | 27 | const userSubscription = new subscriptionImpl(); 28 | userSubscription.initFromDataObject(userSubscriptionDB, user); 29 | 30 | const stripe = new stripeInstance(); 31 | 32 | if (userSubscription && userSubscription.getStripeCustomerId()) { 33 | const stripeSession = await stripe.getBillingPortal(userSubscription.getStripeCustomerId(), dashboardUrl); 34 | return new NextResponse(JSON.stringify({ url: stripeSession.url })) 35 | } 36 | 37 | stripe.setStripePriceId(stripePriceId); 38 | const stripeSession = await stripe.createSession(dashboardUrl, dashboardUrl, userSubscription.getUser()); 39 | return new NextResponse(JSON.stringify({ url: stripeSession.url })) 40 | } catch (e: any) { 41 | const error = new errorHandler(); 42 | error.internalServerError(e); 43 | return error.generateError(); 44 | } 45 | } -------------------------------------------------------------------------------- /components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import { Slot } from "@radix-ui/react-slot" 3 | import { cva, type VariantProps } from "class-variance-authority" 4 | 5 | import { cn } from "@/lib/utils" 6 | 7 | const buttonVariants = cva( 8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50", 9 | { 10 | variants: { 11 | variant: { 12 | default: 13 | "bg-primary text-primary-foreground shadow hover:bg-primary/90", 14 | destructive: 15 | "bg-destructive text-destructive-foreground shadow-sm hover:bg-destructive/90", 16 | outline: 17 | "border border-input bg-background shadow-sm hover:bg-accent hover:text-accent-foreground", 18 | secondary: 19 | "bg-secondary text-secondary-foreground shadow-sm hover:bg-secondary/80", 20 | ghost: "hover:bg-accent hover:text-accent-foreground", 21 | link: "text-primary underline-offset-4 hover:underline", 22 | }, 23 | size: { 24 | default: "h-9 px-4 py-2", 25 | sm: "h-8 rounded-md px-3 text-xs", 26 | lg: "h-10 rounded-md px-8", 27 | icon: "h-9 w-9", 28 | }, 29 | }, 30 | defaultVariants: { 31 | variant: "default", 32 | size: "default", 33 | }, 34 | } 35 | ) 36 | 37 | export interface ButtonProps 38 | extends React.ButtonHTMLAttributes, 39 | VariantProps { 40 | asChild?: boolean 41 | } 42 | 43 | const Button = React.forwardRef( 44 | ({ className, variant, size, asChild = false, ...props }, ref) => { 45 | const Comp = asChild ? Slot : "button" 46 | return ( 47 | 52 | ) 53 | } 54 | ) 55 | Button.displayName = "Button" 56 | 57 | export { Button, buttonVariants } 58 | -------------------------------------------------------------------------------- /app/dashboard/component/manage-subscription.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { Button } from "@/components/ui/button"; 4 | import React from "react"; 5 | import { Loader2 } from "lucide-react"; 6 | import { toast } from "sonner"; 7 | 8 | interface ManageUserSubscriptionButtonProps { 9 | userId: string; 10 | email: string; 11 | isCurrentPlan: boolean; 12 | isSubscribed: boolean; 13 | stripeCustomerId?: string | null; 14 | stripePriceId: string; 15 | } 16 | 17 | export function ManageUserSubscriptionButton({ 18 | userId, 19 | email, 20 | isCurrentPlan, 21 | isSubscribed, 22 | stripeCustomerId, 23 | stripePriceId, 24 | }: ManageUserSubscriptionButtonProps) { 25 | const [isPending, startTransition] = React.useTransition(); 26 | 27 | const handleSubmit = async (e: React.FormEvent) => { 28 | e.preventDefault(); 29 | 30 | startTransition(async () => { 31 | try { 32 | const res = await fetch("/api/stripe", { 33 | method: "POST", 34 | headers: { "Content-Type": "application/json" }, 35 | body: JSON.stringify({ 36 | email, 37 | userId, 38 | isSubscribed, 39 | isCurrentPlan, 40 | stripeCustomerId, 41 | stripePriceId, 42 | }), 43 | }); 44 | const session: { url: string } = await res.json(); 45 | if (session) { 46 | window.location.href = session.url ?? "/dashboard/billing"; 47 | } 48 | } catch (err) { 49 | console.error((err as Error).message); 50 | toast.error("Something went wrong, please try again later."); 51 | } 52 | }); 53 | }; 54 | 55 | return ( 56 |
57 | 65 |
66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /controllers/PasswordResetTokenController.tsx: -------------------------------------------------------------------------------- 1 | import prisma from "@/lib/prisma"; 2 | import jsonUtilsImpl from "@/utils/jsonUtils"; 3 | // import jsonUtilsImpl from "@/utils/jsonUtils"; 4 | 5 | export default class passwordResetTokenController { 6 | async create(email: string, token: string, expires: any) { 7 | try { 8 | const userJson: any = { 9 | "email": email, 10 | "token": token, 11 | "expires": expires 12 | }; 13 | 14 | const result = await prisma.passwordResetToken.create({ 15 | data: userJson 16 | }) 17 | 18 | return result 19 | } catch (e) { 20 | return e; 21 | } 22 | } 23 | 24 | async deletePasswordResetTokenById(id: string) { 25 | try { 26 | const whereJson = { 27 | "id": id 28 | } 29 | 30 | const finalQuery = { 31 | where: whereJson 32 | } 33 | 34 | await prisma.passwordResetToken.delete(finalQuery); 35 | } catch (e) { 36 | return e; 37 | } 38 | } 39 | 40 | async getPasswordResetTokenByToken(token: string) { 41 | try { 42 | const whereJson = { 43 | "token": token 44 | } 45 | 46 | const finalQuery = { 47 | where: whereJson 48 | } 49 | 50 | const result = await prisma.passwordResetToken.findUnique(finalQuery); 51 | return result; 52 | } catch (e) { 53 | return e; 54 | } 55 | } 56 | 57 | async getPasswordResetTokenByEmail(email: string) { 58 | try { 59 | const whereJson = { 60 | "email": email 61 | } 62 | 63 | const finalQuery = { 64 | where: whereJson 65 | } 66 | 67 | const result = await prisma.passwordResetToken.findFirst(finalQuery); 68 | return result; 69 | } catch (e) { 70 | return e; 71 | } 72 | } 73 | 74 | 75 | } -------------------------------------------------------------------------------- /controllers/VerficationTokenController.tsx: -------------------------------------------------------------------------------- 1 | import prisma from "@/lib/prisma"; 2 | import jsonUtilsImpl from "@/utils/jsonUtils"; 3 | // import jsonUtilsImpl from "@/utils/jsonUtils"; 4 | 5 | export default class verficationTokenController { 6 | async create(email: string, token: string, expires: any) { 7 | try { 8 | const userJson: any = { 9 | "email": email, 10 | "token": token, 11 | "expires": expires 12 | }; 13 | 14 | const result = await prisma.verificationToken.create({ 15 | data: userJson 16 | }) 17 | 18 | return result 19 | } catch (e) { 20 | return e; 21 | } 22 | } 23 | 24 | async deleteVerificationTokenById(id: string) { 25 | try { 26 | const whereJson = { 27 | "id": id 28 | } 29 | 30 | const finalQuery = { 31 | where: whereJson 32 | } 33 | 34 | await prisma.verificationToken.delete(finalQuery); 35 | } catch (e) { 36 | return e; 37 | } 38 | } 39 | 40 | async getVerificationTokenByToken(token: string) { 41 | try { 42 | const whereJson = { 43 | "token": token 44 | } 45 | 46 | const finalQuery = { 47 | where: whereJson 48 | } 49 | 50 | const result = await prisma.verificationToken.findUnique(finalQuery); 51 | return result; 52 | } catch (e) { 53 | return e; 54 | } 55 | } 56 | 57 | async getVerificationTokenByEmail(email: string) { 58 | try { 59 | const whereJson = { 60 | "email": email 61 | } 62 | 63 | const finalQuery = { 64 | where: whereJson 65 | } 66 | 67 | const result = await prisma.verificationToken.findFirst(finalQuery); 68 | return result; 69 | } catch (e) { 70 | return e; 71 | } 72 | } 73 | 74 | 75 | } -------------------------------------------------------------------------------- /auth.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextAuthConfig } from "next-auth"; 2 | import Credentials from "next-auth/providers/credentials"; 3 | import Google from "next-auth/providers/google"; 4 | import configEnv from "@/config" 5 | import bcrypt from "bcryptjs"; 6 | 7 | import userImpl from "@/data/user/userImpl"; 8 | import userController from "@/controllers/UserController"; 9 | 10 | 11 | export default { 12 | providers: [ 13 | Google({ 14 | clientId: configEnv.google.clientId, 15 | clientSecret: configEnv.google.clientSecret 16 | }), 17 | Credentials({ 18 | async authorize(credentials: any) { 19 | if (!credentials?.email || !credentials?.password) { 20 | return null; 21 | } 22 | 23 | const userControllerHandler = new userController(); 24 | const userDB = await userControllerHandler.getUserByEmail(credentials.email); 25 | const user = new userImpl(); 26 | user.initFromDataObject(userDB) 27 | 28 | if (!user || !user.getPassword()) { 29 | return null 30 | } 31 | 32 | const isPasswordValid = await bcrypt.compare(credentials.password, user.getPassword() || ''); 33 | if (!isPasswordValid) { 34 | return null 35 | } 36 | 37 | return user.toJson(); 38 | // const validatedFields = LoginSchema.safeParse(credentials); 39 | 40 | // if (validatedFields.success) { 41 | // const { email, password } = validatedFields.data; 42 | 43 | // const user = await getUserByEmail(email); 44 | // if (!user || !user.password) return null; 45 | 46 | // const passwordsMatch = await bcrypt.compare( 47 | // password, 48 | // user.password, 49 | // ); 50 | 51 | // if (passwordsMatch) return user; 52 | // } 53 | 54 | return null; 55 | } 56 | }) 57 | ], 58 | } satisfies NextAuthConfig -------------------------------------------------------------------------------- /components/ui/card.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | const Card = React.forwardRef< 6 | HTMLDivElement, 7 | React.HTMLAttributes 8 | >(({ className, ...props }, ref) => ( 9 |
17 | )) 18 | Card.displayName = "Card" 19 | 20 | const CardHeader = React.forwardRef< 21 | HTMLDivElement, 22 | React.HTMLAttributes 23 | >(({ className, ...props }, ref) => ( 24 |
29 | )) 30 | CardHeader.displayName = "CardHeader" 31 | 32 | const CardTitle = React.forwardRef< 33 | HTMLParagraphElement, 34 | React.HTMLAttributes 35 | >(({ className, ...props }, ref) => ( 36 |

44 | )) 45 | CardTitle.displayName = "CardTitle" 46 | 47 | const CardDescription = React.forwardRef< 48 | HTMLParagraphElement, 49 | React.HTMLAttributes 50 | >(({ className, ...props }, ref) => ( 51 |

56 | )) 57 | CardDescription.displayName = "CardDescription" 58 | 59 | const CardContent = React.forwardRef< 60 | HTMLDivElement, 61 | React.HTMLAttributes 62 | >(({ className, ...props }, ref) => ( 63 |

64 | )) 65 | CardContent.displayName = "CardContent" 66 | 67 | const CardFooter = React.forwardRef< 68 | HTMLDivElement, 69 | React.HTMLAttributes 70 | >(({ className, ...props }, ref) => ( 71 |
76 | )) 77 | CardFooter.displayName = "CardFooter" 78 | 79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent } 80 | -------------------------------------------------------------------------------- /lib/axios.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosInstance } from "axios"; 2 | 3 | //File Imports 4 | import env_values from '@/config' 5 | import jsonUtilsImpl from "@/utils/jsonUtils"; 6 | 7 | export default class axiosInstance { 8 | private instance: AxiosInstance; 9 | private params: any = {}; 10 | private headers: any = {}; 11 | private payload: any = {}; 12 | private headerJson: any = {}; 13 | 14 | public getInstance(): AxiosInstance { 15 | return this.instance; 16 | } 17 | 18 | public setInstance(instance: AxiosInstance): void { 19 | this.instance = instance; 20 | } 21 | 22 | public getParams(): any { 23 | return this.params; 24 | } 25 | 26 | public setParams(params: any): void { 27 | this.params = params; 28 | } 29 | 30 | public getHeaders(): any { 31 | return this.headers; 32 | } 33 | 34 | public setHeaders(headers: any): void { 35 | this.headers = headers; 36 | } 37 | 38 | public getPayload(): any { 39 | return this.payload; 40 | } 41 | 42 | public setPayload(payload: any): void { 43 | this.payload = payload; 44 | } 45 | 46 | public getHeaderJson(): any { 47 | return this.headerJson; 48 | } 49 | 50 | public setHeaderJson(headerJson: any): void { 51 | this.headerJson = headerJson; 52 | } 53 | 54 | constructor() { 55 | this.instance = axios.create({ 56 | baseURL: env_values.app.url 57 | }); 58 | } 59 | 60 | 61 | 62 | 63 | public async makeCall(URL: string, method: string) { 64 | 65 | let configJson: any = {}; 66 | 67 | if (!jsonUtilsImpl.isEmpty(this.params)) { 68 | configJson['params'] = this.params; 69 | } 70 | 71 | let response: any; 72 | try { 73 | if (method === 'GET') { 74 | response = await this.instance.get(URL, configJson); 75 | } 76 | 77 | if (method === 'POST') { 78 | response = await this.instance.post(URL, this.payload, configJson); 79 | } 80 | 81 | } catch(e: any) { 82 | let error = e.response.data; 83 | return error; 84 | } 85 | 86 | return response.data; 87 | } 88 | 89 | 90 | } -------------------------------------------------------------------------------- /data/verificationToken/verificationTokenImpl.tsx: -------------------------------------------------------------------------------- 1 | import stringUtils from "@/utils/stringUtils"; 2 | import verificationTokenInterface from "./verificationTokenInterface"; 3 | import { v4 as uuidv4 } from "uuid"; 4 | 5 | import prisma from "@/lib/prisma"; 6 | import verficationTokenController from "@/controllers/VerficationTokenController"; 7 | 8 | export default class verificationTokenImpl implements verificationTokenInterface { 9 | id: string = ''; 10 | email: string = ''; 11 | token: string = ''; 12 | expires: string = '' 13 | 14 | initFromDataObject(data: any) { 15 | if (!stringUtils.isUndefinedEmptyorNull(data.id)) { 16 | this.id = data.id 17 | } 18 | 19 | if (!stringUtils.isUndefinedEmptyorNull(data.email)) { 20 | this.email = data.email 21 | } 22 | 23 | if (!stringUtils.isUndefinedEmptyorNull(data.token)) { 24 | this.token = data.token 25 | } 26 | 27 | if (!stringUtils.isUndefinedEmptyorNull(data.expires)) { 28 | this.expires = data.expires 29 | } 30 | } 31 | 32 | async generateVerificationToken(email: string) { 33 | // GENERATING TOKEN AND INSERTING INTO DATABASE 34 | 35 | const token = uuidv4(); 36 | const expires = new Date(new Date().getTime() + 3600 * 1000); 37 | 38 | const vertificationTokenControllerHandler = new verficationTokenController(); 39 | const existingToken: any = await vertificationTokenControllerHandler.getVerificationTokenByEmail(email); 40 | 41 | if (existingToken) { 42 | this.initFromDataObject(existingToken); 43 | await vertificationTokenControllerHandler.deleteVerificationTokenById(this.id); 44 | } 45 | 46 | const verficationToken = await vertificationTokenControllerHandler.create(email, token, expires); 47 | this.initFromDataObject(verficationToken); 48 | } 49 | 50 | setToken(token: string) { 51 | this.token = token; 52 | } 53 | 54 | getToken() { 55 | return this.token; 56 | } 57 | 58 | getEmail() { 59 | return this.email; 60 | } 61 | 62 | getId() { 63 | return this.id; 64 | } 65 | 66 | isTokenExpired() { 67 | return new Date(this.expires) < new Date() 68 | } 69 | } -------------------------------------------------------------------------------- /app/dashboard/component/mail.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import axiosInstance from "@/lib/axios"; 4 | import AllAPIRouteMapping from "@/utils/AllAPIRouteMapping"; 5 | import { Loader } from "lucide-react"; 6 | import { FormEvent, useRef, useState } from "react"; 7 | import { toast } from "sonner"; 8 | 9 | type Props = {}; 10 | 11 | export default function Mail({}: Props) { 12 | 13 | const [mailStatus, setMailStatus] = useState(false); 14 | const nameRef = useRef(null); 15 | const emailRef = useRef(null); 16 | 17 | const sendEmail = async (event: FormEvent) => { 18 | event.preventDefault(); 19 | setMailStatus(true); 20 | try { 21 | const axios = new axiosInstance(); 22 | axios.setPayload(JSON.stringify({ 23 | email: emailRef.current?.value, 24 | name: nameRef.current?.value, 25 | })); 26 | 27 | const response = await axios.makeCall(AllAPIRouteMapping.mails.send.apiPath, AllAPIRouteMapping.mails.send.method); 28 | // console.log("response: ", response); 29 | const { error } = response; 30 | if (error) { 31 | toast.error("Something went wrong!!"); 32 | return; 33 | } 34 | toast.success("Email send successfully"); 35 | } catch (error) { 36 | toast.error("Something went wrong!!"); 37 | } finally { 38 | setMailStatus(false); 39 | } 40 | }; 41 | return ( 42 |
43 |
47 | 54 | 61 | 67 |
68 |
69 | ); 70 | } 71 | -------------------------------------------------------------------------------- /config.ts: -------------------------------------------------------------------------------- 1 | //config.js 2 | const env = process.env.NEXT_PUBLIC_ENV || ''; 3 | // console.log(env); 4 | 5 | const dev: any = { 6 | nextEnv: process.env.NEXT_PUBLIC_ENV || '', 7 | nextAuth: { 8 | secret: process.env.NEXTAUTH_SECRET || '', 9 | url: process.env.DEV_NEXTAUTH_URL || '' 10 | }, 11 | app: { 12 | url: process.env.NEXT_PUBLIC_DEV_API_URL || '' 13 | }, 14 | google: { 15 | clientId: process.env.DEV_GOOGLE_CLIENT_ID || '', 16 | clientSecret: process.env.DEV_GOOGLE_CLIENT_SECRET || '' 17 | }, 18 | db: { 19 | host: process.env.DEV_DB_HOST || '', 20 | name: process.env.DEV_DB_NAME || '', 21 | username: process.env.DEV_DB_USERNAME || '', 22 | password: process.env.DEV_DB_PASSWORD || '', 23 | port: process.env.PROD_DB_PORT || '', 24 | url: process.env.DEV_DATABASE_URL || '' 25 | }, 26 | stripe: { 27 | secret: process.env.DEV_STRIPE_SECRET_KEY || '', 28 | webhook: process.env.DEV_STRIPE_WEBHOOK_SECRET || '', 29 | publishable: process.env.DEV_NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY || '' 30 | }, 31 | resend: { 32 | apiKey: process.env.DEV_RESEND_API_KEY || '' 33 | } 34 | }; 35 | 36 | const prod: any = { 37 | nextEnv: process.env.NEXT_PUBLIC_ENV || '', 38 | nextAuthSecret: process.env.NEXTAUTH_SECRET || '', 39 | app: { 40 | url: process.env.NEXT_PUBLIC_PROD_API_URL || '' 41 | }, 42 | db: { 43 | host: process.env.PROD_DB_HOST || '', 44 | name: process.env.PROD_DB_NAME || '', 45 | username: process.env.PROD_DB_USERNAME || '', 46 | password: process.env.PROD_DB_PASSWORD || '', 47 | port: process.env.PROD_DB_PORT || '' 48 | }, 49 | google: { 50 | clientId: process.env.PROD_GOOGLE_CLIENT_ID || '', 51 | clientSecret: process.env.PROD_GOOGLE_CLIENT_SECRET || '' 52 | }, 53 | stripe: { 54 | secret: process.env.PROD_STRIPE_SECRET_KEY || '', 55 | webhook: process.env.PROD_STRIPE_WEBHOOK_SECRET || '', 56 | publishable: process.env.PROD_NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY || '' 57 | }, 58 | resend: { 59 | apiKey: process.env.PROD_RESEND_API_KEY || '' 60 | } 61 | }; 62 | 63 | const config: any = { 64 | dev, 65 | prod 66 | }; 67 | 68 | export default config[env]; -------------------------------------------------------------------------------- /lib/stripe.ts: -------------------------------------------------------------------------------- 1 | import userInterface from "@/data/user/userInterface"; 2 | import Stripe from "stripe"; 3 | import { absoluteUrl } from "./utils"; 4 | import configEnv from "@/config" 5 | 6 | export default class stripeInstance { 7 | stripe: any 8 | stripePriceId: string = '' 9 | 10 | constructor() { 11 | this.stripe = new Stripe(configEnv.stripe.secret || "", { 12 | apiVersion: "2023-10-16", 13 | typescript: true, 14 | }); 15 | } 16 | 17 | getStripe() { 18 | return this.stripe; 19 | } 20 | 21 | setStripePriceId(stripePriceId: string) { 22 | this.stripePriceId = stripePriceId; 23 | } 24 | 25 | async getBillingPortal(stripeCustomerId: string, returnUrl: string) { 26 | const billingPortal = await this.stripe.billingPortal.sessions.create({ 27 | customer: stripeCustomerId, 28 | return_url: returnUrl 29 | }) 30 | return billingPortal; 31 | } 32 | 33 | async createSession(success_url: string, cancel_url: string, user: userInterface) { 34 | const userId = user.getId(); 35 | const stripeSession = await this.stripe.checkout.sessions.create({ 36 | success_url: absoluteUrl("/dashboard/billing?success=true"), 37 | cancel_url: absoluteUrl("/dashboard"), 38 | payment_method_types: ["card"], 39 | mode: "subscription", 40 | billing_address_collection: "auto", 41 | customer_email: user.getEmail(), 42 | line_items: [ 43 | { 44 | price: this.stripePriceId, 45 | quantity: 1, 46 | }, 47 | ], 48 | metadata: { 49 | userId, 50 | }, 51 | }); 52 | 53 | return stripeSession; 54 | } 55 | 56 | 57 | async webhookEvent(body: string, signature: string) { 58 | const event = await this.stripe.webhooks.constructEvent( 59 | body, 60 | signature, 61 | configEnv.stripe.webhook! 62 | ) 63 | return event; 64 | } 65 | 66 | 67 | async retriveSubscription(stripeSession: any) { 68 | const result = await this.stripe.subscriptions.retrieve( 69 | stripeSession.subscription as string 70 | ) 71 | return result; 72 | } 73 | 74 | } -------------------------------------------------------------------------------- /data/mail/mailImpl.tsx: -------------------------------------------------------------------------------- 1 | import stringUtils from "@/utils/stringUtils"; 2 | import mailInterface from "./mailInterface"; 3 | import configEnv from "@/config"; 4 | 5 | export default class mailImpl implements mailInterface { 6 | name: string = ''; 7 | email: string = ''; 8 | from: string = ''; 9 | subject: string = ''; 10 | text?: string = ''; 11 | html?: string; 12 | 13 | initFromDataObject(data: any) { 14 | if (!stringUtils.isUndefinedEmptyorNull(data.name)) { 15 | this.name = data.name 16 | } 17 | 18 | if (!stringUtils.isUndefinedEmptyorNull(data.name)) { 19 | this.name = data.name 20 | } 21 | 22 | if (!stringUtils.isUndefinedEmptyorNull(data.from)) { 23 | this.from = data.from 24 | } 25 | 26 | if (!stringUtils.isUndefinedEmptyorNull(data.subject)) { 27 | this.subject = data.subject 28 | } 29 | 30 | if (!stringUtils.isUndefinedEmptyorNull(data.text)) { 31 | this.text = data.text 32 | } 33 | } 34 | 35 | setFrom(from: string) { 36 | this.from = from 37 | } 38 | 39 | getFrom(): string { 40 | return this.from; 41 | } 42 | 43 | setSubject(subject: string) { 44 | this.subject = subject 45 | } 46 | 47 | getSubject(): string { 48 | return this.subject; 49 | } 50 | 51 | setText(text: string) { 52 | this.text = text 53 | } 54 | 55 | getText() { 56 | return this.text || '' 57 | } 58 | 59 | setHTML(html: string) { 60 | this.html = html; 61 | } 62 | 63 | getHTML() { 64 | return this.html || '' 65 | } 66 | 67 | getName() { 68 | return this.name; 69 | } 70 | 71 | setEmail(email: string) { 72 | this.email = email; 73 | } 74 | 75 | getEmail() { 76 | return this.email; 77 | } 78 | 79 | populateTestCredentials() { 80 | this.from = "onboarding@resend.dev"; 81 | } 82 | 83 | populateProdCredentials() { 84 | this.from = "onboarding@buildfast.co.in"; 85 | } 86 | 87 | populateCredentials() { 88 | if (configEnv.nextEnv === 'dev') { 89 | this.populateTestCredentials(); 90 | } else { 91 | // TODO: Chagne to prod 92 | this.populateProdCredentials(); 93 | } 94 | } 95 | } -------------------------------------------------------------------------------- /auth.ts: -------------------------------------------------------------------------------- 1 | import NextAuth from "next-auth" 2 | import { PrismaAdapter } from "@auth/prisma-adapter"; 3 | 4 | import userController from "@/controllers/UserController"; 5 | import prisma from "@/lib/prisma"; 6 | import authConfig from "@/auth.config"; 7 | import userImpl from "./data/user/userImpl"; 8 | import UserRoleEnum from "./enums/UserRoleEnum"; 9 | 10 | 11 | export const { 12 | handlers: { GET, POST }, 13 | auth, 14 | signIn, 15 | signOut, 16 | } = NextAuth({ 17 | pages: { 18 | signIn: "/", 19 | error: "/auth/error" 20 | }, 21 | events: { 22 | async linkAccount({user}) { 23 | const userControllerHandler = new userController(); 24 | await userControllerHandler.setEmailVerifiedById(user.id || ''); 25 | } 26 | }, 27 | callbacks: { 28 | async signIn({user, account}) { 29 | // Allow OAuth without email verification 30 | if (account?.provider !== "credentials") return true; 31 | 32 | 33 | const userControlledHandler = new userController(); 34 | const existingUser: any = await userControlledHandler.getUserById(user.id as string); 35 | 36 | // Prevent sign in without email verification 37 | if (!existingUser?.emailVerified) return false; 38 | 39 | // TODO: Add 2FA check 40 | 41 | return true; 42 | }, 43 | async session({token, session}) { 44 | if(token.sub && session.user) { 45 | session.user.id = token.sub; 46 | } 47 | 48 | if(token.role && session.user) { 49 | session.user.role = token.role as UserRoleEnum; 50 | } 51 | return session; 52 | 53 | }, 54 | async jwt({ token }) { 55 | if(!token.sub) return token; 56 | 57 | const userControlledHandler = new userController(); 58 | const existingUser = await userControlledHandler.getUserById(token.sub); 59 | 60 | if(!existingUser) return token; 61 | 62 | const userForm = new userImpl(); 63 | userForm.initFromDataObject(existingUser); 64 | 65 | token.role = userForm.getRole(); 66 | return token; 67 | } 68 | }, 69 | adapter: PrismaAdapter(prisma), 70 | session: { strategy: "jwt" }, 71 | ...authConfig, 72 | }); -------------------------------------------------------------------------------- /app/api/auth/user/route.ts: -------------------------------------------------------------------------------- 1 | import userController from "@/controllers/UserController"; 2 | import mailImpl from "@/data/mail/mailImpl"; 3 | import userImpl from "@/data/user/userImpl"; 4 | import userInterface from "@/data/user/userInterface"; 5 | import verificationTokenImpl from "@/data/verificationToken/verificationTokenImpl"; 6 | import errorHandler from "@/helpers/errorHandler"; 7 | import resendInstance from "@/lib/resend"; 8 | import bcrypt from "bcryptjs"; 9 | import { NextRequest, NextResponse } from "next/server"; 10 | 11 | 12 | export async function POST(req: NextRequest) { 13 | try { 14 | 15 | const userData: userInterface = await req.json(); 16 | 17 | const hashedPassword = await bcrypt.hash(userData.password || '', 12); 18 | userData.password = hashedPassword; 19 | const userForm = new userImpl(); 20 | userForm.initFromDataObject(userData); 21 | 22 | // checking wheather user exist 23 | const userControllerHandler = new userController(); 24 | const isEmailExists = await userControllerHandler.isEmailexists(userForm.email); 25 | 26 | if (!isEmailExists) { 27 | // TODO: add functionality for lastname 28 | await userControllerHandler.create(userForm.getName(), userForm.getPassword() || '', userForm.getEmail()) 29 | 30 | const verificationTokenForm = new verificationTokenImpl(); 31 | await verificationTokenForm.generateVerificationToken(userForm.getEmail()); 32 | 33 | const mail = new mailImpl(); 34 | mail.populateCredentials() 35 | 36 | const resend = new resendInstance(); 37 | resend.sendVerificationEmail(userForm.getEmail(), verificationTokenForm.getToken(), mail); 38 | } else { 39 | const error = new errorHandler(); 40 | error.conflict("Email Already Exists"); 41 | return error.generateError(); 42 | } 43 | 44 | // TODO: Send Verification token email 45 | const returnJson: any = { 46 | status: 200, 47 | message: "User Created", 48 | success: true 49 | } 50 | return NextResponse.json(returnJson); 51 | 52 | } catch (e: any) { 53 | const error = new errorHandler(); 54 | error.internalServerError(e); 55 | return error.generateError(); 56 | } 57 | } -------------------------------------------------------------------------------- /app/api/auth/verification/route.tsx: -------------------------------------------------------------------------------- 1 | import userController from "@/controllers/UserController"; 2 | import verficationTokenController from "@/controllers/VerficationTokenController"; 3 | import userImpl from "@/data/user/userImpl"; 4 | import verificationTokenImpl from "@/data/verificationToken/verificationTokenImpl"; 5 | import errorHandler from "@/helpers/errorHandler"; 6 | import { NextRequest, NextResponse } from "next/server"; 7 | 8 | export async function POST(req: NextRequest) { 9 | try { 10 | const { token } = await req.json(); 11 | 12 | const verficationTokenControllerHandler = new verficationTokenController(); 13 | const existingToken = await verficationTokenControllerHandler.getVerificationTokenByToken(token); 14 | 15 | if(!existingToken) { 16 | const error = new errorHandler(); 17 | error.internalServerError("Token does not exist"); 18 | return error.generateError(); 19 | } 20 | 21 | const verificationTokenForm = new verificationTokenImpl(); 22 | verificationTokenForm.initFromDataObject(existingToken); 23 | 24 | if(verificationTokenForm.isTokenExpired()) { 25 | const error = new errorHandler(); 26 | error.internalServerError("Token has expired"); 27 | return error.generateError(); 28 | } 29 | 30 | const userControllerHandler = new userController(); 31 | const existingUser = await userControllerHandler.getUserByEmail(verificationTokenForm.getEmail()) 32 | 33 | if(!existingUser) { 34 | const error = new errorHandler(); 35 | error.missingItem("User Not found"); 36 | return error.generateError(); 37 | } 38 | 39 | const userForm = new userImpl(); 40 | userForm.initFromDataObject(existingUser); 41 | await userControllerHandler.updateData(userForm.getId(), userForm.getEmail()); 42 | 43 | await verficationTokenControllerHandler.deleteVerificationTokenById(verificationTokenForm.getId()); 44 | const returnJson: any = { 45 | status: 200, 46 | message: "User Verified", 47 | success: true 48 | } 49 | return NextResponse.json(returnJson); 50 | } catch (e: any) { 51 | const error = new errorHandler(); 52 | error.internalServerError(e); 53 | return error.generateError(); 54 | } 55 | } -------------------------------------------------------------------------------- /data/passwordResetToken/passwordResetTokenImpl.tsx: -------------------------------------------------------------------------------- 1 | import stringUtils from "@/utils/stringUtils"; 2 | import { v4 as uuidv4 } from "uuid"; 3 | 4 | import prisma from "@/lib/prisma"; 5 | import verficationTokenController from "@/controllers/VerficationTokenController"; 6 | import passwordResetTokenInterface from "./passwordResetTokenInterface"; 7 | import passwordResetTokenController from "@/controllers/PasswordResetTokenController"; 8 | 9 | export default class passwordResetTokenImpl implements passwordResetTokenInterface { 10 | id: string = ''; 11 | email: string = ''; 12 | token: string = ''; 13 | expires: string = '' 14 | 15 | initFromDataObject(data: any) { 16 | if (!stringUtils.isUndefinedEmptyorNull(data.id)) { 17 | this.id = data.id 18 | } 19 | 20 | if (!stringUtils.isUndefinedEmptyorNull(data.email)) { 21 | this.email = data.email 22 | } 23 | 24 | if (!stringUtils.isUndefinedEmptyorNull(data.token)) { 25 | this.token = data.token 26 | } 27 | 28 | if (!stringUtils.isUndefinedEmptyorNull(data.expires)) { 29 | this.expires = data.expires 30 | } 31 | } 32 | 33 | async generateVerificationToken(email: string) { 34 | // GENERATING TOKEN AND INSERTING INTO DATABASE 35 | 36 | const token = uuidv4(); 37 | const expires = new Date(new Date().getTime() + 3600 * 1000); 38 | 39 | const passwordResetTokenControllerHandler = new passwordResetTokenController(); 40 | const existingToken: any = await passwordResetTokenControllerHandler.getPasswordResetTokenByEmail(email); 41 | 42 | if (existingToken) { 43 | this.initFromDataObject(existingToken); 44 | await passwordResetTokenControllerHandler.deletePasswordResetTokenById(this.id); 45 | } 46 | 47 | const verficationToken = await passwordResetTokenControllerHandler.create(email, token, expires); 48 | this.initFromDataObject(verficationToken); 49 | } 50 | 51 | setToken(token: string) { 52 | this.token = token; 53 | } 54 | 55 | getToken() { 56 | return this.token; 57 | } 58 | 59 | getEmail() { 60 | return this.email; 61 | } 62 | 63 | getId() { 64 | return this.id; 65 | } 66 | 67 | isTokenExpired() { 68 | return new Date(this.expires) < new Date() 69 | } 70 | } -------------------------------------------------------------------------------- /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 = "mysql" 10 | url = env("DATABASE_URL") 11 | } 12 | 13 | model Account { 14 | id String @id @default(cuid()) 15 | userId String 16 | type String 17 | provider String 18 | providerAccountId String 19 | refresh_token String? @db.Text 20 | access_token String? @db.Text 21 | expires_at Int? 22 | token_type String? 23 | scope String? 24 | id_token String? @db.Text 25 | session_state String? 26 | 27 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 28 | 29 | @@unique([provider, providerAccountId]) 30 | } 31 | 32 | model Session { 33 | id String @id @default(cuid()) 34 | sessionToken String @unique 35 | userId String 36 | expires DateTime 37 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 38 | } 39 | 40 | enum UserRole { 41 | ADMIN 42 | USER 43 | } 44 | 45 | model User { 46 | id String @id @default(cuid()) 47 | name String? 48 | email String? @unique 49 | emailVerified DateTime? 50 | password String? 51 | image String? 52 | role UserRole @default(USER) 53 | accounts Account[] 54 | sessions Session[] 55 | subscriptions Subscription? 56 | } 57 | 58 | model VerificationToken { 59 | id String @id @default(cuid()) 60 | email String 61 | token String @unique 62 | expires DateTime 63 | 64 | @@unique([email, token]) 65 | } 66 | 67 | model PasswordResetToken { 68 | id String @id @default(cuid()) 69 | email String 70 | token String @unique 71 | expires DateTime 72 | 73 | @@unique([email, token]) 74 | } 75 | 76 | 77 | model Subscription { 78 | id String @id @default(cuid()) 79 | userId String @unique 80 | stripeCustomerId String? @unique @map(name: "stripe_customer_id") 81 | stripeSubscriptionId String? @unique @map(name: "stripe_subscription_id") 82 | stripePriceId String? @map(name: "stripe_price_id") 83 | stripeCurrentPeriodEnd DateTime? @map(name: "stripe_current_period_end") 84 | user User @relation(fields: [userId], references: [id]) 85 | } -------------------------------------------------------------------------------- /actions/login.tsx: -------------------------------------------------------------------------------- 1 | "use server"; 2 | 3 | import * as z from "zod"; 4 | import { AuthError } from "next-auth"; 5 | 6 | import { signIn } from "@/auth"; 7 | import { loginFormSchema } from "@/schemas/index"; 8 | import { DEFAULT_LOGIN_REDIRECT } from "@/routes"; 9 | import userImpl from "@/data/user/userImpl"; 10 | import userController from "@/controllers/UserController"; 11 | import verificationTokenImpl from "@/data/verificationToken/verificationTokenImpl"; 12 | import mailImpl from "@/data/mail/mailImpl"; 13 | import resendInstance from "@/lib/resend"; 14 | import stringUtils from "@/utils/stringUtils"; 15 | 16 | export const login = async (values: z.infer) => { 17 | const validatedFields = loginFormSchema.safeParse(values); 18 | 19 | if (!validatedFields.success) { 20 | return { error: "Invalid fields!" }; 21 | } 22 | 23 | const { email, password } = validatedFields.data; 24 | 25 | const userControllerHandler = new userController(); 26 | const existingUser: any = await userControllerHandler.getUserByEmail(email); 27 | 28 | if(!existingUser || !existingUser.email || !existingUser.password) { 29 | return { error: "Email Does not exist"} 30 | } 31 | 32 | if(!existingUser.emailVerified) { 33 | const verificationTokenForm = new verificationTokenImpl(); 34 | await verificationTokenForm.generateVerificationToken(existingUser.email); 35 | 36 | const mail = new mailImpl(); 37 | mail.populateCredentials() 38 | 39 | const resend = new resendInstance(); 40 | const result: any = await resend.sendVerificationEmail(email, verificationTokenForm.getToken(), mail); 41 | if(!stringUtils.isUndefinedEmptyorNull(result?.data)) { 42 | return { success: "Confirmation Email Send" }; 43 | } else { 44 | return { error: "Error sending mail", "result": result }; 45 | } 46 | 47 | } 48 | 49 | try { 50 | await signIn("credentials", { 51 | email, 52 | password, 53 | redirectTo: DEFAULT_LOGIN_REDIRECT, 54 | }) 55 | } catch (error) { 56 | if (error instanceof AuthError) { 57 | switch (error.type) { 58 | case "CredentialsSignin": 59 | return { error: "Invalid credentials!" } 60 | default: 61 | return { error: "Something went wrong!" } 62 | } 63 | } 64 | 65 | throw error; 66 | } 67 | }; -------------------------------------------------------------------------------- /tailwind.config.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from "tailwindcss" 2 | 3 | const config = { 4 | darkMode: ["class"], 5 | content: [ 6 | './pages/**/*.{ts,tsx}', 7 | './components/**/*.{ts,tsx}', 8 | './app/**/*.{ts,tsx}', 9 | './src/**/*.{ts,tsx}', 10 | ], 11 | prefix: "", 12 | theme: { 13 | container: { 14 | center: true, 15 | padding: "2rem", 16 | screens: { 17 | "2xl": "1400px", 18 | }, 19 | }, 20 | extend: { 21 | colors: { 22 | border: "hsl(var(--border))", 23 | input: "hsl(var(--input))", 24 | ring: "hsl(var(--ring))", 25 | background: "hsl(var(--background))", 26 | foreground: "hsl(var(--foreground))", 27 | primary: { 28 | DEFAULT: "hsl(var(--primary))", 29 | foreground: "hsl(var(--primary-foreground))", 30 | }, 31 | secondary: { 32 | DEFAULT: "hsl(var(--secondary))", 33 | foreground: "hsl(var(--secondary-foreground))", 34 | }, 35 | destructive: { 36 | DEFAULT: "hsl(var(--destructive))", 37 | foreground: "hsl(var(--destructive-foreground))", 38 | }, 39 | muted: { 40 | DEFAULT: "hsl(var(--muted))", 41 | foreground: "hsl(var(--muted-foreground))", 42 | }, 43 | accent: { 44 | DEFAULT: "hsl(var(--accent))", 45 | foreground: "hsl(var(--accent-foreground))", 46 | }, 47 | popover: { 48 | DEFAULT: "hsl(var(--popover))", 49 | foreground: "hsl(var(--popover-foreground))", 50 | }, 51 | card: { 52 | DEFAULT: "hsl(var(--card))", 53 | foreground: "hsl(var(--card-foreground))", 54 | }, 55 | }, 56 | borderRadius: { 57 | lg: "var(--radius)", 58 | md: "calc(var(--radius) - 2px)", 59 | sm: "calc(var(--radius) - 4px)", 60 | }, 61 | keyframes: { 62 | "accordion-down": { 63 | from: { height: "0" }, 64 | to: { height: "var(--radix-accordion-content-height)" }, 65 | }, 66 | "accordion-up": { 67 | from: { height: "var(--radix-accordion-content-height)" }, 68 | to: { height: "0" }, 69 | }, 70 | }, 71 | animation: { 72 | "accordion-down": "accordion-down 0.2s ease-out", 73 | "accordion-up": "accordion-up 0.2s ease-out", 74 | }, 75 | }, 76 | }, 77 | plugins: [require("tailwindcss-animate")], 78 | } satisfies Config 79 | 80 | export default config -------------------------------------------------------------------------------- /app/api/auth/new-password/route.tsx: -------------------------------------------------------------------------------- 1 | import passwordResetTokenController from "@/controllers/PasswordResetTokenController"; 2 | import userController from "@/controllers/UserController"; 3 | import passwordResetTokenImpl from "@/data/passwordResetToken/passwordResetTokenImpl"; 4 | import userImpl from "@/data/user/userImpl"; 5 | import errorHandler from "@/helpers/errorHandler"; 6 | import { NextRequest, NextResponse } from "next/server"; 7 | import bcrypt from "bcryptjs"; 8 | 9 | export async function POST(req: NextRequest) { 10 | try { 11 | const { password, token } = await req.json(); 12 | 13 | const passwordResetTokenControllerHandler = new passwordResetTokenController(); 14 | const existingToken = await passwordResetTokenControllerHandler.getPasswordResetTokenByToken(token); 15 | 16 | if (!existingToken) { 17 | const error = new errorHandler(); 18 | error.internalServerError("Token does not exist"); 19 | return error.generateError(); 20 | } 21 | 22 | const passwordResetTokenForm = new passwordResetTokenImpl(); 23 | passwordResetTokenForm.initFromDataObject(existingToken); 24 | 25 | if (passwordResetTokenForm.isTokenExpired()) { 26 | const error = new errorHandler(); 27 | error.internalServerError("Token has expired"); 28 | return error.generateError(); 29 | } 30 | 31 | const userControllerHandler = new userController(); 32 | const existingUser = await userControllerHandler.getUserByEmail(passwordResetTokenForm.getEmail()) 33 | 34 | if (!existingUser) { 35 | const error = new errorHandler(); 36 | error.missingItem("User Not found"); 37 | return error.generateError(); 38 | } 39 | 40 | const userForm = new userImpl(); 41 | userForm.initFromDataObject(existingUser); 42 | 43 | const hashedPassword = await bcrypt.hash(password, 10); 44 | await userControllerHandler.updatePassword(userForm.getId(), hashedPassword); 45 | await passwordResetTokenControllerHandler.deletePasswordResetTokenById(passwordResetTokenForm.getId()); 46 | 47 | const returnJson: any = { 48 | status: 200, 49 | message: "Password Updated", 50 | success: true 51 | } 52 | return NextResponse.json(returnJson); 53 | } catch (e: any) { 54 | const error = new errorHandler(); 55 | error.internalServerError(e); 56 | return error.generateError(); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /components/internal/Forms/NewVerificationForm.tsx: -------------------------------------------------------------------------------- 1 | "use client"; 2 | 3 | import { useSearchParams } from "next/navigation"; 4 | import { useCallback, useEffect, useState } from "react"; 5 | 6 | import { 7 | Card, 8 | CardContent, 9 | CardDescription, 10 | CardFooter, 11 | CardHeader, 12 | CardTitle, 13 | } from "@/components/ui/card" 14 | import { FormSuccess } from "../FormSuccess"; 15 | import { FormError } from "../FormError"; 16 | import axiosInstance from "@/lib/axios"; 17 | import AllAPIRouteMapping from "@/utils/AllAPIRouteMapping"; 18 | 19 | 20 | export default function NewVerificationForm() { 21 | const [error, setError] = useState(); 22 | const [success, setSuccess] = useState(); 23 | const searchParams = useSearchParams(); 24 | 25 | const token = searchParams.get("token"); 26 | 27 | 28 | const onSubmit = useCallback(async () => { 29 | if (success || error) return; 30 | 31 | if (!token) { 32 | setError("Missing token!"); 33 | return; 34 | } 35 | 36 | const axios = new axiosInstance() 37 | const data = { 38 | "token": token 39 | }; 40 | axios.setPayload(data); 41 | const response = await axios.makeCall(AllAPIRouteMapping.users.verify.apiPath, AllAPIRouteMapping.users.verify.method); 42 | if (response?.success) { 43 | setSuccess(response?.message) 44 | setError("") 45 | } else { 46 | setError(response?.error) 47 | setSuccess("") 48 | } 49 | // newVerification(token) 50 | // .then((data) => { 51 | // setSuccess(data.success); 52 | // setError(data.error); 53 | // }) 54 | // .catch(() => { 55 | // setError("Something went wrong!"); 56 | // }) 57 | }, [token, success, error]); 58 | 59 | useEffect(() => { 60 | onSubmit(); 61 | }, [onSubmit]); 62 | 63 | 64 | return ( 65 | 66 | 67 | Card Title 68 | Card Description 69 | 70 | 71 |
72 | 73 | {!success && ( 74 | 75 | )} 76 |
77 |
78 | 79 |

Card Footer

80 |
81 |
82 | ) 83 | } -------------------------------------------------------------------------------- /app/api/webhook/stripe/route.ts: -------------------------------------------------------------------------------- 1 | 2 | import stripeInstance from "@/lib/stripe"; 3 | import { headers } from "next/headers"; 4 | import Stripe from "stripe"; 5 | import prisma from "@/lib/prisma"; 6 | 7 | import configEnv from "@/config" 8 | export async function POST(request: Request) { 9 | const body = await request.text(); 10 | const signature = headers().get("Stripe-Signature") ?? ""; 11 | 12 | let event: Stripe.Event; 13 | 14 | const stripeInstaceHandler = new stripeInstance(); 15 | 16 | try { 17 | event = stripeInstaceHandler.getStripe().webhooks.constructEvent( 18 | body, 19 | signature, 20 | configEnv.stripe.webhook || "" 21 | ); 22 | } catch (err) { 23 | return new Response( 24 | `Webhook Error: ${err instanceof Error ? err.message : "Unknown Error"}`, 25 | { status: 400 } 26 | ); 27 | } 28 | 29 | const session = event.data.object as Stripe.Checkout.Session; 30 | // console.log("this is the session metadata -> ", session); 31 | 32 | if (!session?.metadata?.userId && session.customer == null) { 33 | return new Response(null, { 34 | status: 200, 35 | }); 36 | } 37 | 38 | if (event.type === "checkout.session.completed") { 39 | const subscription = await stripeInstaceHandler.getStripe().subscriptions.retrieve( 40 | session.subscription as string 41 | ); 42 | const updatedData = { 43 | stripeSubscriptionId: subscription.id, 44 | stripeCustomerId: subscription.customer as string, 45 | stripePriceId: subscription.items.data[0].price.id, 46 | stripeCurrentPeriodEnd: new Date(subscription.current_period_end * 1000), 47 | }; 48 | 49 | if (session?.metadata?.userId != null) { 50 | await prisma.subscription.upsert({ 51 | where: { userId: session.metadata.userId }, 52 | update: { ...updatedData, userId: session.metadata.userId }, 53 | create: { ...updatedData, userId: session.metadata.userId }, 54 | }); 55 | } else if ( 56 | typeof session.customer === "string" && 57 | session.customer != null 58 | ) { 59 | await prisma.subscription.update({ 60 | where: { stripeCustomerId: session.customer }, 61 | data: updatedData, 62 | }); 63 | } 64 | } 65 | 66 | if (event.type === "invoice.payment_succeeded") { 67 | // Retrieve the subscription details from Stripe. 68 | const subscription = await stripeInstaceHandler.getStripe().subscriptions.retrieve( 69 | session.subscription as string 70 | ); 71 | 72 | // Update the price id and set the new period end. 73 | await prisma.subscription.update({ 74 | where: { 75 | stripeSubscriptionId: subscription.id, 76 | }, 77 | data: { 78 | stripePriceId: subscription.items.data[0].price.id, 79 | stripeCurrentPeriodEnd: new Date( 80 | subscription.current_period_end * 1000 81 | ), 82 | }, 83 | }); 84 | } 85 | 86 | return new Response(null, { status: 200 }); 87 | } 88 | -------------------------------------------------------------------------------- /data/user/userImpl.ts: -------------------------------------------------------------------------------- 1 | import stringUtils from "@/utils/stringUtils"; 2 | import userInterface from "./userInterface"; 3 | import UserRoleEnum from "@/enums/UserRoleEnum"; 4 | 5 | export default class userImpl implements userInterface { 6 | id: string = ''; 7 | name: string = ''; 8 | email: string = ''; 9 | emailVerified: boolean = false; 10 | password?: string; 11 | role?: UserRoleEnum 12 | image?: string; 13 | 14 | getId() { 15 | return this.id; 16 | } 17 | 18 | getName() { 19 | return this.name; 20 | } 21 | 22 | getEmail() { 23 | return this.email; 24 | } 25 | 26 | getEmailVerified() { 27 | return this.emailVerified 28 | } 29 | 30 | getPassword() { 31 | return this.password 32 | } 33 | 34 | getImage() { 35 | return this.image; 36 | } 37 | 38 | getRole() { 39 | return this.role; 40 | } 41 | 42 | initFromDataObject(data: any) { 43 | 44 | if (!stringUtils.isUndefinedEmptyorNull(data.id)) { 45 | this.id = data.id 46 | } 47 | 48 | if (!stringUtils.isUndefinedEmptyorNull(data.name)) { 49 | this.name = data.name 50 | } 51 | 52 | if (!stringUtils.isUndefinedEmptyorNull(data.email)) { 53 | this.email = data.email 54 | } 55 | 56 | if (!stringUtils.isUndefinedEmptyorNull(data.emailVerified)) { 57 | this.emailVerified = data.emailVerified 58 | } 59 | 60 | if (!stringUtils.isUndefinedEmptyorNull(data.password)) { 61 | this.password = data.password 62 | } 63 | 64 | if (!stringUtils.isUndefinedEmptyorNull(data.role)) { 65 | if(data.role === UserRoleEnum.ADMIN) { 66 | this.role = UserRoleEnum.ADMIN 67 | } 68 | 69 | else if(data.role === UserRoleEnum.USER) { 70 | this.role = UserRoleEnum.USER 71 | } 72 | } 73 | 74 | if (!stringUtils.isUndefinedEmptyorNull(data.image)) { 75 | this.image = data.image 76 | } 77 | } 78 | 79 | toJson() { 80 | // haven't added password to maintian the privacy 81 | let json: any = {} 82 | 83 | if (!stringUtils.isUndefinedEmptyorNull(this.id)) { 84 | json['id'] = this.id 85 | } 86 | 87 | if (!stringUtils.isUndefinedEmptyorNull(this.name)) { 88 | json['name'] = this.name 89 | } 90 | 91 | if (!stringUtils.isUndefinedEmptyorNull(this.email)) { 92 | json['email'] = this.email 93 | } 94 | 95 | if (!stringUtils.isUndefinedEmptyorNull(this.emailVerified)) { 96 | json['emailVerified'] = this.emailVerified 97 | } 98 | 99 | if (!stringUtils.isUndefinedEmptyorNull(this.image)) { 100 | json['image'] = this.image 101 | } 102 | 103 | return json; 104 | } 105 | 106 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Nextjs Boilerplate · [![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/buildFast10x/Nextjs-Boilerplate/blob/main/LICENSE) [![PRs Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/buildFast10x/Nextjs-Boilerplate/blob/main/contribution.md) 3 | 4 | Welcome to the Next.js Open Source Boilerplate by [Buildfast](https://buildfast.co.in/)! This boilerplate is designed to kickstart your Next.js projects with a well-organized and extensible foundation. 5 | 6 | 7 | ## Features 8 | 9 | ✅ Email Login and Signup Using Next-Auth
10 | ✅ Stripe Subscription Payments
11 | ✅ Resend
12 | ✅ Social Logins
13 | ✅ Email Verification
14 | ✅ Forget Password
15 | ⏩ User Roles
16 | ⏩ 2FA
17 | ⏩ Admin Dashboard with multiple features
18 | ⏩ blogs
19 | 20 | 21 | ## Tech Stack 22 | 23 | **Frontend**: Shadcdn, Tailwind css 24 | 25 | **Framework**: Nextjs, Prisma 26 | 27 | **Database**: Mysql 28 | 29 | **Payment Gateway**: Stripe 30 | 31 | 32 | 33 | ## Contributing 34 | 35 | Contributions are always welcome! 36 | 37 | See `contributing.md` for ways to get started. 38 | 39 | Please adhere to this project's `code of conduct`. 40 | 41 | ### Project structure 42 | 43 | ```shell 44 | . 45 | ├── README.md # README file 46 | ├── .vscode # VSCode configuration for Debugging in local 47 | ├── contribution.md # Steps to follow to contribute to project 48 | ├── app # Next JS App (App Router) 49 | │ └── api # api fuctions and routes 50 | ├── components # React components 51 | │ └── internal # Internal Build Components 52 | │ └── ui # Shadcdn Components 53 | ├── controllers # Handles Database Queries 54 | ├── data # Class Objects of data 55 | ├── helpers # Helper Classes - Repetative Functions 56 | ├── libs # 3rd party libraries configuration 57 | ├── next-auth # Next Auth Configration 58 | ├── prisma # Prisma Configration 59 | ├── public # Public assets folder 60 | ├── redux # Redux Store 61 | ├── utils # Utility Function Class 62 | ├── middleware.ts # Middleware Functionality code 63 | ├── tailwind.config.js # Tailwind CSS configuration 64 | └── tsconfig.json # TypeScript configuration 65 | ``` 66 | 67 | 68 | ## Getting Started 69 | 70 | ### 1. Clone the repo. 71 | 72 | ```shell 73 | git clone https://github.com/buildFast10x/Nextjs-Boilerplate.git 74 | ``` 75 | 76 | ### 2. Fill the .env variables 77 | 78 | Here is the .env file [example](https://github.com/buildFast10x/Nextjs-Boilerplate/blob/main/.env.example). 79 | 80 | 81 | ### 3. Setup the Database 82 | 83 | ```shell 84 | npx prisma generate 85 | ``` 86 | ```shell 87 | npx prisma db push 88 | ``` 89 | 90 | ### 4. Run Project 91 | 92 | ```shell 93 | npm install 94 | ``` 95 | ```shell 96 | npm run dev 97 | ``` -------------------------------------------------------------------------------- /controllers/SubscriptionController.ts: -------------------------------------------------------------------------------- 1 | import errorHandler from "@/helpers/errorHandler"; 2 | import prisma from "@/lib/prisma"; 3 | import jsonUtilsImpl from "@/utils/jsonUtils"; 4 | // import jsonUtilsImpl from "@/utils/jsonUtils"; 5 | 6 | export default class SubscriptionController { 7 | 8 | async create(userId: string, subscription: any) { 9 | try { 10 | const data = { 11 | "userId": userId, 12 | "stripeSubscriptionId": subscription.id, 13 | "stripeCustomerId": subscription.customer as string, 14 | "stripePriceId": subscription.items.data[0].price.id, 15 | "stripeCurrentPeriodEnd": new Date( 16 | subscription.current_period_end * 1000 17 | ), 18 | } 19 | const reuult = await prisma.subscription.create({data}); 20 | } catch (e) { 21 | const error = new errorHandler(); 22 | error.internalServerError(e); 23 | return error.generateError(); 24 | } 25 | } 26 | 27 | async findUnique(userIdData: string) { 28 | try { 29 | const result = await prisma.subscription.findUnique({ 30 | where: { 31 | userId: userIdData 32 | } 33 | }) 34 | return result 35 | } catch(e) { 36 | const error = new errorHandler(); 37 | error.internalServerError(e); 38 | return error.generateError(); 39 | } 40 | } 41 | 42 | async update(subscription: any) { 43 | try { 44 | 45 | const dataJson = { 46 | "stripePriceId": subscription.items.data[0].price.id, 47 | "stripeCurrentPeriodEnd": new Date( 48 | subscription.current_period_end * 1000 49 | ), 50 | } 51 | const whereJson = { 52 | "stripeSubscriptionId": subscription.id, 53 | } 54 | 55 | const result = await prisma.subscription.update({ 56 | where: whereJson, 57 | data: dataJson 58 | }) 59 | return result; 60 | 61 | } catch (e) { 62 | const error = new errorHandler(); 63 | error.internalServerError(e); 64 | return error.generateError(); 65 | } 66 | } 67 | 68 | async getSubscription(userId: string) { 69 | try { 70 | 71 | const whereJson = { 72 | "userId": userId 73 | } 74 | 75 | const selectJson = { 76 | "stripeSubscriptionId": true, 77 | "stripeCurrentPeriodEnd": true, 78 | "stripeCustomerId": true, 79 | "stripePriceId": true, 80 | } 81 | 82 | const subscription = await prisma.subscription.findUnique({ 83 | where: whereJson, 84 | select: selectJson 85 | }) 86 | 87 | return subscription; 88 | 89 | } catch (e) { 90 | const error = new errorHandler(); 91 | error.internalServerError(e); 92 | return error.generateError(); 93 | } 94 | } 95 | 96 | } -------------------------------------------------------------------------------- /next-auth/config.ts: -------------------------------------------------------------------------------- 1 | // import { NextAuthOptions, User } from "next-auth"; 2 | // import CredentialsProvider from "next-auth/providers/credentials"; 3 | // import GoogleProvider from "next-auth/providers/google"; 4 | // import prisma from "@/lib/prisma"; 5 | // import { compare } from "bcrypt"; 6 | // import { PrismaAdapter } from "@next-auth/prisma-adapter"; 7 | // import configEnv from "@/config" 8 | 9 | // import userImpl from "@/data/user/userImpl"; 10 | // import userController from "@/controllers/UserController"; 11 | 12 | // export const authOptions: NextAuthOptions = { 13 | // adapter: PrismaAdapter(prisma), 14 | // session: { 15 | // strategy: "jwt", 16 | // }, 17 | // providers: [ 18 | // CredentialsProvider({ 19 | // name: "Email", 20 | // credentials: { 21 | // email: { 22 | // label: "Email", 23 | // type: "email", 24 | // }, 25 | // password: { 26 | // label: "password", 27 | // type: "password", 28 | // }, 29 | // }, 30 | 31 | // async authorize(credentials) { 32 | 33 | // if (!credentials?.email || !credentials?.password) { 34 | // return null; 35 | // } 36 | 37 | // const userControllerHandler = new userController(); 38 | // const userDB = await userControllerHandler.getUserByEmail(credentials.email); 39 | // const user = new userImpl(); 40 | // user.initFromDataObject(userDB) 41 | 42 | // if(!user || !user.getPassword()) { 43 | // return null 44 | // } 45 | 46 | // const isPasswordValid = await compare(credentials.password, user.getPassword() || ''); 47 | // if(!isPasswordValid) { 48 | // return null 49 | // } 50 | 51 | // return user.toJson(); 52 | 53 | 54 | 55 | 56 | // // if (!credentials?.email || !credentials?.password) { 57 | // // return null 58 | // // } 59 | 60 | // // const user = await prisma.user.findUnique({ 61 | // // where: { 62 | // // email: credentials.email 63 | // // } 64 | // // }) 65 | 66 | // // if (!user || !user.password) { 67 | // // return null 68 | // // } 69 | // // const isPasswordValid = await compare(credentials.password, user.password) 70 | // // if (!isPasswordValid) { 71 | // // return null 72 | // // } 73 | 74 | // // return { 75 | // // id: String(user.id), 76 | // // email: user.email, 77 | // // name: user.name 78 | // // }; 79 | // }, 80 | // }), 81 | // GoogleProvider({ 82 | // clientId: configEnv.google.clientId!, 83 | // clientSecret: configEnv.google.clientSecret! 84 | // }) 85 | // ], 86 | // // get id in session 87 | 88 | // callbacks: { 89 | // async jwt({ token, user }) { 90 | // if (user) { 91 | // token.id = user.id 92 | // token.name = user.name 93 | // token.email = user.email 94 | // token.picture = user.image 95 | 96 | // } 97 | 98 | // return token; 99 | // }, 100 | // async session({ token, session }) { 101 | // if (token) { 102 | // session.user.id = token.id; 103 | // session.user.name = token.name; 104 | // session.user.email = token.email; 105 | // session.user.image = token.picture; 106 | // } 107 | 108 | // return session; 109 | // }, 110 | // }, 111 | // }; 112 | -------------------------------------------------------------------------------- /contribution.md: -------------------------------------------------------------------------------- 1 | # Contributing to Auth0 projects 2 | 3 | A big welcome and thank you for considering contributing to Nextjs-Boilerplate open source projects! It’s people like you that make it a reality for users in our community. 4 | 5 | Reading and following these guidelines will help us make the contribution process easy and effective for everyone involved. It also communicates that you agree to respect the time of the developers managing and developing these open source projects. In return, we will reciprocate that respect by addressing your issue, assessing changes, and helping you finalize your pull requests. 6 | 7 | ## Quicklinks 8 | 9 | * [Code of Conduct](#code-of-conduct) 10 | * [Getting Started](#getting-started) 11 | * [Issues](#issues) 12 | * [Pull Requests](#pull-requests) 13 | * [Getting Help](#getting-help) 14 | 15 | ## Code of Conduct 16 | 17 | We take our open source community seriously and hold ourselves and other contributors to high standards of communication. By participating and contributing to this project, you agree to uphold our [Code of Conduct](https://github.com/buildFast10x/Nextjs-Boilerplate). 18 | 19 | ## Getting Started 20 | 21 | Contributions are made to this repo via Issues and Pull Requests (PRs). A few general guidelines that cover both: 22 | 23 | - Search for existing Issues and PRs before creating your own. 24 | - We work hard to makes sure issues are handled in a timely manner but, depending on the impact, it could take a while to investigate the root cause. A friendly ping in the comment thread to the submitter or a contributor can help draw attention if your issue is blocking. 25 | 26 | ### Issues 27 | 28 | Issues should be used to report problems with the library, request a new feature, or to discuss potential changes before a PR is created. When you create a new Issue, a template will be loaded that will guide you through collecting and providing the information we need to investigate. 29 | 30 | If you find an Issue that addresses the problem you're having, please add your own reproduction information to the existing issue rather than creating a new one. Adding a [reaction](https://github.blog/2016-03-10-add-reactions-to-pull-requests-issues-and-comments/) can also help be indicating to our maintainers that a particular problem is affecting more than just the reporter. 31 | 32 | ### Pull Requests 33 | 34 | PRs to our libraries are always welcome and can be a quick way to get your fix or improvement slated for the next release. In general, PRs should: 35 | 36 | - Only fix/add the functionality in question **OR** address wide-spread whitespace/style issues, not both. 37 | - Add unit or integration tests for fixed or changed functionality (if a test suite already exists). 38 | - Address a single concern in the least number of changed lines as possible. 39 | - Be accompanied by a complete Pull Request template (loaded automatically when a PR is created). 40 | 41 | For changes that address core functionality or would require breaking changes (e.g. a major release), it's best to open an Issue to discuss your proposal first. This is not required but can save time creating and reviewing changes. 42 | 43 | In general, we follow the ["fork-and-pull" Git workflow](https://github.com/susam/gitpr) 44 | 45 | 1. Fork the repository to your own Github account 46 | 2. Clone the project to your machine 47 | 3. Create a branch locally with a succinct but descriptive name 48 | 4. Commit changes to the branch 49 | 5. Following any formatting and testing guidelines specific to this repo 50 | 6. Push changes to your fork 51 | 7. Open a PR in our repository and follow the PR template so that we can efficiently review the changes. 52 | 53 | ## Getting Help 54 | 55 | Join us in the [Buildfast Community](https://discord.gg/UFsVfJPBP6) and post your question there. -------------------------------------------------------------------------------- /controllers/UserController.ts: -------------------------------------------------------------------------------- 1 | import errorHandler from "@/helpers/errorHandler"; 2 | import prisma from "@/lib/prisma"; 3 | import jsonUtilsImpl from "@/utils/jsonUtils"; 4 | // import jsonUtilsImpl from "@/utils/jsonUtils"; 5 | 6 | export default class userController { 7 | async create(name: string, password: string, email: string) { 8 | try { 9 | const userJson: any = { 10 | "name": name, 11 | "password": password, 12 | "email": email 13 | }; 14 | 15 | const result = await prisma.user.create({ 16 | data: userJson 17 | }) 18 | 19 | return result 20 | } catch (e) { 21 | return e; 22 | } 23 | } 24 | 25 | async isEmailexists(email: string) { 26 | const result = await this.getUserByEmail(email); 27 | return !jsonUtilsImpl.isEmpty(result) === true; 28 | } 29 | 30 | async getUserByEmail(email: string) { 31 | try{ 32 | const whereJson = { 33 | "email": email 34 | } 35 | 36 | const finalQuery = { 37 | where: whereJson 38 | } 39 | const result = await prisma.user.findUnique(finalQuery); 40 | return result; 41 | } catch (e: any) { 42 | const error = new errorHandler(); 43 | error.internalServerError("Email does not found"); 44 | return error.generateError(); 45 | } 46 | 47 | } 48 | 49 | async getUserById(id: string) { 50 | try { 51 | const whereJson = { 52 | "id": id 53 | } 54 | 55 | const finalQuery = { 56 | where: whereJson 57 | } 58 | const result = await prisma.user.findUnique(finalQuery); 59 | return result; 60 | } catch (e) { 61 | return e; 62 | } 63 | } 64 | 65 | async setEmailVerifiedById(id: string) { 66 | try { 67 | const dataJson = { 68 | "emailVerified": new Date() 69 | } 70 | const whereJson = { 71 | "id": id 72 | } 73 | 74 | const finalQuery = { 75 | data: dataJson, 76 | where: whereJson 77 | } 78 | const result = await prisma.user.update(finalQuery); 79 | return result; 80 | } catch (e) { 81 | return e; 82 | } 83 | } 84 | 85 | async updateData(id: string, email: string) { 86 | try { 87 | const dataJson = { 88 | "emailVerified": new Date(), 89 | "email": email 90 | } 91 | const whereJson = { 92 | "id": id 93 | } 94 | 95 | const finalQuery = { 96 | data: dataJson, 97 | where: whereJson 98 | } 99 | const result = await prisma.user.update(finalQuery); 100 | return result; 101 | } catch (e) { 102 | return e; 103 | } 104 | } 105 | 106 | async updatePassword(id: string, password: string) { 107 | try { 108 | const dataJson = { 109 | "password": password 110 | } 111 | const whereJson = { 112 | "id": id 113 | } 114 | 115 | const finalQuery = { 116 | data: dataJson, 117 | where: whereJson 118 | } 119 | const result = await prisma.user.update(finalQuery); 120 | return result; 121 | } catch (e) { 122 | return e; 123 | } 124 | } 125 | 126 | } -------------------------------------------------------------------------------- /data/subscription/subscriptionImpl.ts: -------------------------------------------------------------------------------- 1 | import stringUtils from "@/utils/stringUtils"; 2 | import userImpl from "../user/userImpl"; 3 | import userInterface from "../user/userInterface"; 4 | import subscriptionInterface from "./subscriptionInterface"; 5 | 6 | const DAY_IN_MS = 86_400_000; 7 | 8 | export default class subscriptionImpl implements subscriptionInterface { 9 | id: string = '' 10 | user: userInterface = new userImpl() ; 11 | stripeCustomerId?: string 12 | stripeSubscriptionId?: string 13 | stripePriceId?: string 14 | stripeCurrentPeriodEnd?: string 15 | isValid?: boolean; 16 | 17 | initFromDataObject(data: any, userSession: any) { 18 | 19 | if (stringUtils.isUndefinedEmptyorNull(data) && stringUtils.isUndefinedEmptyorNull(userSession)) { 20 | return null; 21 | } 22 | 23 | if (!stringUtils.isUndefinedEmptyorNull(data?.id)) { 24 | this.id = data?.id 25 | } 26 | 27 | if (!stringUtils.isUndefinedEmptyorNull(data?.stripeCustomerId)) { 28 | this.stripeCustomerId = data?.stripeCustomerId 29 | } 30 | 31 | if (!stringUtils.isUndefinedEmptyorNull(data?.stripeSubscriptionId)) { 32 | this.stripeSubscriptionId = data?.stripeSubscriptionId 33 | } 34 | 35 | if (!stringUtils.isUndefinedEmptyorNull(data?.stripePriceId)) { 36 | this.stripePriceId = data?.stripePriceId 37 | } 38 | 39 | if (!stringUtils.isUndefinedEmptyorNull(data?.stripeCurrentPeriodEnd)) { 40 | this.stripeCurrentPeriodEnd = data?.stripeCurrentPeriodEnd 41 | } 42 | 43 | if (!stringUtils.isUndefinedEmptyorNull(userSession)) { 44 | this.user.initFromDataObject(userSession.user); 45 | } 46 | } 47 | 48 | getStripeCustomerId() { 49 | return this.stripeCustomerId || ''; 50 | } 51 | 52 | getUser() { 53 | return this.user; 54 | } 55 | 56 | getId() { 57 | return this.id; 58 | } 59 | 60 | getStripePriceId() { 61 | return this.stripePriceId || ''; 62 | } 63 | 64 | getStripeCurrentPeriodEnd() { 65 | return new Date(this.stripeCurrentPeriodEnd || ''); 66 | } 67 | 68 | setIsValid(isValid: boolean) { 69 | this.isValid = isValid 70 | } 71 | 72 | getIsValid() { 73 | return this.isValid; 74 | } 75 | 76 | isSubsciptionValid() { 77 | const isValid: boolean = this.getStripePriceId() && 78 | this.getStripeCurrentPeriodEnd()?.getTime()! + DAY_IN_MS > Date.now() || false; 79 | this.setIsValid(isValid); 80 | } 81 | 82 | toJson() { 83 | 84 | let json: any = {} 85 | 86 | if (!stringUtils.isUndefinedEmptyorNull(this.id)) { 87 | json['id'] = this.id 88 | } 89 | 90 | if (!stringUtils.isUndefinedEmptyorNull(this.user)) { 91 | json['user'] = this.user.toJson(); 92 | } 93 | 94 | if (!stringUtils.isUndefinedEmptyorNull(this.stripeCustomerId)) { 95 | json['stripeCustomerId'] = this.stripeCustomerId 96 | } 97 | 98 | if (!stringUtils.isUndefinedEmptyorNull(this.stripeSubscriptionId)) { 99 | json['stripeSubscriptionId'] = this.stripeSubscriptionId 100 | } 101 | 102 | if (!stringUtils.isUndefinedEmptyorNull(this.stripePriceId)) { 103 | json['stripePriceId'] = this.stripePriceId 104 | } 105 | 106 | if (!stringUtils.isUndefinedEmptyorNull(this.stripeCurrentPeriodEnd)) { 107 | json['stripeCurrentPeriodEnd'] = this.stripeCurrentPeriodEnd 108 | } 109 | 110 | if (!stringUtils.isUndefinedEmptyorNull(this.isValid)) { 111 | json['isValid'] = this.isValid 112 | } 113 | 114 | return json; 115 | } 116 | } -------------------------------------------------------------------------------- /app/dashboard/page.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from "@/components/ui/button"; 2 | import { 3 | Card, 4 | CardContent, 5 | CardDescription, 6 | CardFooter, 7 | CardHeader, 8 | CardTitle, 9 | } from "@/components/ui/card"; 10 | // import { authOptions } from "@/next-auth/config"; 11 | import { subscriptionPlansData } from "@/utils/subscriptionPlans"; 12 | import { CheckCircle2Icon } from "lucide-react"; 13 | // import { getServerSession } from "next-auth"; 14 | import Link from "next/link"; 15 | import { redirect } from "next/navigation"; 16 | import Mail from "./component/mail"; 17 | import { ManageUserSubscriptionButton } from "./component/manage-subscription"; 18 | import UserMenu from "./component/user-menu"; 19 | import checkSubscription from "@/lib/subscription"; 20 | import subscriptionInterface from "@/data/subscription/subscriptionInterface"; 21 | import { getCurrentUser } from "@/next-auth/utils"; 22 | 23 | export default async function Dashboard() { 24 | const session: any = await getCurrentUser(); 25 | const subscription: subscriptionInterface = await checkSubscription(); 26 | if (!session) { 27 | redirect("/"); 28 | } 29 | return ( 30 |
31 |
32 |
33 | 34 |
35 | 36 |
37 | {subscriptionPlansData.map((plan) => ( 38 | 44 | {plan.stripePriceId === subscription.stripePriceId ? ( 45 |
46 |
47 | Current Plan 48 |
49 |
50 | ) : null} 51 | 52 | {plan.name} 53 | {plan.description} 54 | 55 | 56 |
57 |

58 | ${plan.price} / month 59 |

60 |
61 |
    62 | {plan.features.map((feature, i) => ( 63 |
  • 64 | 65 | {feature} 66 |
  • 67 | ))} 68 |
69 |
70 | 71 | {session?.user.email ? ( 72 | 80 | ) : ( 81 |
82 | 83 | 86 | 87 |
88 | )} 89 |
90 |
91 | ))} 92 |
93 | 94 |
95 | ); 96 | } 97 | -------------------------------------------------------------------------------- /components/ui/form.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | import * as LabelPrimitive from "@radix-ui/react-label" 3 | import { Slot } from "@radix-ui/react-slot" 4 | import { 5 | Controller, 6 | ControllerProps, 7 | FieldPath, 8 | FieldValues, 9 | FormProvider, 10 | useFormContext, 11 | } from "react-hook-form" 12 | 13 | import { cn } from "@/lib/utils" 14 | import { Label } from "@/components/ui/label" 15 | 16 | const Form = FormProvider 17 | 18 | type FormFieldContextValue< 19 | TFieldValues extends FieldValues = FieldValues, 20 | TName extends FieldPath = FieldPath 21 | > = { 22 | name: TName 23 | } 24 | 25 | const FormFieldContext = React.createContext( 26 | {} as FormFieldContextValue 27 | ) 28 | 29 | const FormField = < 30 | TFieldValues extends FieldValues = FieldValues, 31 | TName extends FieldPath = FieldPath 32 | >({ 33 | ...props 34 | }: ControllerProps) => { 35 | return ( 36 | 37 | 38 | 39 | ) 40 | } 41 | 42 | const useFormField = () => { 43 | const fieldContext = React.useContext(FormFieldContext) 44 | const itemContext = React.useContext(FormItemContext) 45 | const { getFieldState, formState } = useFormContext() 46 | 47 | const fieldState = getFieldState(fieldContext.name, formState) 48 | 49 | if (!fieldContext) { 50 | throw new Error("useFormField should be used within ") 51 | } 52 | 53 | const { id } = itemContext 54 | 55 | return { 56 | id, 57 | name: fieldContext.name, 58 | formItemId: `${id}-form-item`, 59 | formDescriptionId: `${id}-form-item-description`, 60 | formMessageId: `${id}-form-item-message`, 61 | ...fieldState, 62 | } 63 | } 64 | 65 | type FormItemContextValue = { 66 | id: string 67 | } 68 | 69 | const FormItemContext = React.createContext( 70 | {} as FormItemContextValue 71 | ) 72 | 73 | const FormItem = React.forwardRef< 74 | HTMLDivElement, 75 | React.HTMLAttributes 76 | >(({ className, ...props }, ref) => { 77 | const id = React.useId() 78 | 79 | return ( 80 | 81 |
82 | 83 | ) 84 | }) 85 | FormItem.displayName = "FormItem" 86 | 87 | const FormLabel = React.forwardRef< 88 | React.ElementRef, 89 | React.ComponentPropsWithoutRef 90 | >(({ className, ...props }, ref) => { 91 | const { error, formItemId } = useFormField() 92 | 93 | return ( 94 |