├── .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 |
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 |
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 · [](https://github.com/buildFast10x/Nextjs-Boilerplate/blob/main/LICENSE) [](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 |
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 |
84 | Add Email to Subscribe
85 |
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 |
100 | )
101 | })
102 | FormLabel.displayName = "FormLabel"
103 |
104 | const FormControl = React.forwardRef<
105 | React.ElementRef,
106 | React.ComponentPropsWithoutRef
107 | >(({ ...props }, ref) => {
108 | const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
109 |
110 | return (
111 |
122 | )
123 | })
124 | FormControl.displayName = "FormControl"
125 |
126 | const FormDescription = React.forwardRef<
127 | HTMLParagraphElement,
128 | React.HTMLAttributes
129 | >(({ className, ...props }, ref) => {
130 | const { formDescriptionId } = useFormField()
131 |
132 | return (
133 |
139 | )
140 | })
141 | FormDescription.displayName = "FormDescription"
142 |
143 | const FormMessage = React.forwardRef<
144 | HTMLParagraphElement,
145 | React.HTMLAttributes
146 | >(({ className, children, ...props }, ref) => {
147 | const { error, formMessageId } = useFormField()
148 | const body = error ? String(error?.message) : children
149 |
150 | if (!body) {
151 | return null
152 | }
153 |
154 | return (
155 |
161 | {body}
162 |
163 | )
164 | })
165 | FormMessage.displayName = "FormMessage"
166 |
167 | export {
168 | useFormField,
169 | Form,
170 | FormItem,
171 | FormLabel,
172 | FormControl,
173 | FormDescription,
174 | FormMessage,
175 | FormField,
176 | }
177 |
--------------------------------------------------------------------------------
/components/internal/Forms/NewPasswordForm.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 | import React, { startTransition, useState } from 'react'
3 | import { zodResolver } from "@hookform/resolvers/zod"
4 | import { useForm } from "react-hook-form"
5 | import { z } from "zod"
6 |
7 | import { Button } from "@/components/ui/button"
8 | import {
9 | Form,
10 | FormControl,
11 | FormDescription,
12 | FormField,
13 | FormItem,
14 | FormLabel,
15 | FormMessage,
16 | } from "@/components/ui/form"
17 | import { Input } from "@/components/ui/input"
18 | import userInterface from '@/data/user/userInterface'
19 | import axiosInstance from '@/lib/axios'
20 | import AllAPIRouteMapping from '@/utils/AllAPIRouteMapping'
21 | import { useRouter } from 'next/navigation';
22 | import { useSearchParams } from 'next/navigation'
23 | import { signIn } from 'next-auth/react'
24 | import {
25 | Card,
26 | CardContent,
27 | CardDescription,
28 | CardFooter,
29 | CardHeader,
30 | CardTitle,
31 | } from "@/components/ui/card"
32 | import { Label } from "@/components/ui/label"
33 | import Link from "next/link"
34 | import { DEFAULT_LOGIN_REDIRECT } from '@/routes'
35 | import { FormError } from '../FormError'
36 | import { FormSuccess } from '../FormSuccess'
37 | import { AuthError } from 'next-auth'
38 | import { login } from '@/actions/login'
39 | import { newPasswordSchema } from '@/schemas'
40 |
41 | // const newPasswordSchema = z.object({
42 | // email: z.string().email(),
43 | // password: z.string(),
44 | // })
45 |
46 | export default function NewPasswordForm() {
47 |
48 | const searchParams = useSearchParams();
49 | const token = searchParams.get("token");
50 |
51 | const [error, setError] = useState("");
52 | const [success, setSuccess] = useState("");
53 |
54 | const form = useForm>({
55 | resolver: zodResolver(newPasswordSchema),
56 | defaultValues: {
57 | password: "",
58 | },
59 | })
60 |
61 | // 2. Define a submit handler.
62 | async function onSubmit(values: z.infer) {
63 | // Do something with the form values.
64 | // ✅ This will be type-safe and validated.
65 | // console.log("value", values)
66 | setError("");
67 | setSuccess("");
68 |
69 | const axios = new axiosInstance();
70 | const data: any = {
71 | "password": values.password,
72 | "token": token
73 | }
74 | axios.setPayload(data);
75 | const response = await axios.makeCall(AllAPIRouteMapping.users.updatePassword.apiPath, AllAPIRouteMapping.users.updatePassword.method);
76 | if (response?.success) {
77 | setSuccess(response?.message)
78 | setError("")
79 | } else {
80 | setError(response?.error)
81 | setSuccess("")
82 | }
83 |
84 | // startTransition(() => {
85 | // login(values)
86 | // .then((data) => {
87 | // setError(data?.error);
88 | // setSuccess(data?.success);
89 | // // TODO: Add when we add 2FA
90 |
91 | // });
92 | // });
93 | }
94 |
95 |
96 | return (
97 | <>
98 |
99 |
100 | Reset Email
101 |
102 | Enter your email below to reset your account
103 |
104 |
105 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 | >
147 |
148 | )
149 | }
150 |
151 |
--------------------------------------------------------------------------------
/components/internal/Forms/ResetForm.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 | import React, { startTransition, useState } from 'react'
3 | import { zodResolver } from "@hookform/resolvers/zod"
4 | import { useForm } from "react-hook-form"
5 | import { z } from "zod"
6 |
7 | import { Button } from "@/components/ui/button"
8 | import {
9 | Form,
10 | FormControl,
11 | FormDescription,
12 | FormField,
13 | FormItem,
14 | FormLabel,
15 | FormMessage,
16 | } from "@/components/ui/form"
17 | import { Input } from "@/components/ui/input"
18 | import userInterface from '@/data/user/userInterface'
19 | import axiosInstance from '@/lib/axios'
20 | import AllAPIRouteMapping from '@/utils/AllAPIRouteMapping'
21 | import { useRouter } from 'next/navigation';
22 | import { useSearchParams } from 'next/navigation'
23 | import { signIn } from 'next-auth/react'
24 | import {
25 | Card,
26 | CardContent,
27 | CardDescription,
28 | CardFooter,
29 | CardHeader,
30 | CardTitle,
31 | } from "@/components/ui/card"
32 | import { Label } from "@/components/ui/label"
33 | import Link from "next/link"
34 | import { DEFAULT_LOGIN_REDIRECT } from '@/routes'
35 | import { FormError } from '../FormError'
36 | import { FormSuccess } from '../FormSuccess'
37 | import { AuthError } from 'next-auth'
38 | import { login } from '@/actions/login'
39 | import { resetFormSchema } from '@/schemas'
40 |
41 | // const resetFormSchema = z.object({
42 | // email: z.string().email(),
43 | // password: z.string(),
44 | // })
45 |
46 | export default function ResetForm() {
47 |
48 | const searchParams = useSearchParams();
49 | const router = useRouter();
50 | const callbackUrl = searchParams.get("callbackUrl");
51 | const urlError = searchParams.get("error") === "OAuthAccountNotLinked"
52 | ? "Email already in use with Different providor!" : "";
53 |
54 | const [error, setError] = useState("");
55 | const [success, setSuccess] = useState("");
56 |
57 | const form = useForm>({
58 | resolver: zodResolver(resetFormSchema),
59 | defaultValues: {
60 | email: "",
61 | },
62 | })
63 |
64 | // 2. Define a submit handler.
65 | async function onSubmit(values: z.infer) {
66 | // Do something with the form values.
67 | // ✅ This will be type-safe and validated.
68 | // console.log("value", values)
69 | setError("");
70 | setSuccess("");
71 |
72 | const axios = new axiosInstance();
73 | axios.setPayload(values);
74 | const response = await axios.makeCall(AllAPIRouteMapping.users.resetPasswordMail.apiPath, AllAPIRouteMapping.users.resetPasswordMail.method);
75 | if (response?.success) {
76 | setSuccess(response?.message)
77 | setError("")
78 | } else {
79 | setError(response?.error)
80 | setSuccess("")
81 | }
82 |
83 | // startTransition(() => {
84 | // login(values)
85 | // .then((data) => {
86 | // setError(data?.error);
87 | // setSuccess(data?.success);
88 | // // TODO: Add when we add 2FA
89 |
90 | // });
91 | // });
92 | }
93 |
94 |
95 | return (
96 | <>
97 |
98 |
99 | Reset Email
100 |
101 | Enter your email below to reset your account
102 |
103 |
104 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 | >
149 |
150 | )
151 | }
152 |
153 |
--------------------------------------------------------------------------------
/components/internal/Forms/SignupForm.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 | import React, { useState } from 'react'
3 | import { zodResolver } from "@hookform/resolvers/zod"
4 | import { useForm } from "react-hook-form"
5 | import { z } from "zod"
6 |
7 | import { Button } from "@/components/ui/button"
8 | import {
9 | Form,
10 | FormControl,
11 | FormDescription,
12 | FormField,
13 | FormItem,
14 | FormLabel,
15 | FormMessage,
16 | } from "@/components/ui/form"
17 | import { Input } from "@/components/ui/input"
18 | import userInterface from '@/data/user/userInterface'
19 | import axiosInstance from '@/lib/axios'
20 | import AllAPIRouteMapping from '@/utils/AllAPIRouteMapping'
21 | import { FormError } from '../FormError'
22 | import { FormSuccess } from '../FormSuccess'
23 |
24 | import Link from "next/link"
25 | import {
26 | Card,
27 | CardContent,
28 | CardDescription,
29 | CardHeader,
30 | CardTitle,
31 | } from "@/components/ui/card"
32 | import { Label } from "@/components/ui/label"
33 |
34 | const userFormSchema= z.object({
35 | name: z.string(),
36 | email: z.string().email(),
37 | password: z.string(),
38 | confirmPassword: z.string()
39 | }).superRefine(({ confirmPassword, password }, ctx) => {
40 | if (confirmPassword !== password) {
41 | ctx.addIssue({
42 | code: "custom",
43 | message: "The passwords did not match"
44 | });
45 | }
46 | });
47 |
48 | export default function SignupForm() {
49 |
50 | const [error, setError] = useState("");
51 | const [success, setSuccess] = useState("");
52 |
53 | const form = useForm>({
54 | resolver: zodResolver(userFormSchema),
55 | defaultValues: {
56 | name: "",
57 | email: "",
58 | password: "",
59 | confirmPassword: ""
60 | },
61 | })
62 |
63 | // 2. Define a submit handler.
64 | async function onSubmit(values: z.infer) {
65 | // Do something with the form values.
66 | // ✅ This will be type-safe and validated.
67 | // console.log("value", values)
68 |
69 | const axios = new axiosInstance();
70 | axios.setPayload(values);
71 | const response = await axios.makeCall(AllAPIRouteMapping.users.add.apiPath, AllAPIRouteMapping.users.add.method);
72 | if (response?.success) {
73 | setSuccess(response?.message)
74 | setError("")
75 | } else {
76 | setError(response?.error)
77 | setSuccess("")
78 | }
79 | }
80 |
81 | return (
82 | <>
83 |
84 |
85 | Sign Up
86 |
87 | Enter your information to create an account
88 |
89 |
90 |
173 |
174 |
175 |
176 |
177 | >
178 | )
179 | }
180 |
--------------------------------------------------------------------------------
/components/ui/dropdown-menu.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
5 | import { Check, ChevronRight, Circle } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const DropdownMenu = DropdownMenuPrimitive.Root
10 |
11 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
12 |
13 | const DropdownMenuGroup = DropdownMenuPrimitive.Group
14 |
15 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal
16 |
17 | const DropdownMenuSub = DropdownMenuPrimitive.Sub
18 |
19 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
20 |
21 | const DropdownMenuSubTrigger = React.forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef & {
24 | inset?: boolean
25 | }
26 | >(({ className, inset, children, ...props }, ref) => (
27 |
36 | {children}
37 |
38 |
39 | ))
40 | DropdownMenuSubTrigger.displayName =
41 | DropdownMenuPrimitive.SubTrigger.displayName
42 |
43 | const DropdownMenuSubContent = React.forwardRef<
44 | React.ElementRef,
45 | React.ComponentPropsWithoutRef
46 | >(({ className, ...props }, ref) => (
47 |
55 | ))
56 | DropdownMenuSubContent.displayName =
57 | DropdownMenuPrimitive.SubContent.displayName
58 |
59 | const DropdownMenuContent = React.forwardRef<
60 | React.ElementRef,
61 | React.ComponentPropsWithoutRef
62 | >(({ className, sideOffset = 4, ...props }, ref) => (
63 |
64 |
73 |
74 | ))
75 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
76 |
77 | const DropdownMenuItem = React.forwardRef<
78 | React.ElementRef,
79 | React.ComponentPropsWithoutRef & {
80 | inset?: boolean
81 | }
82 | >(({ className, inset, ...props }, ref) => (
83 |
92 | ))
93 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
94 |
95 | const DropdownMenuCheckboxItem = React.forwardRef<
96 | React.ElementRef,
97 | React.ComponentPropsWithoutRef
98 | >(({ className, children, checked, ...props }, ref) => (
99 |
108 |
109 |
110 |
111 |
112 |
113 | {children}
114 |
115 | ))
116 | DropdownMenuCheckboxItem.displayName =
117 | DropdownMenuPrimitive.CheckboxItem.displayName
118 |
119 | const DropdownMenuRadioItem = React.forwardRef<
120 | React.ElementRef,
121 | React.ComponentPropsWithoutRef
122 | >(({ className, children, ...props }, ref) => (
123 |
131 |
132 |
133 |
134 |
135 |
136 | {children}
137 |
138 | ))
139 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
140 |
141 | const DropdownMenuLabel = React.forwardRef<
142 | React.ElementRef,
143 | React.ComponentPropsWithoutRef & {
144 | inset?: boolean
145 | }
146 | >(({ className, inset, ...props }, ref) => (
147 |
156 | ))
157 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
158 |
159 | const DropdownMenuSeparator = React.forwardRef<
160 | React.ElementRef,
161 | React.ComponentPropsWithoutRef
162 | >(({ className, ...props }, ref) => (
163 |
168 | ))
169 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
170 |
171 | const DropdownMenuShortcut = ({
172 | className,
173 | ...props
174 | }: React.HTMLAttributes) => {
175 | return (
176 |
180 | )
181 | }
182 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
183 |
184 | export {
185 | DropdownMenu,
186 | DropdownMenuTrigger,
187 | DropdownMenuContent,
188 | DropdownMenuItem,
189 | DropdownMenuCheckboxItem,
190 | DropdownMenuRadioItem,
191 | DropdownMenuLabel,
192 | DropdownMenuSeparator,
193 | DropdownMenuShortcut,
194 | DropdownMenuGroup,
195 | DropdownMenuPortal,
196 | DropdownMenuSub,
197 | DropdownMenuSubContent,
198 | DropdownMenuSubTrigger,
199 | DropdownMenuRadioGroup,
200 | }
201 |
--------------------------------------------------------------------------------
/components/internal/Forms/LoginForm.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 | import React, { startTransition, useState } from 'react'
3 | import { zodResolver } from "@hookform/resolvers/zod"
4 | import { useForm } from "react-hook-form"
5 | import { z } from "zod"
6 |
7 | import { Button } from "@/components/ui/button"
8 | import {
9 | Form,
10 | FormControl,
11 | FormDescription,
12 | FormField,
13 | FormItem,
14 | FormLabel,
15 | FormMessage,
16 | } from "@/components/ui/form"
17 | import { Input } from "@/components/ui/input"
18 | import userInterface from '@/data/user/userInterface'
19 | import axiosInstance from '@/lib/axios'
20 | import AllAPIRouteMapping from '@/utils/AllAPIRouteMapping'
21 | import { useRouter } from 'next/navigation';
22 | import { useSearchParams } from 'next/navigation'
23 | import { signIn } from 'next-auth/react'
24 | import {
25 | Card,
26 | CardContent,
27 | CardDescription,
28 | CardFooter,
29 | CardHeader,
30 | CardTitle,
31 | } from "@/components/ui/card"
32 | import { Label } from "@/components/ui/label"
33 | import Link from "next/link"
34 | import { DEFAULT_LOGIN_REDIRECT } from '@/routes'
35 | import { FormError } from '../FormError'
36 | import { FormSuccess } from '../FormSuccess'
37 | import { AuthError } from 'next-auth'
38 | import { login } from '@/actions/login'
39 | import { loginFormSchema } from '@/schemas'
40 |
41 | // const loginFormSchema = z.object({
42 | // email: z.string().email(),
43 | // password: z.string(),
44 | // })
45 |
46 | export default function LoginForm() {
47 |
48 | const searchParams = useSearchParams();
49 | const router = useRouter();
50 | const callbackUrl = searchParams.get("callbackUrl");
51 | const urlError = searchParams.get("error") === "OAuthAccountNotLinked"
52 | ? "Email already in use with Different providor!": "";
53 |
54 | const [error, setError] = useState("");
55 | const [success, setSuccess] = useState("");
56 |
57 | const form = useForm>({
58 | resolver: zodResolver(loginFormSchema),
59 | defaultValues: {
60 | email: "",
61 | password: "",
62 | },
63 | })
64 |
65 | // 2. Define a submit handler.
66 | async function onSubmit(values: z.infer) {
67 | // Do something with the form values.
68 | // ✅ This will be type-safe and validated.
69 | // console.log("value", values)
70 | setError("");
71 | setSuccess("");
72 | startTransition(() => {
73 | login(values)
74 | .then((data) => {
75 | setError(data?.error);
76 | setSuccess(data?.success);
77 | // TODO: Add when we add 2FA
78 |
79 | });
80 | });
81 | // startTransition(() => {
82 | // try {
83 | // signIn("credentials", values);
84 |
85 | // } catch(e: any) {
86 | // if(e instanceof AuthError) {
87 | // switch (e.type) {
88 | // case "CredentialsSignin":
89 | // setError("Invalid credentials!");
90 | // setSuccess("");
91 | // break
92 | // default:
93 | // setError("Something went wrong!");
94 | // setSuccess("");
95 | // }
96 | // }
97 | // }
98 | // });
99 |
100 |
101 | // // startTransition(() => {
102 | // login(values, callbackUrl)
103 | // .then((data) => {
104 | // // if (data?.error) {
105 | // // form.reset();
106 | // // setError(data.error);
107 | // // }
108 |
109 | // // if (data?.success) {
110 | // // form.reset();
111 | // // setSuccess(data.success);
112 | // // }
113 |
114 | // // if (data?.twoFactor) {
115 | // // setShowTwoFactor(true);
116 | // // }
117 | // console.log("success")
118 | // }).catch(() => console.log("Something is wrong"));
119 | // // });
120 | }
121 |
122 | function handleSocialLogin(provider: "google" | "github") {
123 | signIn(provider, {
124 | callbackUrl: DEFAULT_LOGIN_REDIRECT
125 | })
126 | }
127 |
128 | return (
129 | <>
130 |
131 |
132 | Login
133 |
134 | Enter your email below to login to your account
135 |
136 |
137 |
197 |
198 |
199 |
200 |
201 |
202 |
203 |
204 |
205 | >
206 |
207 | )
208 | }
209 |
210 | export { loginFormSchema };
211 |
--------------------------------------------------------------------------------