├── .editorconfig ├── .env.sample ├── .gitignore ├── .vscode └── settings.json ├── README.md ├── components ├── AuthGuard │ ├── AuthGuard.module.css │ ├── AuthGuard.tsx │ └── index.ts ├── CurrentPlanCard │ ├── CurrentPlanCard.module.css │ ├── CurrentPlanCard.tsx │ └── index.ts ├── Dialog │ ├── Dialog.module.css │ ├── Dialog.tsx │ ├── DialogContent.tsx │ ├── DialogDescription.tsx │ ├── DialogFooter.tsx │ ├── DialogHeader.tsx │ ├── DialogOverlay.tsx │ ├── DialogPortal.tsx │ ├── DialogTitle.tsx │ └── index.ts ├── DialogComponent │ ├── DialogComponent.module.css │ ├── DialogComponent.tsx │ └── index.ts ├── Flex │ ├── Flex.module.css │ ├── Flex.tsx │ └── index.ts ├── Logo │ ├── Logo.module.css │ ├── Logo.tsx │ └── index.ts ├── Menu │ ├── Menu.module.css │ ├── Menu.tsx │ ├── MenuItem.tsx │ └── index.ts ├── PricingComponent │ ├── PricingComponent.module.css │ ├── PricingComponent.tsx │ └── index.ts ├── PricingTable │ ├── PricingCard.tsx │ ├── PricingTable.module.css │ ├── PricingTable.tsx │ └── index.ts ├── SubscriptionGuard │ ├── SubscriptionGuard.module.css │ ├── SubscriptionGuard.tsx │ └── index.ts ├── Table │ ├── Table.module.css │ ├── Table.tsx │ └── index.ts ├── Tabs │ ├── Tab.tsx │ ├── Tabs.module.css │ ├── Tabs.tsx │ └── index.ts ├── UserCard │ ├── UserCard.module.css │ ├── UserCard.tsx │ └── index.ts ├── elements │ ├── Button │ │ ├── Button.module.css │ │ ├── Button.tsx │ │ └── index.ts │ ├── Input │ │ ├── Input.module.css │ │ ├── Input.tsx │ │ └── index.ts │ └── index.ts ├── forms │ ├── ChangePasswordForm │ │ ├── ChangePasswordForm.module.css │ │ ├── ChangePasswordForm.tsx │ │ └── index.ts │ ├── ForgotPasswordForm │ │ ├── ForgotPasswordForm.module.css │ │ ├── ForgotPasswordForm.tsx │ │ └── index.ts │ ├── LoginForm │ │ ├── LoginForm.module.css │ │ ├── LoginForm.tsx │ │ └── index.ts │ ├── OrganizationForm │ │ ├── OrganizationForm.module.css │ │ ├── OrganizationForm.tsx │ │ └── index.ts │ ├── OrganizationMemberInviteForm │ │ ├── OrganizationMemberInviteForm.module.css │ │ ├── OrganizationMemberInviteForm.tsx │ │ └── index.ts │ ├── ProfileForm │ │ ├── ProfileForm.module.css │ │ ├── ProfileForm.tsx │ │ └── index.ts │ ├── RegisterForm │ │ ├── RegisterForm.module.css │ │ ├── RegisterForm.tsx │ │ └── index.ts │ ├── ResetPasswordForm │ │ ├── ResetPasswordForm.module.css │ │ ├── ResetPasswordForm.tsx │ │ └── index.ts │ └── index.ts ├── index.ts └── layouts │ ├── AccountLayout │ ├── AccountLayout.module.css │ ├── AccountLayout.tsx │ └── index.ts │ ├── AuthLayout │ ├── AuthLayout.module.css │ ├── AuthLayout.tsx │ └── index.ts │ ├── MainLayout │ ├── MainLayout.module.css │ ├── MainLayout.tsx │ └── index.ts │ └── OrganizationLayout │ ├── OrganizationLayout.module.css │ ├── OrganizationLayout.tsx │ └── index.ts ├── errors └── index.ts ├── hooks ├── query │ ├── currentUser.tsx │ ├── organizations.tsx │ ├── payments.tsx │ ├── plans.tsx │ ├── subscription.tsx │ └── users.tsx ├── useOnClickOutside.tsx └── useYupValidationResolver.tsx ├── lib ├── axios.ts ├── email │ ├── EmailProvider.ts │ ├── index.ts │ ├── providers │ │ └── SMTPProvider.ts │ └── templates │ │ ├── OrganisationInviteTeamMember.tsx │ │ └── ResetPassword.tsx ├── pagination │ └── index.ts ├── payments │ ├── constants.ts │ └── stripe │ │ ├── index.ts │ │ ├── service.ts │ │ └── webhook.ts └── prisma.ts ├── middlewares ├── auth.ts └── organization.ts ├── next.config.js ├── package-lock.json ├── package.json ├── pages ├── 403.tsx ├── _app.tsx ├── account │ ├── billing │ │ └── index.tsx │ ├── index.tsx │ ├── password │ │ └── index.tsx │ └── profile │ │ └── index.tsx ├── api │ ├── auth │ │ ├── [...nextauth].ts │ │ ├── change-password.ts │ │ ├── forgot.ts │ │ ├── register.ts │ │ └── reset.ts │ ├── checkout │ │ └── index.ts │ ├── hello.ts │ ├── invitations │ │ └── accept.ts │ ├── organizations │ │ ├── [id] │ │ │ ├── index.ts │ │ │ ├── invitations │ │ │ │ ├── cancel.ts │ │ │ │ ├── index.ts │ │ │ │ └── send.ts │ │ │ ├── member.ts │ │ │ ├── members │ │ │ │ ├── [memberId] │ │ │ │ │ └── index.ts │ │ │ │ └── index.ts │ │ │ └── subscription │ │ │ │ └── index.ts │ │ └── index.ts │ ├── plans │ │ └── index.ts │ ├── products │ │ └── index.ts │ ├── subscription │ │ ├── index.ts │ │ └── manage.ts │ ├── users │ │ ├── currentUser.ts │ │ └── index.ts │ └── webhook │ │ └── stripe.ts ├── auth │ ├── forgot │ │ └── index.tsx │ ├── login │ │ └── index.tsx │ ├── register │ │ └── index.tsx │ └── reset-password │ │ └── index.tsx ├── dashboard │ └── index.tsx ├── index.tsx ├── invitation │ └── index.tsx ├── organizations │ ├── [organizationId] │ │ ├── billing │ │ │ └── index.tsx │ │ ├── index.tsx │ │ └── members │ │ │ └── index.tsx │ └── index.tsx ├── pricing │ └── index.tsx ├── products │ └── index.tsx └── users │ └── index.tsx ├── plop-templates ├── Component │ ├── Component.js.hbs │ ├── Component.module.css.hbs │ ├── Component.test.js.hbs │ └── index.js.hbs ├── Form │ ├── Form.js.hbs │ ├── Form.module.css.hbs │ ├── Form.test.js.hbs │ └── index.js.hbs ├── hook.js.hbs └── injectable-index.js.hbs ├── plopfile.js ├── postcss.config.js ├── prisma └── schema.prisma ├── public ├── favicon.ico └── vercel.svg ├── styles └── globals.css ├── tailwind.config.js ├── tsconfig.json ├── types └── next-auth.d.ts └── utils ├── HashUtil.ts └── pricing.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_style = space 5 | indent_size = 2 6 | charset = utf-8 7 | trim_trailing_whitespace = true 8 | insert_final_newline = true -------------------------------------------------------------------------------- /.env.sample: -------------------------------------------------------------------------------- 1 | DATABASE_URL= 2 | 3 | NEXTAUTH_URL= 4 | NEXTAUTH_URL_INTERNAL= 5 | SECRET= 6 | 7 | GITHUB_ID= 8 | GITHUB_SECRET= 9 | 10 | MAIL_DOMAIN= 11 | MAIL_GUN_KEY= 12 | 13 | 14 | SMTP_HOST= 15 | SMTP_PORT= 16 | SMTP_USERNAME= 17 | SMTP_PASSWORD= 18 | 19 | FROM_EMAIL= 20 | 21 | STRIPE_WEBHOOK_SECRET= 22 | STRIPE_SECRET_KEY= 23 | NEXT_PUBLIC_STRIPE_PUBLISHABLE_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 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env*.local 30 | 31 | # vercel 32 | .vercel 33 | 34 | # typescript 35 | *.tsbuildinfo 36 | next-env.d.ts 37 | 38 | .env 39 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.insertSpaces": true, 3 | "editor.tabSize": 2 4 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Next.js Serverless Boilerplate 2 | 3 | ## Introduction 4 | 5 | This boilerplate provides a starting point for serverless web application development using Next.js. It includes popular tools and frameworks such as React, TypeScript, and Vercel. 6 | 7 | ## Features 8 | 9 | - Next.js for server-side rendering and client-side rendering 10 | - React for building user interfaces 11 | - TypeScript for type safety 12 | - Vercel for serverless functions 13 | - Stripe API for handling payments 14 | - NextAuth for authentication and authorization 15 | - React Query and React Table for data management and visualization 16 | - Yup for form validation 17 | - bcryptjs for password hashing 18 | 19 | ## Prerequisites 20 | 21 | - Node.js and NPM installed 22 | - A Vercel account for deploying serverless functions 23 | - A Stripe account for payment processing 24 | 25 | ## Getting Started 26 | 27 | 1. Clone this repository to your local machine. 28 | 2. Install dependencies by running `npm install`. 29 | 3. Configure the Stripe API keys in a `.env.local` file. 30 | 4. Connect your Vercel account to the project using `vercel login` and `vercel link`. 31 | 5. Run `npm run dev` to start the development server. 32 | 33 | ## Usage 34 | 35 | This boilerplate includes the following scripts: 36 | 37 | - `dev`: Starts the development server with Next.js. 38 | - `build`: Builds the production-ready assets and files. 39 | - `start`: Starts the production server. 40 | - `deploy`: Deploys the serverless functions to Vercel. 41 | - `remove`: Removes the deployed serverless functions from Vercel. 42 | 43 | ## Conclusion 44 | 45 | This boilerplate provides a solid foundation for building serverless web applications using Next.js and Vercel. It includes popular tools and frameworks to help developers build products quickly and efficiently. Please feel free to customize and modify the code to suit your specific needs. 46 | -------------------------------------------------------------------------------- /components/AuthGuard/AuthGuard.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /components/AuthGuard/AuthGuard.tsx: -------------------------------------------------------------------------------- 1 | import { Role } from '@prisma/client'; 2 | import { useSession } from 'next-auth/react'; 3 | import { useRouter } from 'next/router'; 4 | import React from 'react'; 5 | 6 | export const getLogoutCallbackUrl = (role: string) => { 7 | switch (role) { 8 | case Role.superadmin: 9 | return "/"; 10 | case Role.customer: 11 | return "/"; 12 | default: 13 | return "/"; 14 | } 15 | }; 16 | 17 | interface AuthGuardProps { 18 | children: JSX.Element | JSX.Element[]; 19 | roles: Role[] | undefined; 20 | } 21 | 22 | const AuthGuard: React.FC = (props: AuthGuardProps) => { 23 | const { children, roles } = props; 24 | const { data: session, status } = useSession(); 25 | const router = useRouter(); 26 | 27 | if (status === 'loading') { 28 | return

Loading...

; 29 | } 30 | 31 | 32 | if (status === 'unauthenticated') { 33 | router.replace(`/auth/register?redirect=${router.asPath}`); 34 | return <>; 35 | } 36 | 37 | if (status === 'authenticated') { 38 | const { role: currentUserRole } = session?.user; 39 | if (!roles) return <>{children}; 40 | 41 | if (roles && roles.includes(currentUserRole as Role)) { 42 | return <>{children}; 43 | } else { 44 | router.replace("/403"); 45 | } 46 | } 47 | 48 | return <>; 49 | }; 50 | 51 | AuthGuard.defaultProps = { 52 | 53 | }; 54 | 55 | AuthGuard.propTypes = { 56 | 57 | }; 58 | 59 | export default AuthGuard; 60 | -------------------------------------------------------------------------------- /components/AuthGuard/index.ts: -------------------------------------------------------------------------------- 1 | import AuthGuard from './AuthGuard'; 2 | 3 | export default AuthGuard; 4 | -------------------------------------------------------------------------------- /components/CurrentPlanCard/CurrentPlanCard.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /components/CurrentPlanCard/CurrentPlanCard.tsx: -------------------------------------------------------------------------------- 1 | import { Button } from '@components/elements'; 2 | import { useUserManageSubscriptionBilling, useSubscription } from '@hooks/query/subscription'; 3 | import { getPrice } from '@utils/pricing'; 4 | import React from 'react'; 5 | import PricingComponent from '@components/PricingComponent'; 6 | import { useOrganizationSubscription } from '@hooks/query/organizations'; 7 | 8 | interface CurrentPlanCardProps { 9 | organizationId?: string; 10 | isCreateOrganization?: boolean; 11 | } 12 | 13 | const CurrentPlanCard: React.FC = (props: CurrentPlanCardProps) => { 14 | 15 | const { organizationId, isCreateOrganization } = props; 16 | 17 | const { openBilling, isLoading: apiLoading } = useUserManageSubscriptionBilling(); 18 | const { data: subscriptionData, isLoading } = organizationId ? useOrganizationSubscription({ organizationId }): useSubscription(); 19 | 20 | if (isLoading) { 21 | return <>Please wait...; 22 | } 23 | 24 | if (!subscriptionData || !subscriptionData.subscription) { 25 | return 26 | } 27 | const { subscription } = subscriptionData; 28 | 29 | return ( 30 | <> 31 |
32 |
33 | Active Plan: 34 |
35 | {subscription.product.name} 36 |
37 | {getPrice(subscription.price && subscription.price.unitAmount || 0, subscription.price.currency)}/ 38 | {subscription.price.interval_count && subscription.price.interval_count > 1 ? 39 | subscription.price.interval_count: '' } {subscription.price.interval} 40 |
41 |
42 |
43 |
44 | 45 |
46 |
47 | 48 | ) 49 | }; 50 | 51 | CurrentPlanCard.defaultProps = { 52 | 53 | }; 54 | 55 | CurrentPlanCard.propTypes = { 56 | 57 | }; 58 | 59 | export default CurrentPlanCard; 60 | -------------------------------------------------------------------------------- /components/CurrentPlanCard/index.ts: -------------------------------------------------------------------------------- 1 | import CurrentPlanCard from './CurrentPlanCard'; 2 | 3 | export default CurrentPlanCard; 4 | -------------------------------------------------------------------------------- /components/Dialog/Dialog.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /components/Dialog/Dialog.tsx: -------------------------------------------------------------------------------- 1 | import * as DialogPrimitive from "@radix-ui/react-dialog"; 2 | 3 | const Dialog = DialogPrimitive.Root; 4 | const DialogTrigger = DialogPrimitive.Trigger 5 | 6 | export { 7 | Dialog, 8 | DialogTrigger 9 | }; 10 | -------------------------------------------------------------------------------- /components/Dialog/DialogContent.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as DialogPrimitive from "@radix-ui/react-dialog"; 3 | import cn from 'classnames'; 4 | import DialogPortal from "./DialogPortal"; 5 | import DialogOverlay from "./DialogOverlay"; 6 | 7 | import { AiOutlineClose } from 'react-icons/ai'; 8 | 9 | const DialogContent = React.forwardRef< 10 | React.ElementRef, 11 | React.ComponentPropsWithoutRef 12 | >(({ className, children, ...props }, ref) => ( 13 | 14 | 15 | 24 | {children} 25 | 26 | 27 | 28 | Close 29 | 30 | 31 | 32 | )) 33 | DialogContent.displayName = DialogPrimitive.Content.displayName 34 | 35 | export default DialogContent; 36 | 37 | -------------------------------------------------------------------------------- /components/Dialog/DialogDescription.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as DialogPrimitive from "@radix-ui/react-dialog"; 3 | import cn from 'classnames'; 4 | 5 | const DialogDescription = React.forwardRef< 6 | React.ElementRef, 7 | React.ComponentPropsWithoutRef 8 | >(({ className, ...props }, ref) => ( 9 | 14 | )) 15 | DialogDescription.displayName = DialogPrimitive.Description.displayName; 16 | 17 | export default DialogDescription; 18 | -------------------------------------------------------------------------------- /components/Dialog/DialogFooter.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import cn from 'classnames'; 3 | 4 | const DialogFooter = ({ 5 | className, 6 | ...props 7 | }: React.HTMLAttributes) => ( 8 |
15 | ) 16 | DialogFooter.displayName = "DialogFooter"; 17 | 18 | export default DialogFooter; 19 | -------------------------------------------------------------------------------- /components/Dialog/DialogHeader.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import cn from 'classnames'; 3 | 4 | const DialogHeader = ({ 5 | className, 6 | ...props 7 | }: React.HTMLAttributes) => ( 8 |
15 | ) 16 | DialogHeader.displayName = "DialogHeader"; 17 | 18 | export default DialogHeader; 19 | -------------------------------------------------------------------------------- /components/Dialog/DialogOverlay.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as DialogPrimitive from "@radix-ui/react-dialog"; 3 | import cn from 'classnames'; 4 | 5 | const DialogOverlay = React.forwardRef< 6 | React.ElementRef, 7 | React.ComponentPropsWithoutRef 8 | >(({ className, children, ...props }, ref) => ( 9 | 17 | )) 18 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName; 19 | 20 | export default DialogOverlay; 21 | -------------------------------------------------------------------------------- /components/Dialog/DialogPortal.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as DialogPrimitive from "@radix-ui/react-dialog"; 3 | import cn from 'classnames'; 4 | 5 | const DialogPortal = ({ 6 | className, 7 | children, 8 | ...props 9 | }: DialogPrimitive.DialogPortalProps) => ( 10 | 11 |
12 | {children} 13 |
14 |
15 | ) 16 | DialogPortal.displayName = DialogPrimitive.Portal.displayName; 17 | 18 | 19 | export default DialogPortal; 20 | -------------------------------------------------------------------------------- /components/Dialog/DialogTitle.tsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import * as DialogPrimitive from "@radix-ui/react-dialog"; 3 | import cn from 'classnames'; 4 | 5 | const DialogTitle = React.forwardRef< 6 | React.ElementRef, 7 | React.ComponentPropsWithoutRef 8 | >(({ className, ...props }, ref) => ( 9 | 18 | )) 19 | DialogTitle.displayName = DialogPrimitive.Title.displayName; 20 | 21 | export default DialogTitle; 22 | -------------------------------------------------------------------------------- /components/Dialog/index.ts: -------------------------------------------------------------------------------- 1 | import { Dialog, DialogTrigger } from './Dialog'; 2 | import DialogContent from './DialogContent'; 3 | import DialogHeader from './DialogHeader'; 4 | import DialogFooter from './DialogFooter'; 5 | import DialogTitle from './DialogTitle'; 6 | import DialogDescription from './DialogDescription'; 7 | import DialogPortal from './DialogPortal'; 8 | import DialogOverlay from './DialogOverlay'; 9 | 10 | export { 11 | Dialog, 12 | DialogTrigger, 13 | DialogContent, 14 | DialogHeader, 15 | DialogFooter, 16 | DialogTitle, 17 | DialogDescription, 18 | DialogPortal, 19 | DialogOverlay 20 | } -------------------------------------------------------------------------------- /components/DialogComponent/DialogComponent.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /components/DialogComponent/DialogComponent.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { DialogPortal, DialogTrigger, DialogContent, DialogOverlay, Dialog } from '@components/Dialog'; 3 | import { Button } from '@components/elements'; 4 | interface DialogComponentProps { 5 | children: JSX.Element | JSX.Element[]; 6 | onPointerDownOutside?: boolean; 7 | buttonText?: string | null; 8 | 9 | open?: boolean; 10 | onOpenChange?: (open: boolean) => void; 11 | } 12 | 13 | const DialogComponent: React.FC = (props: DialogComponentProps) => { 14 | const { children, onPointerDownOutside, buttonText, onOpenChange, open } = props; 15 | return ( 16 | 17 | {buttonText && ( 18 | 19 | 20 | 21 | )} 22 | 23 | 24 | 25 | { !onPointerDownOutside ? e.preventDefault(): null }} 27 | > 28 | {children} 29 | 30 | 31 | 32 | ); 33 | }; 34 | 35 | DialogComponent.defaultProps = { 36 | onPointerDownOutside: false, 37 | buttonText: 'Add New Record' 38 | }; 39 | 40 | DialogComponent.propTypes = { 41 | 42 | }; 43 | 44 | export default DialogComponent; 45 | -------------------------------------------------------------------------------- /components/DialogComponent/index.ts: -------------------------------------------------------------------------------- 1 | import DialogComponent from './DialogComponent'; 2 | 3 | export default DialogComponent; 4 | -------------------------------------------------------------------------------- /components/Flex/Flex.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /components/Flex/Flex.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | interface FlexProps { 5 | direction?: 'row' | 'col'; 6 | justifyContent?: 'start' | 'end' | 'center' | 'between' | 'around'; 7 | alignItems?: 'start' | 'end' | 'center' | 'stretch'; 8 | wrap?: 'nowrap' | 'wrap' | 'wrap-reverse'; 9 | gap?: string; 10 | classes?: string; 11 | children?: React.ReactNode; 12 | } 13 | 14 | const Flex: React.FC = (props: FlexProps) => { 15 | const { children, direction = 'row', justifyContent, alignItems, wrap, gap = '0', classes } = props; 16 | return ( 17 |
27 | {children} 28 |
29 | ); 30 | }; 31 | 32 | Flex.defaultProps = { 33 | 34 | }; 35 | 36 | Flex.propTypes = { 37 | 38 | }; 39 | 40 | export default Flex; 41 | -------------------------------------------------------------------------------- /components/Flex/index.ts: -------------------------------------------------------------------------------- 1 | import Flex from './Flex'; 2 | 3 | export default Flex; 4 | -------------------------------------------------------------------------------- /components/Logo/Logo.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /components/Logo/Logo.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface LogoProps { 4 | 5 | } 6 | 7 | const Logo: React.FC = (props: LogoProps) => { 8 | return ( 9 |
10 |
11 | 22 | 23 | Aceternity 24 | 25 |
26 |
27 | ); 28 | }; 29 | 30 | Logo.defaultProps = { 31 | 32 | }; 33 | 34 | Logo.propTypes = { 35 | 36 | }; 37 | 38 | export default Logo; 39 | -------------------------------------------------------------------------------- /components/Logo/index.ts: -------------------------------------------------------------------------------- 1 | import Logo from './Logo'; 2 | 3 | export default Logo; 4 | -------------------------------------------------------------------------------- /components/Menu/Menu.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /components/Menu/Menu.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import MenuItem, { IMenuItem } from './MenuItem'; 3 | 4 | interface MenuProps { 5 | items: IMenuItem[] 6 | } 7 | 8 | const Menu = (props: MenuProps) => { 9 | const { items } = props; 10 | return ( 11 |
    12 | {items.map((item) => )} 13 |
14 | ); 15 | }; 16 | 17 | Menu.defaultProps = { 18 | 19 | }; 20 | 21 | Menu.propTypes = { 22 | 23 | }; 24 | 25 | export default Menu; 26 | -------------------------------------------------------------------------------- /components/Menu/MenuItem.tsx: -------------------------------------------------------------------------------- 1 | import { useRouter } from 'next/router'; 2 | import React from 'react'; 3 | import classNames from 'classnames'; 4 | import Link from 'next/link'; 5 | 6 | export type IMenuItem = { 7 | icon: React.ReactElement; 8 | title: string; 9 | key: string; 10 | url: string; 11 | } 12 | 13 | export interface MenuItemProps { 14 | item: IMenuItem; 15 | } 16 | 17 | const MenuItem = (props: MenuItemProps) => { 18 | const { item } = props; 19 | const router = useRouter(); 20 | 21 | const { icon, title } = item; 22 | 23 | const active = router.pathname.includes(item.url); 24 | const liClasses = classNames( 25 | 'flex w-full p-2 justify-between cursor-pointer items-center mb-2', { 26 | "text-gray-700 hover:text-gray-500": !active, 27 | "bg-gray-100 rounded-md text-gray-800 hover:text-gray-900": active, 28 | }); 29 | 30 | return ( 31 | 32 |
  • 33 |
    34 | {icon} 35 | {title} 36 |
    37 |
  • 38 | 39 | ); 40 | }; 41 | 42 | MenuItem.defaultProps = { 43 | 44 | }; 45 | 46 | MenuItem.propTypes = { 47 | 48 | }; 49 | 50 | export default MenuItem; 51 | -------------------------------------------------------------------------------- /components/Menu/index.ts: -------------------------------------------------------------------------------- 1 | import Menu from './Menu'; 2 | 3 | export default Menu; 4 | -------------------------------------------------------------------------------- /components/PricingComponent/PricingComponent.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /components/PricingComponent/PricingComponent.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | 3 | import { usePlans } from '@hooks/query/plans'; 4 | import AxoisClient from '@lib/axios'; 5 | import { CheckoutBody, CheckoutData } from '@pages/api/checkout'; 6 | import { AxiosResponse } from 'axios'; 7 | import getStripe from '@lib/payments/stripe'; 8 | import PricingTable from '@components/PricingTable'; 9 | import { useSession } from 'next-auth/react'; 10 | import { useRouter } from 'next/router'; 11 | import { SUBSCRIPTION_PLAN } from '@lib/payments/constants'; 12 | import DialogComponent from '@components/DialogComponent'; 13 | import { OrganizationForm } from '@components/forms'; 14 | import { OrganizationFormValues } from '@components/forms/OrganizationForm/OrganizationForm'; 15 | 16 | interface PricingComponentProps { 17 | organizationId?: string | undefined; 18 | isCreateOrganization?: boolean; 19 | } 20 | 21 | const PricingComponent: React.FC = (props: PricingComponentProps) => { 22 | const { organizationId = undefined, isCreateOrganization = false } = props; 23 | 24 | 25 | const { isLoading, data } = usePlans({ isOrganization: organizationId ? true: false }); 26 | const { data: authSession } = useSession(); 27 | const router = useRouter(); 28 | 29 | const [selectedPriceId, setSelectedPriceId] = useState(undefined); 30 | const [organizationFormDialogOpen, setOrganizationFormDialogOpen] = useState(false); 31 | const [loading, setLoading] = useState(false); 32 | 33 | const onClickSubscribe = async (priceId: string | undefined, uniqueIdentifier: string) => { 34 | console.log(priceId, uniqueIdentifier); 35 | if (!authSession) { 36 | router.replace('/auth/login?redirect=/pricing'); 37 | return; 38 | } 39 | 40 | if (uniqueIdentifier === SUBSCRIPTION_PLAN.TEAMS && isCreateOrganization) { 41 | setSelectedPriceId(priceId); 42 | setOrganizationFormDialogOpen(true); 43 | return; 44 | } 45 | 46 | await proceedToCheckout(priceId); 47 | } 48 | 49 | const proceedToCheckout = async (priceId: string | undefined, organizationValues?: OrganizationFormValues | undefined) => { 50 | 51 | const requestObject: CheckoutBody = { 52 | priceId, 53 | organization: organizationValues, 54 | organizationId: organizationId, 55 | }; 56 | 57 | const { data: { session } }: AxiosResponse = await AxoisClient.getInstance().post( 58 | `api/checkout${organizationId ? `?organizationId=${organizationId}`: ''}`, requestObject); 59 | 60 | const stripe = await getStripe(); 61 | const { error } = await stripe!.redirectToCheckout({ 62 | sessionId: session.id, 63 | }); 64 | } 65 | 66 | const onOrganizationFromOpenChange = (value: boolean) => { 67 | setOrganizationFormDialogOpen(value); 68 | } 69 | 70 | const onSubmit = (values: OrganizationFormValues) => { 71 | setLoading(true); 72 | proceedToCheckout(selectedPriceId, values); 73 | } 74 | 75 | return ( 76 |
    77 | 78 | 79 | 80 | {isLoading && <>Loading...} 81 | {data && } 82 |
    83 | ); 84 | }; 85 | 86 | PricingComponent.defaultProps = { 87 | 88 | }; 89 | 90 | PricingComponent.propTypes = { 91 | 92 | }; 93 | 94 | export default PricingComponent; 95 | -------------------------------------------------------------------------------- /components/PricingComponent/index.ts: -------------------------------------------------------------------------------- 1 | import PricingComponent from './PricingComponent'; 2 | 3 | export default PricingComponent; 4 | -------------------------------------------------------------------------------- /components/PricingTable/PricingCard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Plan, Price } from '@pages/api/plans'; 3 | import { getPrice } from '@utils/pricing'; 4 | 5 | interface PricingCardProps { 6 | data: Plan; 7 | currency: string; 8 | interval: string; 9 | 10 | onClickSubscribe: (priceId: string | undefined, uniqueIdentifier: string) => void; 11 | } 12 | 13 | const PricingCard = (props: PricingCardProps) => { 14 | const { data, currency, interval, onClickSubscribe } = props; 15 | 16 | const renderType = (price: Price) => { 17 | switch(price.type) { 18 | case 'recurring': 19 | return price.interval; 20 | case 'one_time': 21 | return 'Lifetime'; 22 | } 23 | } 24 | let price = data.prices.find((price) => price.currency === currency && price.interval === interval); 25 | if (!price) { 26 | price = data.prices[0]; 27 | } 28 | return ( 29 |
    30 |

    {data?.name}

    31 |

    {data.description}

    32 |
    33 | {getPrice(price && price.unitAmount || 0, currency)} 34 | /{renderType(price)} 35 |
    36 | 37 |
      38 |
    • 39 | 40 | 41 | Individual configuration 42 |
    • 43 |
    • 44 | 45 | 46 | No setup, or hidden fees 47 |
    • 48 |
    • 49 | 50 | 51 | Team size: 1 developer 52 |
    • 53 |
    • 54 | 55 | 56 | Premium support: 6 months 57 |
    • 58 |
    • 59 | 60 | 61 | Free updates: 6 months 62 |
    • 63 |
    64 | onClickSubscribe(price?.priceId, data.uniqueIdentifier)} className="cursor-pointer bg-blue-500 text-white hover:bg-primary-700 focus:ring-4 focus:ring-primary-200 font-medium rounded-lg text-sm px-5 py-2.5 text-cente"> 65 | Get started 66 | 67 |
    68 | ); 69 | }; 70 | 71 | PricingCard.defaultProps = { 72 | 73 | }; 74 | 75 | PricingCard.propTypes = { 76 | 77 | }; 78 | 79 | export default PricingCard; 80 | -------------------------------------------------------------------------------- /components/PricingTable/PricingTable.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /components/PricingTable/PricingTable.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import * as Switch from '@radix-ui/react-switch'; 3 | import { Plan } from '@pages/api/plans'; 4 | 5 | import PricingCard from './PricingCard'; 6 | 7 | interface PricingTableProps { 8 | data: Plan[] | undefined 9 | onClickSubscribe: (priceId: string | undefined, uniqueIdentifier: string) => void; 10 | } 11 | 12 | const PricingTable = (props: PricingTableProps) => { 13 | const { data, onClickSubscribe } = props; 14 | const [activeInterval, setActiveInterval] = useState('month'); 15 | const activeCurrency = 'usd'; 16 | 17 | return ( 18 |
    19 |
    20 |
    21 | 24 | setActiveInterval(activeInterval === 'month' ? 'year': 'month')} 26 | className="w-[42px] h-[25px] bg-blackA9 rounded-full relative border focus:shadow-black data-[state=checked]:bg-black outline-none cursor-default" 27 | > 28 | 29 | 30 | 33 |
    34 |
    35 | {data && data.map((plan) => ( 36 | 43 | ))} 44 |
    45 |
    46 |
    47 | ); 48 | }; 49 | 50 | PricingTable.defaultProps = { 51 | 52 | }; 53 | 54 | PricingTable.propTypes = { 55 | 56 | }; 57 | 58 | export default PricingTable; 59 | -------------------------------------------------------------------------------- /components/PricingTable/index.ts: -------------------------------------------------------------------------------- 1 | import PricingTable from './PricingTable'; 2 | 3 | export default PricingTable; 4 | -------------------------------------------------------------------------------- /components/SubscriptionGuard/SubscriptionGuard.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /components/SubscriptionGuard/SubscriptionGuard.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { SUBSCRIPTION_PLAN } from '@lib/payments/constants'; 3 | import { useSubscription } from '@hooks/query/subscription'; 4 | import { useRouter } from 'next/router'; 5 | import { useSession } from 'next-auth/react'; 6 | import { Role } from '@prisma/client'; 7 | 8 | interface SubscriptionGuardProps { 9 | children: JSX.Element | JSX.Element[]; 10 | plans: SUBSCRIPTION_PLAN[] | undefined; 11 | } 12 | 13 | const SubscriptionGuard: React.FC = (props: SubscriptionGuardProps) => { 14 | const { children, plans } = props; 15 | const { data, status } = useSubscription(); 16 | const { data: session } = useSession(); 17 | const router = useRouter(); 18 | 19 | 20 | if (status === 'loading') { 21 | return

    Loading...

    ; 22 | } 23 | 24 | if (status === 'error') { 25 | return <>Error loading subscriptions; 26 | } 27 | 28 | if (!data) { 29 | return <>{children}; 30 | } 31 | 32 | if (!plans) return <>{children}; 33 | 34 | if (data.subscription) { 35 | const { product } = data.subscription; 36 | if (plans.some((plan) => plan?.includes(product.uniqueIdentifier))) { 37 | return <>{children}; 38 | } 39 | } 40 | 41 | if (session?.user.isOrganizationUser || plans.includes(SUBSCRIPTION_PLAN.TEAMS)) { 42 | return <>{children}; 43 | } 44 | 45 | if (session?.user.role === Role.superadmin) { 46 | return <>{children}; 47 | } 48 | router.replace("/pricing"); 49 | return <>; 50 | }; 51 | 52 | 53 | SubscriptionGuard.defaultProps = { 54 | 55 | }; 56 | 57 | SubscriptionGuard.propTypes = { 58 | 59 | }; 60 | 61 | export default SubscriptionGuard; 62 | -------------------------------------------------------------------------------- /components/SubscriptionGuard/index.ts: -------------------------------------------------------------------------------- 1 | import SubscriptionGuard from './SubscriptionGuard'; 2 | 3 | export default SubscriptionGuard; 4 | -------------------------------------------------------------------------------- /components/Table/Table.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /components/Table/Table.tsx: -------------------------------------------------------------------------------- 1 | import { useReactTable, getCoreRowModel, ColumnDef, flexRender } from '@tanstack/react-table'; 2 | import React from 'react'; 3 | 4 | export type RowData = { 5 | } & T; 6 | 7 | interface TableProps { 8 | title?: string; 9 | caption: string; 10 | data: RowData[] | undefined; 11 | columns: ColumnDef[]; 12 | } 13 | 14 | const Table = (props: TableProps) => { 15 | const { title, caption, columns, data = [] } = props; 16 | 17 | const table = useReactTable({ 18 | data, 19 | columns, 20 | getCoreRowModel: getCoreRowModel(), 21 | }) 22 | 23 | return ( 24 |
    25 | 26 | 32 | 33 | {table.getHeaderGroups().map(headerGroup => ( 34 | 35 | {headerGroup.headers.map(header => ( 36 | 44 | ))} 45 | 46 | ))} 47 | 48 | 49 | {table.getRowModel().rows.map(row => ( 50 | 51 | {row.getVisibleCells().map(cell => ( 52 | 55 | ))} 56 | 57 | ))} 58 | 59 | 60 |
    27 | {title && title} 28 |

    29 | {caption && caption} 30 |

    31 |
    37 | {header.isPlaceholder 38 | ? null 39 | : flexRender( 40 | header.column.columnDef.header, 41 | header.getContext() 42 | )} 43 |
    53 | {flexRender(cell.column.columnDef.cell, cell.getContext())} 54 |
    61 |
    62 | ); 63 | }; 64 | 65 | Table.defaultProps = { 66 | 67 | }; 68 | 69 | Table.propTypes = { 70 | 71 | }; 72 | 73 | export default Table; 74 | -------------------------------------------------------------------------------- /components/Table/index.ts: -------------------------------------------------------------------------------- 1 | import Table from './Table'; 2 | 3 | export default Table; 4 | -------------------------------------------------------------------------------- /components/Tabs/Tab.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | import { useRouter } from 'next/router'; 4 | 5 | export interface TabItem { 6 | key: string; 7 | title: JSX.Element | string, 8 | component?: JSX.Element[] | JSX.Element; 9 | route?: string; 10 | } 11 | 12 | export interface TabProps { 13 | item: TabItem; 14 | activeTab: number; 15 | currentTab: number; 16 | setActiveTab: (tabIndex: number) => void; 17 | } 18 | 19 | const Tab = (props: TabProps) => { 20 | const { item } = props; 21 | const { 22 | activeTab, 23 | currentTab, 24 | setActiveTab 25 | } = props; 26 | 27 | const router = useRouter(); 28 | 29 | const { title, route } = item; 30 | 31 | const isActive = (activeTab === currentTab || route === router.pathname); 32 | return ( 33 | <> 34 |
  • { 35 | if (route) { 36 | router.push(route); 37 | return; 38 | } 39 | setActiveTab(currentTab) 40 | }}> 41 | 53 |
  • 54 | 55 | ); 56 | }; 57 | 58 | Tab.defaultProps = { 59 | 60 | }; 61 | 62 | Tab.propTypes = { 63 | 64 | }; 65 | 66 | export default Tab; 67 | -------------------------------------------------------------------------------- /components/Tabs/Tabs.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /components/Tabs/Tabs.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import Tab, { TabItem } from './Tab'; 3 | 4 | interface TabsProps { 5 | items: TabItem[]; 6 | defaultTab?: string; 7 | } 8 | 9 | const Tabs = (props: TabsProps) => { 10 | const { items = [], defaultTab } = props; 11 | 12 | const defaultIdx = items.findIndex((tab) => tab.key === defaultTab); 13 | 14 | const [activeTab, setActiveTab] = useState(defaultIdx); 15 | return ( 16 | <> 17 |
    18 |
      19 | {items.map((tab, index) => ( 20 | 26 | ))} 27 |
    28 |
    29 | {items[activeTab]?.component} 30 | 31 | ); 32 | }; 33 | 34 | Tabs.defaultProps = { 35 | 36 | }; 37 | 38 | Tabs.propTypes = { 39 | 40 | }; 41 | 42 | export default Tabs; 43 | -------------------------------------------------------------------------------- /components/Tabs/index.ts: -------------------------------------------------------------------------------- 1 | import Tabs from './Tabs'; 2 | 3 | export default Tabs; 4 | -------------------------------------------------------------------------------- /components/UserCard/UserCard.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /components/UserCard/UserCard.tsx: -------------------------------------------------------------------------------- 1 | import { signOut, useSession } from 'next-auth/react'; 2 | import React from 'react'; 3 | import * as Popover from '@radix-ui/react-popover'; 4 | import { RxAvatar } from 'react-icons/rx'; 5 | 6 | interface UserCardProps { 7 | 8 | } 9 | 10 | const UserCard: React.FC = (props: UserCardProps) => { 11 | const { data: session } = useSession(); 12 | return ( 13 |
    14 | 15 | 16 |
    17 | 26 | {session?.user.role === 'superadmin' && ( 27 |
    28 | {session?.user.role} 29 |
    30 | )} 31 |
    32 |
    33 | 34 | 35 | 45 | 46 | 47 |
    48 | 49 |
    50 | ); 51 | }; 52 | 53 | UserCard.defaultProps = { 54 | 55 | }; 56 | 57 | UserCard.propTypes = { 58 | 59 | }; 60 | 61 | export default UserCard; 62 | -------------------------------------------------------------------------------- /components/UserCard/index.ts: -------------------------------------------------------------------------------- 1 | import UserCard from './UserCard'; 2 | 3 | export default UserCard; 4 | -------------------------------------------------------------------------------- /components/elements/Button/Button.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /components/elements/Button/Button.tsx: -------------------------------------------------------------------------------- 1 | import React, { HTMLProps } from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | interface ButtonProps extends Omit, 'size'> { 5 | variant?: 'primary' | 'secondary' | 'warning' | 'danger'; 6 | children: React.ReactNode; 7 | onClick?: () => void; 8 | size?: 'xs' | 'sm' | 'md' | 'lg'; 9 | type?: 'button' | 'submit' | 'reset'; 10 | loading?: boolean; 11 | disabled?: boolean; 12 | fullWidth?: boolean; 13 | classes?: string; 14 | }; 15 | 16 | const Button: React.FC = (props: ButtonProps) => { 17 | const { variant, onClick, children, size, type, loading,fullWidth, classes, ...restProps } = props; 18 | const getVariantClasses = () => { 19 | switch (variant) { 20 | case 'secondary': 21 | return 'bg-gray-300 hover:bg-gray-400 text-gray-800'; 22 | case 'warning': 23 | return 'bg-yellow-500 hover:bg-yellow-600 text-white'; 24 | case 'danger': 25 | return 'bg-red-500 hover:bg-red-600 text-white'; 26 | default: 27 | return 'bg-blue-500 hover:bg-blue-600 text-white'; 28 | } 29 | }; 30 | 31 | const getSizeClasses = () => { 32 | switch (size) { 33 | case 'xs': 34 | return 'px-2 py-[0.3rem] text-xs'; 35 | case 'sm': 36 | return 'px-2 py-1 text-sm'; 37 | case 'lg': 38 | return 'px-6 py-3 text-lg'; 39 | default: 40 | return 'px-4 py-2 text-base'; 41 | } 42 | }; 43 | 44 | return ( 45 | 59 | ); 60 | }; 61 | 62 | Button.defaultProps = { 63 | variant: 'primary', 64 | size: 'md' 65 | }; 66 | 67 | Button.propTypes = { 68 | 69 | }; 70 | 71 | export default Button; 72 | -------------------------------------------------------------------------------- /components/elements/Button/index.ts: -------------------------------------------------------------------------------- 1 | import Button from './Button'; 2 | 3 | export default Button; 4 | -------------------------------------------------------------------------------- /components/elements/Input/Input.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /components/elements/Input/Input.tsx: -------------------------------------------------------------------------------- 1 | import React, { HTMLProps } from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | interface InputProps extends Omit, 'size'> { 5 | variant?: 'outline' | 'filled'; 6 | size?: 'sm' | 'md' | 'lg'; 7 | name?: string; 8 | label?: string; 9 | placeholder?: string; 10 | defaultValue?: string; 11 | type?: string; 12 | error?: string; 13 | fullWidth?: boolean; 14 | value?: string | number | any; 15 | } 16 | 17 | const Input = (props: InputProps) => { 18 | const { 19 | disabled, 20 | placeholder, 21 | variant, 22 | size, 23 | label, 24 | name, 25 | type, 26 | defaultValue, 27 | error, 28 | fullWidth, 29 | ...restProps 30 | } = props; 31 | const getVariantClasses = () => { 32 | switch (variant) { 33 | case 'filled': 34 | return 'bg-gray-100 focus:bg-white focus:ring-blue-500 focus:border-blue-500'; 35 | default: 36 | return 'border border-gray-300 focus:border-blue-500 focus:ring-blue-500'; 37 | } 38 | }; 39 | 40 | const getSizeClasses = () => { 41 | switch (size) { 42 | case 'sm': 43 | return 'px-2 py-1 text-sm'; 44 | case 'lg': 45 | return 'px-6 py-3 text-lg'; 46 | default: 47 | return 'px-4 py-2 text-base'; 48 | } 49 | }; 50 | 51 | return ( 52 |
    53 | {label && } 54 | 71 | {error && {error}} 72 |
    73 | ); 74 | }; 75 | 76 | Input.defaultProps = { 77 | 78 | }; 79 | 80 | Input.propTypes = { 81 | 82 | }; 83 | 84 | export default Input; 85 | -------------------------------------------------------------------------------- /components/elements/Input/index.ts: -------------------------------------------------------------------------------- 1 | import Input from './Input'; 2 | 3 | export default Input; 4 | -------------------------------------------------------------------------------- /components/elements/index.ts: -------------------------------------------------------------------------------- 1 | /* PLOP_INJECT_IMPORT */ 2 | import Button from './Button'; 3 | import Input from './Input'; 4 | 5 | export { 6 | /* PLOP_INJECT_EXPORT */ 7 | Button, 8 | Input, 9 | } -------------------------------------------------------------------------------- /components/forms/ChangePasswordForm/ChangePasswordForm.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /components/forms/ChangePasswordForm/ChangePasswordForm.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Input } from '@components/elements'; 2 | import Flex from '@components/Flex'; 3 | import useYupValidationResolver from '@hooks/useYupValidationResolver'; 4 | import React from 'react'; 5 | import { useForm, FormProvider, Controller } from 'react-hook-form'; 6 | import * as yup from 'yup'; 7 | 8 | export type ChangePasswordFormValues = { 9 | password: string; 10 | newPassword: string; 11 | }; 12 | 13 | interface ChangePasswordFormProps { 14 | onSubmit: (updatedValues: ChangePasswordFormValues) => void; 15 | loading?: boolean; 16 | } 17 | 18 | const validationSchema: yup.ObjectSchema = yup.object({ 19 | password: yup.string().required("Required"), 20 | newPassword: yup.string().required("Required") 21 | }); 22 | 23 | const ChangePasswordForm: React.FC = (props: ChangePasswordFormProps) => { 24 | const resolver = useYupValidationResolver(validationSchema); 25 | 26 | const { onSubmit, loading } = props; 27 | 28 | const methods = useForm({ 29 | defaultValues: {}, 30 | resolver 31 | }); 32 | 33 | return ( 34 | 35 |
    36 | 37 | ( 41 | 51 | )} 52 | /> 53 | ( 57 | 67 | )} 68 | /> 69 | 70 | 71 |
    72 |
    73 | ); 74 | }; 75 | 76 | ChangePasswordForm.defaultProps = { 77 | 78 | }; 79 | 80 | ChangePasswordForm.propTypes = { 81 | 82 | }; 83 | 84 | export default ChangePasswordForm; 85 | -------------------------------------------------------------------------------- /components/forms/ChangePasswordForm/index.ts: -------------------------------------------------------------------------------- 1 | import ChangePasswordForm from './ChangePasswordForm'; 2 | 3 | export default ChangePasswordForm; 4 | -------------------------------------------------------------------------------- /components/forms/ForgotPasswordForm/ForgotPasswordForm.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /components/forms/ForgotPasswordForm/ForgotPasswordForm.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Input } from '@components/elements'; 2 | import Flex from '@components/Flex'; 3 | import useYupValidationResolver from '@hooks/useYupValidationResolver'; 4 | import React, { useEffect } from 'react'; 5 | import { useForm, FormProvider, Controller } from 'react-hook-form'; 6 | import * as yup from 'yup'; 7 | import { LiteralUnion, ClientSafeProvider } from 'next-auth/react'; 8 | import { BuiltInProviderType } from 'next-auth/providers'; 9 | import Link from 'next/link'; 10 | 11 | export type ForgotPasswordFormValues = { 12 | email: string; 13 | }; 14 | 15 | interface ForgotPasswordProps { 16 | loading?: boolean; 17 | onSubmit: (updatedValues: ForgotPasswordFormValues) => void; 18 | } 19 | 20 | const validationSchema: yup.ObjectSchema = yup.object({ 21 | email: yup.string().required("Required"), 22 | }); 23 | 24 | 25 | const ForgotPassword: React.FC = (props: ForgotPasswordProps) => { 26 | 27 | const resolver = useYupValidationResolver(validationSchema); 28 | const { loading, onSubmit } = props; 29 | 30 | const methods = useForm({ 31 | defaultValues: { 32 | }, 33 | resolver 34 | }); 35 | 36 | return ( 37 | 38 |
    39 |
    40 |

    41 | Forgot password ? 42 |

    43 |
    44 | ( 48 | 58 | )} 59 | /> 60 |
    61 | 62 |

    63 | Don’t have an account yet? 64 |

    65 |

    66 | Have an account? 67 |

    68 |
    69 |
    70 |
    71 | ); 72 | }; 73 | 74 | ForgotPassword.defaultProps = { 75 | 76 | }; 77 | 78 | ForgotPassword.propTypes = { 79 | 80 | }; 81 | 82 | export default ForgotPassword; 83 | -------------------------------------------------------------------------------- /components/forms/ForgotPasswordForm/index.ts: -------------------------------------------------------------------------------- 1 | import ForgotPasswordForm from './ForgotPasswordForm'; 2 | 3 | export default ForgotPasswordForm; 4 | -------------------------------------------------------------------------------- /components/forms/LoginForm/LoginForm.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /components/forms/LoginForm/LoginForm.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Input } from '@components/elements'; 2 | import Flex from '@components/Flex'; 3 | import useYupValidationResolver from '@hooks/useYupValidationResolver'; 4 | import React, { useEffect } from 'react'; 5 | import { useForm, FormProvider, Controller } from 'react-hook-form'; 6 | import * as yup from 'yup'; 7 | import { LiteralUnion, ClientSafeProvider, signIn } from 'next-auth/react'; 8 | import { BuiltInProviderType } from 'next-auth/providers'; 9 | import Link from 'next/link'; 10 | 11 | import { FcGoogle } from 'react-icons/fc'; 12 | import { VscGithub } from 'react-icons/vsc'; 13 | import { BsFacebook, BsTwitter } from 'react-icons/bs'; 14 | 15 | import classNames from 'classnames'; 16 | import { useRouter } from 'next/router'; 17 | 18 | export const AuthDivider = () => { 19 | return ( 20 |
    21 |
    22 | or 23 |
    24 | ) 25 | } 26 | interface SocialAuthProps { 27 | providers: Record, ClientSafeProvider> | null, 28 | loading: boolean | undefined; 29 | redirect?: string; 30 | } 31 | export const SocialAuth = ({ providers, loading, redirect }: SocialAuthProps) => { 32 | 33 | const signInHandler = (id: LiteralUnion) => { 34 | signIn(id, { redirect: true, callbackUrl: redirect || '/dashboard' }) 35 | } 36 | 37 | const buttonCommonClasses = "w-full text-sm rounded-md justify-center font-medium py-2 border px-4 flex items-center gap-2 text-secondary-600 hover:underline dark:text-secondary-500"; 38 | 39 | const commonString = 'Login with '; 40 | return ( 41 |
    42 | {providers && Object.keys(providers).map((providerKey) => { 43 | if (providers[providerKey].type === 'oauth') { 44 | switch(providers[providerKey].id) { 45 | case 'google': 46 | return ( 47 | 57 | ); 58 | case 'github': 59 | return ( 60 | 70 | ); 71 | case 'twitter': 72 | return ( 73 | 83 | ); 84 | case 'facebook': 85 | return ( 86 | 96 | ); 97 | } 98 | } 99 | return null; 100 | })} 101 |
    102 | ); 103 | } 104 | 105 | export type LoginFormValues = { 106 | username: string; 107 | password: string; 108 | }; 109 | 110 | interface LoginFormProps { 111 | loading?: boolean; 112 | providers: Record, ClientSafeProvider> | null, 113 | onSubmit: (updatedValues: LoginFormValues, redirect?: string) => void; 114 | } 115 | 116 | const validationSchema: yup.ObjectSchema = yup.object({ 117 | username: yup.string().required("Required"), 118 | password: yup.string().required("Required"), 119 | }); 120 | 121 | 122 | const LoginForm: React.FC = (props: LoginFormProps) => { 123 | const router = useRouter(); 124 | const { redirect } = router.query; 125 | const defaultRedirect = redirect as string || '/dashboard'; 126 | 127 | const resolver = useYupValidationResolver(validationSchema); 128 | const { loading, onSubmit, providers } = props; 129 | 130 | const methods = useForm({ 131 | defaultValues: { 132 | }, 133 | resolver 134 | }); 135 | 136 | const onSubmitHandler = (values: LoginFormValues) => { 137 | onSubmit(values, defaultRedirect); 138 | } 139 | 140 | return ( 141 | <> 142 | 143 | 144 | {providers && providers?.credentials && ( 145 | 146 |
    147 |
    148 |

    149 | Sign in to your account 150 |

    151 |
    152 | ( 156 | 166 | )} 167 | /> 168 |
    169 |
    170 | ( 174 | 184 | )} 185 | /> 186 |
    187 |
    188 | 189 | 190 | 191 |
    192 | 193 |

    194 | Don’t have an account yet? 195 | 198 | 199 | 200 |

    201 |
    202 |
    203 |
    204 | )} 205 | 206 | ); 207 | }; 208 | 209 | LoginForm.defaultProps = { 210 | 211 | }; 212 | 213 | LoginForm.propTypes = { 214 | 215 | }; 216 | 217 | export default LoginForm; 218 | -------------------------------------------------------------------------------- /components/forms/LoginForm/index.ts: -------------------------------------------------------------------------------- 1 | import LoginForm from './LoginForm'; 2 | 3 | export default LoginForm; 4 | -------------------------------------------------------------------------------- /components/forms/OrganizationForm/OrganizationForm.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /components/forms/OrganizationForm/OrganizationForm.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Input } from '@components/elements'; 2 | import useYupValidationResolver from '@hooks/useYupValidationResolver'; 3 | import React from 'react'; 4 | import { useForm, FormProvider, Controller } from 'react-hook-form'; 5 | import * as yup from 'yup'; 6 | 7 | export type OrganizationFormValues = { 8 | name: string; 9 | }; 10 | 11 | interface OrganizationFormProps { 12 | onSubmit: (updatedValues: OrganizationFormValues) => void; 13 | loading?: boolean; 14 | submitLabel?: string; 15 | } 16 | 17 | const validationSchema: yup.ObjectSchema = yup.object({ 18 | name: yup.string().required("Required"), 19 | }); 20 | 21 | const OrganizationForm: React.FC = (props: OrganizationFormProps) => { 22 | const { onSubmit, loading, submitLabel } = props; 23 | const resolver = useYupValidationResolver(validationSchema); 24 | 25 | const methods = useForm({ 26 | defaultValues: {}, 27 | resolver, 28 | }); 29 | 30 | return ( 31 | 32 |
    33 |
    34 |
    35 | ( 39 | 49 | )} 50 | /> 51 |
    52 | 53 |
    54 |
    55 |
    56 | ); 57 | }; 58 | 59 | OrganizationForm.defaultProps = { 60 | submitLabel: 'Save', 61 | }; 62 | 63 | OrganizationForm.propTypes = { 64 | 65 | }; 66 | 67 | export default OrganizationForm; 68 | -------------------------------------------------------------------------------- /components/forms/OrganizationForm/index.ts: -------------------------------------------------------------------------------- 1 | import OrganizationForm from './OrganizationForm'; 2 | 3 | export default OrganizationForm; 4 | -------------------------------------------------------------------------------- /components/forms/OrganizationMemberInviteForm/OrganizationMemberInviteForm.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /components/forms/OrganizationMemberInviteForm/OrganizationMemberInviteForm.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useForm, FormProvider, Controller, UseFormReturn } from 'react-hook-form'; 3 | import useYupValidationResolver from '@hooks/useYupValidationResolver'; 4 | import * as yup from 'yup'; 5 | import { Button, Input } from '@components/elements'; 6 | 7 | export type OrganizationMemberInviteFormValues = { 8 | email: string; 9 | }; 10 | 11 | interface OrganizationMemberInviteFormProps { 12 | onSubmit: (updatedValues: OrganizationMemberInviteFormValues, formInstance: UseFormReturn) => void; 13 | loading?: boolean; 14 | } 15 | 16 | const validationSchema: yup.ObjectSchema = yup.object({ 17 | email: yup.string().required(), 18 | }); 19 | 20 | const OrganizationMemberInviteForm: React.FC = (props: OrganizationMemberInviteFormProps) => { 21 | const { onSubmit, loading } = props; 22 | const resolver = useYupValidationResolver(validationSchema); 23 | 24 | const methods = useForm({ 25 | defaultValues: {}, 26 | resolver 27 | }); 28 | 29 | const handleSubmit = (values: OrganizationMemberInviteFormValues) => { 30 | onSubmit(values, methods); 31 | } 32 | 33 | return ( 34 | 35 |
    36 |
    37 |
    38 | ( 42 | 52 | )} 53 | /> 54 |
    55 | 56 |
    57 |
    58 |
    59 | ); 60 | }; 61 | 62 | OrganizationMemberInviteForm.defaultProps = { 63 | 64 | }; 65 | 66 | OrganizationMemberInviteForm.propTypes = { 67 | 68 | }; 69 | 70 | export default OrganizationMemberInviteForm; 71 | -------------------------------------------------------------------------------- /components/forms/OrganizationMemberInviteForm/index.ts: -------------------------------------------------------------------------------- 1 | import OrganizationMemberInviteForm from './OrganizationMemberInviteForm'; 2 | 3 | export default OrganizationMemberInviteForm; 4 | -------------------------------------------------------------------------------- /components/forms/ProfileForm/ProfileForm.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /components/forms/ProfileForm/ProfileForm.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Input from '@components/elements/Input'; 3 | import Button from '@components/elements/Button'; 4 | import { useForm, FormProvider, Controller } from 'react-hook-form'; 5 | import Flex from '@components/Flex'; 6 | import { UserData } from '@pages/api/users/currentUser'; 7 | 8 | export type ProfileFormValues = { 9 | name: string | null | undefined; 10 | }; 11 | 12 | interface ProfileFormProps { 13 | data: UserData | undefined; 14 | onSubmit: (updatedValues: ProfileFormValues) => void; 15 | loading?: boolean; 16 | } 17 | 18 | const ProfileForm = (props: ProfileFormProps) => { 19 | const { data, onSubmit, loading } = props; 20 | 21 | const methods = useForm({ 22 | defaultValues: { 23 | ...data as ProfileFormValues, 24 | }, 25 | }); 26 | 27 | return ( 28 | 29 |
    30 | 31 | ( 35 | 45 | )} 46 | /> 47 | 48 | 49 |
    50 |
    51 | ); 52 | }; 53 | 54 | ProfileForm.defaultProps = { 55 | 56 | }; 57 | 58 | ProfileForm.propTypes = { 59 | 60 | }; 61 | 62 | export default ProfileForm; 63 | -------------------------------------------------------------------------------- /components/forms/ProfileForm/index.ts: -------------------------------------------------------------------------------- 1 | import ProfileForm from './ProfileForm'; 2 | 3 | export default ProfileForm; 4 | -------------------------------------------------------------------------------- /components/forms/RegisterForm/RegisterForm.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /components/forms/RegisterForm/RegisterForm.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Input } from '@components/elements'; 2 | import Flex from '@components/Flex'; 3 | import useYupValidationResolver from '@hooks/useYupValidationResolver'; 4 | import React, { useEffect } from 'react'; 5 | import { useForm, FormProvider, Controller, UseFormReturn } from 'react-hook-form'; 6 | import * as yup from 'yup'; 7 | import { LiteralUnion, ClientSafeProvider } from 'next-auth/react'; 8 | import { BuiltInProviderType } from 'next-auth/providers'; 9 | import Link from 'next/link'; 10 | import { AuthDivider, SocialAuth } from '../LoginForm/LoginForm'; 11 | import { useRouter } from 'next/router'; 12 | 13 | export type RegisterFormValues = { 14 | name: string; 15 | email: string; 16 | password: string; 17 | }; 18 | 19 | interface RegisterFormProps { 20 | loading?: boolean; 21 | providers: Record, ClientSafeProvider> | null, 22 | onSubmit: (updatedValues: RegisterFormValues, formInstance: UseFormReturn, redirect?: string) => void; 23 | } 24 | 25 | const validationSchema: yup.ObjectSchema = yup.object({ 26 | name: yup.string().required("Required"), 27 | email: yup.string().required("Required"), 28 | password: yup.string().required("Required"), 29 | }); 30 | 31 | 32 | const RegisterForm: React.FC = (props: RegisterFormProps) => { 33 | const router = useRouter(); 34 | const { redirect } = router.query; 35 | const defaultRedirect = redirect ? redirect as string : '/login'; 36 | 37 | const resolver = useYupValidationResolver(validationSchema); 38 | const { loading, onSubmit, providers } = props; 39 | 40 | const methods = useForm({ 41 | defaultValues: { 42 | }, 43 | resolver 44 | }); 45 | 46 | const handleSubmit = (values: RegisterFormValues) => { 47 | onSubmit(values, methods, defaultRedirect); 48 | } 49 | 50 | return ( 51 | <> 52 | 53 | 54 | 55 |
    56 |
    57 |

    58 | Create your account 59 |

    60 |
    61 | ( 65 | 75 | )} 76 | /> 77 |
    78 |
    79 | ( 83 | 93 | )} 94 | /> 95 |
    96 |
    97 | ( 101 | 111 | )} 112 | /> 113 |
    114 |
    115 | 116 | 117 | 118 |
    119 | 120 |

    121 | Have an account? 122 |

    123 |
    124 |
    125 |
    126 | 127 | ); 128 | }; 129 | 130 | RegisterForm.defaultProps = { 131 | 132 | }; 133 | 134 | RegisterForm.propTypes = { 135 | 136 | }; 137 | 138 | export default RegisterForm; 139 | -------------------------------------------------------------------------------- /components/forms/RegisterForm/index.ts: -------------------------------------------------------------------------------- 1 | import RegisterForm from './RegisterForm'; 2 | 3 | export default RegisterForm; 4 | -------------------------------------------------------------------------------- /components/forms/ResetPasswordForm/ResetPasswordForm.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /components/forms/ResetPasswordForm/ResetPasswordForm.tsx: -------------------------------------------------------------------------------- 1 | import { Button, Input } from '@components/elements'; 2 | import Flex from '@components/Flex'; 3 | import useYupValidationResolver from '@hooks/useYupValidationResolver'; 4 | import React from 'react'; 5 | import { useForm, FormProvider, Controller } from 'react-hook-form'; 6 | import * as yup from 'yup'; 7 | 8 | export type ResetPasswordFormValues = { 9 | password: string; 10 | reEnterpassword: string; 11 | }; 12 | 13 | interface ResetPasswordFormProps { 14 | onSubmit: (values: ResetPasswordFormValues) => void; 15 | loading?: boolean; 16 | } 17 | 18 | const validationSchema: yup.ObjectSchema = yup.object({ 19 | password: yup.string().required("Required").min(8), 20 | reEnterpassword: yup.string().required("Required").min(8), 21 | }); 22 | 23 | const ResetPasswordForm: React.FC = (props: ResetPasswordFormProps) => { 24 | const resolver = useYupValidationResolver(validationSchema); 25 | 26 | const { onSubmit, loading } = props; 27 | 28 | const methods = useForm({ 29 | defaultValues: {}, 30 | resolver 31 | }); 32 | 33 | return ( 34 | 35 |
    36 |
    37 |

    38 | Reset your password 39 |

    40 |
    41 | ( 45 | 55 | )} 56 | /> 57 |
    58 |
    59 | ( 63 | 73 | )} 74 | /> 75 |
    76 |
    77 | 78 |
    79 |
    80 |
    81 |
    82 | ); 83 | }; 84 | 85 | ResetPasswordForm.defaultProps = { 86 | 87 | }; 88 | 89 | ResetPasswordForm.propTypes = { 90 | 91 | }; 92 | 93 | export default ResetPasswordForm; 94 | -------------------------------------------------------------------------------- /components/forms/ResetPasswordForm/index.ts: -------------------------------------------------------------------------------- 1 | import ResetPasswordForm from './ResetPasswordForm'; 2 | 3 | export default ResetPasswordForm; 4 | -------------------------------------------------------------------------------- /components/forms/index.ts: -------------------------------------------------------------------------------- 1 | /* PLOP_INJECT_IMPORT */ 2 | import OrganizationMemberInviteForm from './OrganizationMemberInviteForm'; 3 | import OrganizationForm from './OrganizationForm'; 4 | import ResetPasswordForm from './ResetPasswordForm'; 5 | import ForgotPasswordForm from './ForgotPasswordForm'; 6 | import RegisterForm from './RegisterForm'; 7 | import LoginForm from './LoginForm'; 8 | import ChangePasswordForm from './ChangePasswordForm'; 9 | import ProfileForm from './ProfileForm'; 10 | 11 | export { 12 | /* PLOP_INJECT_EXPORT */ 13 | OrganizationMemberInviteForm, 14 | OrganizationForm, 15 | ResetPasswordForm, 16 | ForgotPasswordForm, 17 | RegisterForm, 18 | LoginForm, 19 | ChangePasswordForm, 20 | ProfileForm, 21 | } -------------------------------------------------------------------------------- /components/index.ts: -------------------------------------------------------------------------------- 1 | /* PLOP_INJECT_IMPORT */ 2 | import Logo from './Logo'; 3 | import UserCard from './UserCard'; 4 | import DialogComponent from './DialogComponent'; 5 | import CurrentPlanCard from './CurrentPlanCard'; 6 | import PricingComponent from './PricingComponent'; 7 | import SubscriptionGuard from './SubscriptionGuard'; 8 | import { Dialog } from './Dialog'; 9 | import AuthGuard from './AuthGuard'; 10 | import Flex from './Flex'; 11 | import Tabs from './Tabs'; 12 | import PricingTable from './PricingTable'; 13 | import Table from './Table'; 14 | import Menu from './Menu'; 15 | import MainLayout from './layouts/MainLayout'; 16 | import AuthLayout from './layouts/AuthLayout'; 17 | 18 | export { 19 | /* PLOP_INJECT_EXPORT */ 20 | Logo, 21 | UserCard, 22 | DialogComponent, 23 | CurrentPlanCard, 24 | PricingComponent, 25 | SubscriptionGuard, 26 | Dialog, 27 | AuthGuard, 28 | Flex, 29 | Tabs, 30 | PricingTable, 31 | Table, 32 | Menu, 33 | MainLayout, 34 | AuthLayout, 35 | } -------------------------------------------------------------------------------- /components/layouts/AccountLayout/AccountLayout.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /components/layouts/AccountLayout/AccountLayout.tsx: -------------------------------------------------------------------------------- 1 | import Menu from '@components/Menu'; 2 | import React from 'react'; 3 | 4 | import { TabItem } from '@components/Tabs/Tab'; 5 | import Tabs from '@components/Tabs'; 6 | 7 | export const accountTabs: TabItem[] = [ 8 | { 9 | key: 'profile', 10 | title: 'Profile', 11 | route: '/account/profile' 12 | }, 13 | { 14 | key: 'password', 15 | title: 'Change Password', 16 | route: '/account/password' 17 | }, 18 | { 19 | key: 'billing', 20 | title: 'Billing', 21 | route: '/account/billing' 22 | } 23 | ] 24 | 25 | interface MainLayoutProps { 26 | children: JSX.Element[] | JSX.Element; 27 | tab: string; 28 | } 29 | 30 | const MainLayout = (props: MainLayoutProps) => { 31 | const { children, tab } = props; 32 | return ( 33 |
    34 | 35 | {children} 36 |
    37 | ); 38 | }; 39 | 40 | MainLayout.defaultProps = { 41 | 42 | }; 43 | 44 | MainLayout.propTypes = { 45 | 46 | }; 47 | 48 | export default MainLayout; 49 | -------------------------------------------------------------------------------- /components/layouts/AccountLayout/index.ts: -------------------------------------------------------------------------------- 1 | import AccountLayout from './AccountLayout'; 2 | 3 | export default AccountLayout; 4 | -------------------------------------------------------------------------------- /components/layouts/AuthLayout/AuthLayout.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /components/layouts/AuthLayout/AuthLayout.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface AuthLayoutProps { 4 | children: JSX.Element[] | JSX.Element; 5 | } 6 | 7 | const AuthLayout = (props: AuthLayoutProps) => { 8 | const { children } = props; 9 | return ( 10 |
    11 |
    12 |
    13 | 14 | 15 |
    16 | {children} 17 |
    18 |
    19 |
    20 |
    21 | ); 22 | }; 23 | 24 | AuthLayout.defaultProps = { 25 | 26 | }; 27 | 28 | AuthLayout.propTypes = { 29 | 30 | }; 31 | 32 | export default AuthLayout; 33 | -------------------------------------------------------------------------------- /components/layouts/AuthLayout/index.ts: -------------------------------------------------------------------------------- 1 | import AccountLayout from './AuthLayout'; 2 | 3 | export default AccountLayout; 4 | -------------------------------------------------------------------------------- /components/layouts/MainLayout/MainLayout.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /components/layouts/MainLayout/MainLayout.tsx: -------------------------------------------------------------------------------- 1 | import Menu from '@components/Menu'; 2 | import { IMenuItem } from '@components/Menu/MenuItem'; 3 | import React, { useRef, useState } from 'react'; 4 | 5 | import { RxDashboard } from 'react-icons/rx'; 6 | import { VscAccount } from 'react-icons/vsc'; 7 | 8 | import { HiOutlineUsers } from 'react-icons/hi'; 9 | import { SlOrganization } from 'react-icons/sl'; 10 | 11 | import { TfiLayoutListThumb } from 'react-icons/tfi'; 12 | 13 | import { FaRegListAlt } from 'react-icons/fa'; 14 | 15 | import { useSession } from 'next-auth/react'; 16 | import { Role } from '@prisma/client'; 17 | import UserCard from '@components/UserCard'; 18 | 19 | import { CgMenuLeftAlt } from 'react-icons/cg'; 20 | import useOnClickOutside from '@hooks/useOnClickOutside'; 21 | import classNames from 'classnames'; 22 | import Logo from '@components/Logo'; 23 | 24 | type MenuWithRole = IMenuItem & { 25 | roles?: Role[] 26 | } 27 | 28 | const menu: MenuWithRole[] = [ 29 | { 30 | icon: , 31 | title: 'Dashboard', 32 | key: 'dashboard', 33 | url: '/dashboard', 34 | roles: [Role.superadmin, Role.customer] 35 | }, 36 | { 37 | icon: , 38 | title: 'Pricing', 39 | key: 'pricing', 40 | url: '/pricing', 41 | roles: [Role.customer] 42 | }, 43 | { 44 | icon: , 45 | title: 'Products', 46 | key: 'products', 47 | url: '/products', 48 | roles: [Role.superadmin] 49 | }, 50 | { 51 | icon: , 52 | title: 'Organizations', 53 | key: 'organizations', 54 | url: '/organizations', 55 | roles: [Role.customer] 56 | }, 57 | { 58 | icon: , 59 | title: 'Users', 60 | key: 'users', 61 | url: '/users', 62 | roles: [Role.superadmin] 63 | }, 64 | { 65 | icon: , 66 | title: 'Account', 67 | key: 'account', 68 | url: '/account', 69 | } 70 | ] 71 | interface MainLayoutProps { 72 | children: JSX.Element[] | JSX.Element; 73 | } 74 | 75 | const MainLayout = (props: MainLayoutProps) => { 76 | const { data: session } = useSession(); 77 | 78 | const preparedMenu = menu.filter((item) => { 79 | if (item.key === 'organizations') { 80 | if (!session?.user.isOrganizationUser) { 81 | return false; 82 | } 83 | } 84 | return item.roles ? item.roles.includes(session?.user?.role as Role): true; 85 | }); 86 | 87 | const ref = useRef(); 88 | const [menuOpen, setMenuOpen] = useState(false); 89 | useOnClickOutside(ref, () => setMenuOpen(false)); 90 | 91 | const { children } = props; 92 | return ( 93 |
    94 |
    95 |
    96 |
    97 | 98 |
    99 | 100 |
    101 |
    102 | 103 |
    104 |
    105 |
    110 |
    111 | 112 |
    113 | 114 |
    115 | 116 |
    117 |
    118 |
    119 |
    124 | setMenuOpen(true)}/> 125 |
    126 |
    127 | {children} 128 |
    129 |
    130 |
    131 | ); 132 | }; 133 | 134 | MainLayout.defaultProps = { 135 | 136 | }; 137 | 138 | MainLayout.propTypes = { 139 | 140 | }; 141 | 142 | export default MainLayout; 143 | -------------------------------------------------------------------------------- /components/layouts/MainLayout/index.ts: -------------------------------------------------------------------------------- 1 | import MainLayout from './MainLayout'; 2 | 3 | export default MainLayout; 4 | -------------------------------------------------------------------------------- /components/layouts/OrganizationLayout/OrganizationLayout.module.css: -------------------------------------------------------------------------------- 1 | .root { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /components/layouts/OrganizationLayout/OrganizationLayout.tsx: -------------------------------------------------------------------------------- 1 | import Menu from '@components/Menu'; 2 | import React from 'react'; 3 | 4 | import { TabItem } from '@components/Tabs/Tab'; 5 | import Tabs from '@components/Tabs'; 6 | import Link from 'next/link'; 7 | import { IoReturnUpBackOutline } from 'react-icons/io5'; 8 | import { useRouter } from 'next/router'; 9 | import Flex from '@components/Flex'; 10 | import { OrganizationRole } from '@prisma/client'; 11 | import { useOrganizationMember } from '@hooks/query/organizations'; 12 | 13 | const getOrganizationTabs = (id: string) => { 14 | 15 | const organizationTabs: TabItemWithOrganizationRole[] = [ 16 | { 17 | key: 'dashboard', 18 | title: 'Dashboard', 19 | route: `/organizations/${id}`, 20 | roles: [OrganizationRole.org_admin, OrganizationRole.org_user] 21 | }, 22 | { 23 | key: 'members', 24 | title: 'Members', 25 | route: `/organizations/${id}/members`, 26 | roles: [OrganizationRole.org_admin] 27 | }, 28 | { 29 | key: 'billing', 30 | title: 'Billing', 31 | route: `/organizations/${id}/billing`, 32 | roles: [OrganizationRole.org_admin] 33 | } 34 | ] 35 | 36 | return organizationTabs; 37 | } 38 | 39 | 40 | interface OrganizationLayoutProps { 41 | children: JSX.Element[] | JSX.Element; 42 | tab: string; 43 | } 44 | 45 | type TabItemWithOrganizationRole = TabItem & { 46 | roles?: OrganizationRole[] 47 | } 48 | 49 | const OrganizationLayout = (props: OrganizationLayoutProps) => { 50 | const { children, tab } = props; 51 | const router = useRouter(); 52 | const { organizationId } = router.query; 53 | 54 | const { isLoading, data } = useOrganizationMember({ organizationId: organizationId as string }); 55 | 56 | 57 | if (isLoading) { 58 | return <>Loading....; 59 | } 60 | 61 | const preparedTabs = getOrganizationTabs(organizationId as string).filter((item) => { 62 | return item.roles ? item.roles.includes(data?.role as OrganizationRole): true; 63 | }); 64 | 65 | return ( 66 | 67 | 68 | 69 | 70 |
    71 | 72 | {children} 73 |
    74 |
    75 | ); 76 | }; 77 | 78 | OrganizationLayout.defaultProps = { 79 | 80 | }; 81 | 82 | OrganizationLayout.propTypes = { 83 | 84 | }; 85 | 86 | export default OrganizationLayout; 87 | -------------------------------------------------------------------------------- /components/layouts/OrganizationLayout/index.ts: -------------------------------------------------------------------------------- 1 | import OrganizationLayout from './OrganizationLayout'; 2 | 3 | export default OrganizationLayout; 4 | -------------------------------------------------------------------------------- /errors/index.ts: -------------------------------------------------------------------------------- 1 | 2 | export class InvalidRequestError extends Error { 3 | constructor(message = '') { 4 | super(message) 5 | this.name = 'InvalidRequestError' 6 | if (message) this.message = message 7 | else this.message = 'Invalid request' 8 | } 9 | } 10 | 11 | export class NotFoundError extends Error { 12 | constructor(message = 'Response not found') { 13 | super(message) 14 | this.name = 'NotFoundError' 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /hooks/query/currentUser.tsx: -------------------------------------------------------------------------------- 1 | import AxoisClient from "@lib/axios"; 2 | import { useMutation, useQuery } from "@tanstack/react-query" 3 | import { AxiosResponse } from "axios"; 4 | import toast from 'react-hot-toast'; 5 | import { useRouter } from "next/router"; 6 | import { queryClient } from "@pages/_app"; 7 | import { signIn } from "next-auth/react"; 8 | import { UseFormReturn } from "react-hook-form"; 9 | 10 | import { UserData } from "@pages/api/users/currentUser"; 11 | import { ProfileFormValues } from "@components/forms/ProfileForm/ProfileForm"; 12 | import { ChangePasswordFormValues } from "@components/forms/ChangePasswordForm/ChangePasswordForm"; 13 | import { LoginFormValues } from "@components/forms/LoginForm/LoginForm"; 14 | import { RegisterFormValues } from "@components/forms/RegisterForm/RegisterForm"; 15 | import { ForgotPasswordFormValues } from "@components/forms/ForgotPasswordForm/ForgotPasswordForm"; 16 | import { ResetPasswordFormValues } from "@components/forms/ResetPasswordForm/ResetPasswordForm"; 17 | 18 | 19 | const useUser = () => { 20 | const { data, isLoading, error } = useQuery( 21 | ["user"], 22 | async () => { 23 | const { data }: AxiosResponse = await AxoisClient.getInstance().get('api/users/currentUser'); 24 | return data; 25 | }, 26 | { 27 | refetchOnWindowFocus: false 28 | } 29 | ); 30 | 31 | const { mutateAsync, isLoading: isUpdateLoading } = useMutation( 32 | (updatedData: ProfileFormValues) => { 33 | return AxoisClient.getInstance().patch('api/users/currentUser', { ...updatedData }); 34 | }, 35 | { 36 | onSuccess: async () => { 37 | await queryClient.invalidateQueries(["user"]); 38 | } 39 | } 40 | ); 41 | 42 | const updateUser = (user: ProfileFormValues) => { 43 | const promise = mutateAsync(user); 44 | toast.promise(promise, { 45 | loading: 'Updating profile...', 46 | success: 'Profile updated successfully!', 47 | error: 'Failed to update profile.', 48 | }); 49 | }; 50 | 51 | return { 52 | data, 53 | isUpdateLoading, 54 | isLoading, 55 | error, 56 | updateUser, 57 | }; 58 | }; 59 | 60 | const useUpdatePassword = () => { 61 | const { mutateAsync, isLoading: isUpdateLoading, error } = useMutation( 62 | (updatedData: ChangePasswordFormValues) => { 63 | return AxoisClient.getInstance().patch('api/auth/change-password', { ...updatedData }); 64 | } 65 | ); 66 | 67 | const updatePassword = async (updatedData: ChangePasswordFormValues) => { 68 | const promise = mutateAsync(updatedData); 69 | toast.promise(promise, { 70 | loading: 'Updating password...', 71 | success: 'Password updated successfully!', 72 | error: (err) => `${err.response.data.message || 'something went wrong!'}`, 73 | }); 74 | }; 75 | 76 | return { 77 | updatePassword, 78 | isUpdateLoading, 79 | error, 80 | }; 81 | } 82 | 83 | const useResetPassword = () => { 84 | const { mutateAsync, isLoading: isUpdateLoading, error } = useMutation( 85 | (updatedData: ResetPasswordFormValues & { token: string }) => { 86 | return AxoisClient.getInstance().post('api/auth/reset', { ...updatedData }); 87 | } 88 | ); 89 | 90 | const updatePassword = async (updatedData: ResetPasswordFormValues, token: string) => { 91 | const promise = mutateAsync({token, ...updatedData}); 92 | toast.promise(promise, { 93 | loading: 'Updating password...', 94 | success: 'Password reset successfully!', 95 | error: (err) => `${err.response.data.message || 'something went wrong!'}`, 96 | }); 97 | }; 98 | 99 | return { 100 | updatePassword, 101 | isUpdateLoading, 102 | error, 103 | }; 104 | } 105 | 106 | 107 | const useSignIn = () => { 108 | const router = useRouter(); 109 | const { mutateAsync, isLoading, error } = useMutation( 110 | async (data: LoginFormValues) => { 111 | const response = await signIn("credentials", { 112 | redirect: false, 113 | username: data.username, 114 | password: data.password 115 | }); 116 | 117 | if (response?.error) { 118 | throw new Error(response.error) 119 | } 120 | } 121 | ); 122 | 123 | const login = async (data: LoginFormValues, redirect?: string | undefined) => { 124 | const promise = mutateAsync(data); 125 | toast.promise(promise, { 126 | loading: 'Please wait...', 127 | success: 'Logged In...', 128 | error: (err) => `${err?.response?.data?.message || err || 'something went wrong!'}`, 129 | }).then(() => { 130 | if (redirect) { 131 | router.replace(redirect as string); 132 | } else { 133 | router.replace('/dashboard'); 134 | } 135 | }).catch((e) => { 136 | 137 | }); 138 | }; 139 | 140 | return { 141 | login, 142 | isLoading, 143 | error, 144 | }; 145 | } 146 | 147 | const useSignup = () => { 148 | const router = useRouter(); 149 | const { mutateAsync, isLoading, error, status } = useMutation( 150 | async (data: RegisterFormValues) => { 151 | return AxoisClient.getInstance().post('api/auth/register', { ...data }); 152 | } 153 | ); 154 | 155 | const signup = async (data: RegisterFormValues, formInstance: UseFormReturn, redirect?: string) => { 156 | const promise = mutateAsync(data); 157 | toast.promise(promise, { 158 | loading: 'Please wait...', 159 | success: 'Registered Successfully', 160 | error: (err) => `${err?.response?.data?.message || err || 'something went wrong!'}`, 161 | }).then((value) => { 162 | if (value.status === 200) { 163 | formInstance.reset(); 164 | router.replace(`/auth/login${redirect ? `?redirect=${redirect}`: ''}`); 165 | } 166 | }).catch(() => { 167 | formInstance.setError('email', { message: 'Email already used' }); 168 | }); 169 | }; 170 | 171 | return { 172 | signup, 173 | isLoading, 174 | error, 175 | status, 176 | }; 177 | } 178 | 179 | const useForgotPassword = () => { 180 | const { mutateAsync, isLoading, error, status, data } = useMutation( 181 | (data: ForgotPasswordFormValues) => { 182 | return AxoisClient.getInstance().post('api/auth/forgot', { ...data }); 183 | } 184 | ); 185 | 186 | const forgot = async (data: ForgotPasswordFormValues) => { 187 | const promise = mutateAsync(data); 188 | toast.promise(promise, { 189 | loading: 'Please wait...', 190 | success: 'Password reset link sent to your mail.', 191 | error: (err) => `${err?.response?.data?.message || err || 'something went wrong!'}`, 192 | }); 193 | }; 194 | 195 | return { 196 | forgot, 197 | isLoading, 198 | error, 199 | status, 200 | data, 201 | }; 202 | } 203 | 204 | export { 205 | useUser, 206 | useUpdatePassword, 207 | useSignIn, 208 | useSignup, 209 | useForgotPassword, 210 | useResetPassword 211 | }; 212 | -------------------------------------------------------------------------------- /hooks/query/payments.tsx: -------------------------------------------------------------------------------- 1 | export {} -------------------------------------------------------------------------------- /hooks/query/plans.tsx: -------------------------------------------------------------------------------- 1 | import AxoisClient from "@lib/axios"; 2 | import { useQuery } from "@tanstack/react-query" 3 | import { PlansData } from "@pages/api/plans"; 4 | import { AxiosResponse } from "axios"; 5 | 6 | import { ProductsData } from '@pages/api/products'; 7 | 8 | export interface UsePlansProps { 9 | isOrganization?: boolean; 10 | } 11 | const usePlans = ({ isOrganization }: UsePlansProps) => { 12 | const { data, isLoading, error } = useQuery( 13 | ["plans"], 14 | async () => { 15 | const { data }: AxiosResponse = await AxoisClient.getInstance().get(`api/plans${isOrganization ? '?isOrganization=true': ''}`); 16 | return data.data; 17 | }, 18 | { 19 | refetchOnWindowFocus: false 20 | } 21 | ); 22 | 23 | return { 24 | data, 25 | isLoading, 26 | error, 27 | }; 28 | }; 29 | 30 | const useProducts = () => { 31 | const { data, isLoading, error } = useQuery( 32 | ["products"], 33 | async () => { 34 | const { data }: AxiosResponse = await AxoisClient.getInstance().get('api/products'); 35 | return data; 36 | }, 37 | { 38 | refetchOnWindowFocus: false 39 | } 40 | ); 41 | 42 | return { 43 | data, 44 | isLoading, 45 | error, 46 | }; 47 | }; 48 | export { 49 | usePlans, 50 | useProducts 51 | }; -------------------------------------------------------------------------------- /hooks/query/subscription.tsx: -------------------------------------------------------------------------------- 1 | import AxoisClient from "@lib/axios"; 2 | import { useQuery } from "@tanstack/react-query" 3 | import { AxiosResponse } from "axios"; 4 | 5 | import { UserSubscription } from "@pages/api/subscription"; 6 | import { ManageBillingData } from "@pages/api/subscription/manage"; 7 | import { useRouter } from "next/router"; 8 | 9 | const useSubscription = () => { 10 | const router = useRouter(); 11 | 12 | 13 | const { data, isLoading, error, status } = useQuery( 14 | ["subscriptions"], 15 | async () => { 16 | const { query } = router; 17 | 18 | const { data }: AxiosResponse = await AxoisClient.getInstance().get(`api/subscription`); 19 | 20 | return data; 21 | }, 22 | { 23 | refetchOnWindowFocus: false, 24 | refetchOnReconnect: false, 25 | retry: false, 26 | } 27 | ); 28 | 29 | return { 30 | data, 31 | isLoading, 32 | error, 33 | status, 34 | }; 35 | }; 36 | 37 | const useUserManageSubscriptionBilling = () => { 38 | const router = useRouter(); 39 | const { data, isLoading, error, status, refetch } = useQuery( 40 | ["manageBilling"], 41 | async () => { 42 | const { data }: AxiosResponse = await AxoisClient.getInstance().get('api/subscription/manage'); 43 | return data.url; 44 | }, 45 | { 46 | initialData: null, 47 | refetchOnWindowFocus: false, 48 | refetchOnReconnect: false, 49 | retry: false, 50 | } 51 | ); 52 | 53 | const openBilling = async () => { 54 | const data = await refetch(); 55 | 56 | if (data.data) router.replace(data.data); 57 | } 58 | 59 | return { 60 | data, 61 | isLoading, 62 | error, 63 | status, 64 | openBilling 65 | }; 66 | } 67 | 68 | export { 69 | useSubscription, 70 | useUserManageSubscriptionBilling 71 | }; -------------------------------------------------------------------------------- /hooks/query/users.tsx: -------------------------------------------------------------------------------- 1 | import AxoisClient from "@lib/axios"; 2 | import { useQuery } from "@tanstack/react-query" 3 | import { UsersData } from "@pages/api/users"; 4 | import { AxiosResponse } from "axios"; 5 | 6 | const useUsers = () => { 7 | const { data, isLoading, error } = useQuery( 8 | ["users"], 9 | async () => { 10 | const { data }: AxiosResponse = await AxoisClient.getInstance().get('api/users'); 11 | return data; 12 | }, 13 | { 14 | refetchOnWindowFocus: false 15 | } 16 | ); 17 | 18 | return { 19 | data, 20 | isLoading, 21 | error, 22 | }; 23 | }; 24 | 25 | export { 26 | useUsers, 27 | }; -------------------------------------------------------------------------------- /hooks/useOnClickOutside.tsx: -------------------------------------------------------------------------------- 1 | import { useEffect, RefObject } from "react"; 2 | 3 | function useOnClickOutside( 4 | ref: RefObject, 5 | handler: (event: MouseEvent | TouchEvent) => void 6 | ) { 7 | useEffect(() => { 8 | const listener = (event: MouseEvent | TouchEvent) => { 9 | // Do nothing if clicking ref's element or descendent elements 10 | if (!ref.current || ref.current.contains(event.target as Node)) { 11 | return; 12 | } 13 | handler(event); 14 | }; 15 | document.addEventListener("mousedown", listener); 16 | document.addEventListener("touchstart", listener); 17 | return () => { 18 | document.removeEventListener("mousedown", listener); 19 | document.removeEventListener("touchstart", listener); 20 | }; 21 | }, [ref, handler]); 22 | } 23 | 24 | export default useOnClickOutside; 25 | -------------------------------------------------------------------------------- /hooks/useYupValidationResolver.tsx: -------------------------------------------------------------------------------- 1 | import * as yup from 'yup'; 2 | import { useCallback } from 'react'; 3 | 4 | type FormValues = Record; 5 | type FormErrors = Record; 6 | 7 | type ValidationResult = { 8 | values: FormValues; 9 | errors: FormErrors; 10 | }; 11 | 12 | const useYupValidationResolver = >( 13 | validationSchema: yup.ObjectSchema 14 | ): ((data: FormValues) => Promise) => 15 | useCallback(async (data: FormValues) => { 16 | try { 17 | const values = await validationSchema.validate(data, { 18 | abortEarly: false, 19 | }); 20 | 21 | return { 22 | values, 23 | errors: {}, 24 | }; 25 | } catch (errors: any) { 26 | return { 27 | values: {}, 28 | errors: errors.inner.reduce( 29 | (allErrors: FormErrors, currentError: any) => ({ 30 | ...allErrors, 31 | [currentError.path]: { 32 | type: currentError.type ?? 'validation', 33 | message: currentError.message, 34 | }, 35 | }), 36 | {} as FormErrors 37 | ) 38 | }; 39 | } 40 | }, [validationSchema]); 41 | 42 | export default useYupValidationResolver; 43 | -------------------------------------------------------------------------------- /lib/axios.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosInstance } from "axios"; 2 | 3 | class AxoisClient { 4 | static client: AxoisClient; 5 | 6 | axios: AxiosInstance; 7 | 8 | constructor() { 9 | this.axios = axios.create({ baseURL: '/' }); 10 | this.axios.defaults.timeout = 35000; 11 | } 12 | 13 | setBearerToken = (token: string) => { 14 | this.axios.defaults.headers.common.Authorization = `Bearer ${token}`; 15 | }; 16 | 17 | static getInstance = () => { 18 | if (!AxoisClient.client) { 19 | AxoisClient.client = new AxoisClient(); 20 | } 21 | return AxoisClient.client.axios; 22 | }; 23 | } 24 | 25 | export default AxoisClient; 26 | -------------------------------------------------------------------------------- /lib/email/EmailProvider.ts: -------------------------------------------------------------------------------- 1 | class Email { 2 | to: string; 3 | subject: string; 4 | body: string; 5 | 6 | constructor(to: string, subject: string, body: string) { 7 | this.to = to; 8 | this.subject = subject; 9 | this.body = body; 10 | } 11 | } 12 | 13 | 14 | interface EmailProvider { 15 | sendEmail(email: Email): Promise; 16 | } 17 | 18 | export { 19 | Email 20 | } 21 | 22 | export default EmailProvider; -------------------------------------------------------------------------------- /lib/email/index.ts: -------------------------------------------------------------------------------- 1 | import EmailProvider, { Email } from "./EmailProvider"; 2 | 3 | import SMTPProvider from "./providers/SMTPProvider"; 4 | 5 | class EmailService { 6 | private provider: EmailProvider; 7 | 8 | private static instance: EmailService; 9 | 10 | private constructor(provider: EmailProvider) { 11 | this.provider = provider; 12 | } 13 | 14 | static getInstance(provider: EmailProvider): EmailService { 15 | if (!EmailService.instance) { 16 | EmailService.instance = new EmailService(provider); 17 | } 18 | return EmailService.instance; 19 | } 20 | 21 | async sendEmail(email: Email): Promise { 22 | // Call the sendEmail method on the email provider 23 | return this.provider.sendEmail(email); 24 | } 25 | } 26 | 27 | const emailProvider: EmailProvider = new SMTPProvider({ 28 | host: process.env.SMTP_HOST, 29 | port: process.env.SMTP_PORT as unknown as number, 30 | auth: { 31 | user: process.env.SMTP_USERNAME, 32 | pass: process.env.SMTP_PASSWORD 33 | } 34 | }, process.env.FROM_EMAIL as string 35 | ) 36 | 37 | const mail: EmailService = EmailService.getInstance(emailProvider); 38 | 39 | export default mail; -------------------------------------------------------------------------------- /lib/email/providers/SMTPProvider.ts: -------------------------------------------------------------------------------- 1 | import nodemailer from 'nodemailer'; 2 | import Mail from 'nodemailer/lib/mailer'; 3 | import SMTPTransport from 'nodemailer/lib/smtp-transport'; 4 | import EmailProvider, { Email } from '../EmailProvider'; 5 | 6 | class SMTPProvider implements EmailProvider { 7 | private client: nodemailer.Transporter; 8 | private fromEmail: string; 9 | constructor(config: SMTPTransport | SMTPTransport.Options, fromEmail: string) { 10 | this.fromEmail = fromEmail; 11 | this.client = nodemailer.createTransport(config); 12 | } 13 | 14 | async sendEmail(email: Email): Promise { 15 | const data: Mail.Options = { 16 | from: this.fromEmail, // replace with your email 17 | to: email.to, 18 | subject: email.subject, 19 | html: email.body, 20 | }; 21 | 22 | try { 23 | await this.client.sendMail(data); 24 | return true; 25 | } catch (err) { 26 | console.error(err); 27 | return false; 28 | } 29 | } 30 | } 31 | 32 | export default SMTPProvider; 33 | -------------------------------------------------------------------------------- /lib/email/templates/OrganisationInviteTeamMember.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Button, 4 | Container, 5 | Head, 6 | Html, 7 | Preview, 8 | Section, 9 | Text, 10 | } from '@react-email/components'; 11 | import * as React from 'react'; 12 | 13 | interface OrganizationInviteTeamMemberEmailProps { 14 | url?: string; 15 | } 16 | 17 | export const OrganizationInviteTeamMemberEmail = ({ 18 | url = '', 19 | }: OrganizationInviteTeamMemberEmailProps) => { 20 | return ( 21 | 22 | 23 | Invitation 24 | 25 | 26 |
    27 | Hi, 28 | 29 | 32 |
    33 |
    34 | 35 | 36 | ); 37 | }; 38 | 39 | export default OrganizationInviteTeamMemberEmail; 40 | 41 | const main = { 42 | backgroundColor: '#f6f9fc', 43 | padding: '10px 0', 44 | }; 45 | 46 | const container = { 47 | backgroundColor: '#ffffff', 48 | border: '1px solid #f0f0f0', 49 | padding: '45px', 50 | }; 51 | 52 | const text = { 53 | fontSize: '16px', 54 | fontFamily: 55 | "'Open Sans', 'HelveticaNeue-Light', 'Helvetica Neue Light', 'Helvetica Neue', Helvetica, Arial, 'Lucida Grande', sans-serif", 56 | fontWeight: '300', 57 | color: '#404040', 58 | lineHeight: '26px', 59 | }; 60 | 61 | const button = { 62 | backgroundColor: '#007ee6', 63 | borderRadius: '4px', 64 | color: '#fff', 65 | fontFamily: "'Open Sans', 'Helvetica Neue', Arial", 66 | fontSize: '15px', 67 | textDecoration: 'none', 68 | textAlign: 'center' as const, 69 | display: 'block', 70 | width: '210px', 71 | padding: '14px 7px', 72 | }; 73 | 74 | const anchor = { 75 | textDecoration: 'underline', 76 | }; -------------------------------------------------------------------------------- /lib/email/templates/ResetPassword.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Button, 4 | Container, 5 | Head, 6 | Html, 7 | Preview, 8 | Section, 9 | Text, 10 | } from '@react-email/components'; 11 | import * as React from 'react'; 12 | 13 | interface ResetPasswordEmailProps { 14 | resetPasswordLink?: string; 15 | } 16 | 17 | export const ResetPasswordEmail = ({ 18 | resetPasswordLink = '', 19 | }: ResetPasswordEmailProps) => { 20 | return ( 21 | 22 | 23 | Reset your password 24 | 25 | 26 |
    27 | Hi, 28 | 29 | Someone recently requested a password change for your 30 | account. If this was you, you can set a new password here: 31 | 32 | 35 | 36 | If you don't want to change your password or didn't 37 | request this, just ignore and delete this message. 38 | 39 | 40 | To keep your account secure, please don't forward this email 41 | to anyone. 42 | 43 |
    44 |
    45 | 46 | 47 | ); 48 | }; 49 | 50 | export default ResetPasswordEmail; 51 | 52 | const main = { 53 | backgroundColor: '#f6f9fc', 54 | padding: '10px 0', 55 | }; 56 | 57 | const container = { 58 | backgroundColor: '#ffffff', 59 | border: '1px solid #f0f0f0', 60 | padding: '45px', 61 | }; 62 | 63 | const text = { 64 | fontSize: '16px', 65 | fontFamily: 66 | "'Open Sans', 'HelveticaNeue-Light', 'Helvetica Neue Light', 'Helvetica Neue', Helvetica, Arial, 'Lucida Grande', sans-serif", 67 | fontWeight: '300', 68 | color: '#404040', 69 | lineHeight: '26px', 70 | }; 71 | 72 | const button = { 73 | backgroundColor: '#007ee6', 74 | borderRadius: '4px', 75 | color: '#fff', 76 | fontFamily: "'Open Sans', 'Helvetica Neue', Arial", 77 | fontSize: '15px', 78 | textDecoration: 'none', 79 | textAlign: 'center' as const, 80 | display: 'block', 81 | width: '210px', 82 | padding: '14px 7px', 83 | }; 84 | 85 | const anchor = { 86 | textDecoration: 'underline', 87 | }; -------------------------------------------------------------------------------- /lib/pagination/index.ts: -------------------------------------------------------------------------------- 1 | import { NextApiRequest } from "next" 2 | 3 | export interface PaginatedResult { 4 | data: T[] 5 | meta: { 6 | total: number 7 | lastPage: number 8 | currentPage: number 9 | perPage: number 10 | prev: number | null 11 | next: number | null 12 | } 13 | } 14 | 15 | export interface PaginatedNextApiRequest extends NextApiRequest { 16 | query: Partial<{ [key: string]: string | string[] | undefined }> & { page?: number }; 17 | } 18 | 19 | export type PaginateOptions = { page?: number | string, perPage?: number | string } 20 | export type PaginateFunction = (model: any, args?: K, options?: PaginateOptions) => Promise> 21 | 22 | export const createPaginator = (defaultOptions: PaginateOptions): PaginateFunction => { 23 | return async (model, args: any = { where: undefined }, options) => { 24 | const page = Number(options?.page || defaultOptions?.page) || 1 25 | const perPage = Number(options?.perPage || defaultOptions?.perPage) || 10 26 | 27 | const skip = page > 0 ? perPage * (page - 1) : 0 28 | const [total, data] = await Promise.all([ 29 | model.count({ where: args.where }), 30 | model.findMany({ 31 | ...args, 32 | take: perPage, 33 | skip, 34 | }), 35 | ]) 36 | const lastPage = Math.ceil(total / perPage) 37 | 38 | return { 39 | data, 40 | meta: { 41 | total, 42 | lastPage, 43 | currentPage: page, 44 | perPage, 45 | prev: page > 1 ? page - 1 : null, 46 | next: page < lastPage ? page + 1 : null, 47 | }, 48 | } 49 | } 50 | } -------------------------------------------------------------------------------- /lib/payments/constants.ts: -------------------------------------------------------------------------------- 1 | enum SUBSCRIPTION_PLAN { 2 | INDIVIDUAL = 'individual', 3 | TEAMS = 'teams', 4 | } 5 | 6 | export { 7 | SUBSCRIPTION_PLAN 8 | } -------------------------------------------------------------------------------- /lib/payments/stripe/index.ts: -------------------------------------------------------------------------------- 1 | import { Stripe, loadStripe } from '@stripe/stripe-js'; 2 | 3 | let stripePromise: Promise; 4 | const getStripe = () => { 5 | if (!stripePromise) { 6 | stripePromise = loadStripe(process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY!); 7 | } 8 | return stripePromise; 9 | }; 10 | 11 | export default getStripe; 12 | -------------------------------------------------------------------------------- /lib/payments/stripe/service.ts: -------------------------------------------------------------------------------- 1 | import Stripe from 'stripe'; 2 | 3 | export class StripeService { 4 | private static instance: StripeService; 5 | private stripe: Stripe; 6 | 7 | private constructor(secretKey: string) { 8 | this.stripe = new Stripe(secretKey, { 9 | apiVersion: "2022-11-15" 10 | }); 11 | } 12 | 13 | public static getInstance(secretKey: string): Stripe { 14 | if (!StripeService.instance) { 15 | StripeService.instance = new StripeService(secretKey); 16 | } 17 | return StripeService.instance.stripe; 18 | } 19 | 20 | public getStripe(): Stripe { 21 | return this.stripe; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /lib/prisma.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client'; 2 | 3 | class PrismaSingleton { 4 | static client: PrismaSingleton; 5 | 6 | prisma: PrismaClient; 7 | 8 | constructor() { 9 | if (process.env.NODE_ENV === 'production') { 10 | this.prisma = new PrismaClient({ log: ['info', 'warn'] }); 11 | } else { 12 | if (!global || !(global as any).prisma) { 13 | (global as any).prisma = new PrismaClient({ log: ['error'] }); 14 | } 15 | this.prisma = (global as any).prisma; 16 | } 17 | } 18 | 19 | static getInstance = () => { 20 | if (!PrismaSingleton.client) { 21 | PrismaSingleton.client = new PrismaSingleton(); 22 | } 23 | return PrismaSingleton.client; 24 | }; 25 | 26 | static getPrismaInstance = () => { 27 | if (!PrismaSingleton.client) { 28 | PrismaSingleton.client = new PrismaSingleton(); 29 | } 30 | return PrismaSingleton.client.prisma; 31 | }; 32 | } 33 | 34 | export default PrismaSingleton.getPrismaInstance(); -------------------------------------------------------------------------------- /middlewares/auth.ts: -------------------------------------------------------------------------------- 1 | import { Session } from 'next-auth' 2 | import { NextApiRequest, NextApiResponse } from 'next' 3 | import { getSession } from 'next-auth/react' 4 | import { NextHandler } from 'next-connect' 5 | 6 | export type NextApiRequestWithSession = NextApiRequest & { 7 | session: Session; 8 | }; 9 | 10 | const AuthMiddleWare = (roles: string[]) => { 11 | return async (req: NextApiRequestWithSession, res: NextApiResponse, next: NextHandler) => { 12 | 13 | if (roles.length === 0) { 14 | next(); 15 | return; 16 | } 17 | 18 | const session: Session | null = await getSession({ req }); 19 | 20 | if (!session) { 21 | res.statusCode = 400 22 | return res.json({ 23 | message: 'Unauthorized access', 24 | }) 25 | } 26 | 27 | req.session = session 28 | const { role } = session.user 29 | if (roles.findIndex((_r) => _r === role) > -1) { 30 | next() 31 | } else { 32 | res.statusCode = 405 33 | return res.json({ 34 | message: 'Bad request', 35 | }) 36 | } 37 | } 38 | } 39 | 40 | export { AuthMiddleWare } 41 | -------------------------------------------------------------------------------- /middlewares/organization.ts: -------------------------------------------------------------------------------- 1 | import { Session } from 'next-auth' 2 | import { NextApiResponse } from 'next' 3 | import { NextHandler } from 'next-connect' 4 | import { OrganizationRole } from '@prisma/client'; 5 | import { NextApiRequestWithSession } from './auth'; 6 | import prisma from '@lib/prisma'; 7 | 8 | export type NextApiOrganizationRequest = NextApiRequestWithSession & { 9 | session: Session; 10 | query: Partial<{ [key: string]: string | string[] | undefined }> & { id: string }; 11 | }; 12 | 13 | const OrganizationMiddleWare = (roles: OrganizationRole[]) => { 14 | return async (req: NextApiOrganizationRequest, res: NextApiResponse, next: NextHandler) => { 15 | 16 | const { session, query } = req; 17 | 18 | if (!session) { 19 | return res.status(400).json({ 20 | message: 'Unauthorized access', 21 | }) 22 | } 23 | 24 | if (roles.length === 0) { 25 | next(); 26 | return; 27 | } 28 | 29 | const { user } = session; 30 | 31 | const member = await prisma.organizationMember.findUnique({ 32 | where: { 33 | userId_organizationId: { 34 | userId: user.id as string, 35 | organizationId: query.id, 36 | } 37 | } 38 | }); 39 | 40 | if (!member) { 41 | return res.status(400).json({ 42 | message: 'Unauthorized access', 43 | }) 44 | } 45 | 46 | const { role } = member; 47 | if (roles.findIndex((_r) => _r === role) > -1) { 48 | next() 49 | } else { 50 | res.statusCode = 405 51 | return res.json({ 52 | message: 'Bad request', 53 | }) 54 | } 55 | } 56 | } 57 | 58 | export { OrganizationMiddleWare } 59 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | module.exports = { 3 | reactStrictMode: true, 4 | } 5 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "dev": "prisma generate && next dev", 5 | "build": "prisma generate && next build", 6 | "start": "next start", 7 | "prisma-sync-db": "npx prisma db push", 8 | "generate": "plop" 9 | }, 10 | "dependencies": { 11 | "@next-auth/prisma-adapter": "^1.0.5", 12 | "@prisma/client": "^4.11.0", 13 | "@radix-ui/react-dialog": "^1.0.3", 14 | "@radix-ui/react-popover": "^1.0.5", 15 | "@radix-ui/react-switch": "^1.0.2", 16 | "@react-email/components": "0.0.4", 17 | "@react-email/render": "0.0.6", 18 | "@stripe/stripe-js": "^1.49.0", 19 | "@tanstack/react-query": "^4.26.1", 20 | "@tanstack/react-table": "^8.7.9", 21 | "axios": "^1.3.4", 22 | "bcryptjs": "^2.4.3", 23 | "classnames": "^2.3.2", 24 | "form-data": "^4.0.0", 25 | "jsonwebtoken": "^9.0.0", 26 | "mailgun-js": "^0.22.0", 27 | "mailgun.js": "^8.2.1", 28 | "micro": "^10.0.1", 29 | "next": "latest", 30 | "next-auth": "^4.20.1", 31 | "next-connect": "^0.13.0", 32 | "nodemailer": "^6.9.1", 33 | "react": "18.2.0", 34 | "react-dom": "18.2.0", 35 | "react-hook-form": "^7.43.5", 36 | "react-hot-toast": "^2.4.0", 37 | "react-icons": "^4.8.0", 38 | "stripe": "^11.14.0", 39 | "yup": "^1.0.2" 40 | }, 41 | "devDependencies": { 42 | "@types/bcryptjs": "^2.4.2", 43 | "@types/jsonwebtoken": "^9.0.1", 44 | "@types/mailgun-js": "^0.22.13", 45 | "@types/next": "^9.0.0", 46 | "@types/node": "18.11.3", 47 | "@types/nodemailer": "^6.4.7", 48 | "@types/react": "18.0.21", 49 | "@types/react-dom": "18.0.6", 50 | "autoprefixer": "^10.4.12", 51 | "editorconfig": "^1.0.2", 52 | "plop": "^3.1.2", 53 | "postcss": "^8.4.18", 54 | "prisma": "^4.11.0", 55 | "tailwindcss": "^3.2.4", 56 | "typescript": "4.9.4" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /pages/403.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { NextPage } from 'next'; 4 | import { Button } from '@components/elements'; 5 | import { useRouter } from 'next/router'; 6 | 7 | const Dashboard: NextPage = () => { 8 | const router = useRouter(); 9 | return ( 10 |
    11 | 403 - Access Denied 12 |
    13 |
    14 | ) 15 | } 16 | 17 | export default Dashboard; 18 | -------------------------------------------------------------------------------- /pages/_app.tsx: -------------------------------------------------------------------------------- 1 | import { ReactElement, ReactNode } from 'react'; 2 | import { NextPage } from 'next'; 3 | import type { AppProps } from 'next/app' 4 | 5 | import type { Session } from "next-auth"; 6 | import { SessionProvider } from 'next-auth/react' 7 | 8 | import '../styles/globals.css' 9 | import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; 10 | import { Role } from '@prisma/client'; 11 | import AuthGuard from '@components/AuthGuard'; 12 | import { Toaster } from 'react-hot-toast'; 13 | 14 | import { SUBSCRIPTION_PLAN } from '@lib/payments/constants'; 15 | import SubscriptionGuard from '@components/SubscriptionGuard'; 16 | 17 | export type NextPageWithProps

    = NextPage & { 18 | requireAuth?: boolean, 19 | roles?: Role[] | undefined; 20 | 21 | requireSubscription?: boolean; 22 | plans?: SUBSCRIPTION_PLAN[] 23 | }; 24 | 25 | export type AppPropsWithExtra

    = AppProps

    & { 26 | Component: NextPageWithProps

    ; 27 | }; 28 | 29 | export const queryClient = new QueryClient(); 30 | 31 | const App = ({ Component, pageProps }: AppPropsWithExtra<{ session: Session; }>) => { 32 | const { session } = pageProps; 33 | return ( 34 | 35 | 36 | {Component.requireAuth ? ( 37 | 38 | {Component.requireSubscription ? ( 39 | 40 | 41 | 42 | ): ( 43 | 44 | )} 45 | 46 | ) : ( 47 | 48 | )} 49 | 50 | 51 | 52 | ); 53 | } 54 | 55 | export default App; 56 | -------------------------------------------------------------------------------- /pages/account/billing/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { NextPage } from 'next'; 3 | import { CurrentPlanCard, MainLayout } from '@components/index'; 4 | import AccountLayout from '@components/layouts/AccountLayout'; 5 | 6 | 7 | const Profile: NextPage = () => { 8 | 9 | 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | ) 17 | } 18 | 19 | export default Profile; -------------------------------------------------------------------------------- /pages/account/index.tsx: -------------------------------------------------------------------------------- 1 | import { MainLayout, Tabs } from '@components/index'; 2 | import { TabItem } from '@components/Tabs/Tab'; 3 | import { NextPageWithProps } from '@pages/_app'; 4 | import { Role } from '@prisma/client'; 5 | import { useRouter } from 'next/router'; 6 | import React, { useEffect } from 'react' 7 | 8 | const UserAccount: NextPageWithProps = () => { 9 | const router = useRouter(); 10 | 11 | useEffect(() => { 12 | router.push('/account/profile'); 13 | }, []); 14 | return ( 15 | 16 | 17 | ) 18 | } 19 | UserAccount.requireAuth = true; 20 | UserAccount.roles = [ 21 | Role.customer, 22 | Role.superadmin 23 | ]; 24 | export default UserAccount; 25 | -------------------------------------------------------------------------------- /pages/account/password/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { NextPage } from 'next'; 3 | import { MainLayout } from '@components/index'; 4 | import AccountLayout from '@components/layouts/AccountLayout'; 5 | import { ChangePasswordForm } from '@components/forms'; 6 | import { useUpdatePassword } from '@hooks/query/currentUser'; 7 | 8 | const ChangePasswordPage: NextPage = () => { 9 | const { updatePassword, isUpdateLoading } = useUpdatePassword(); 10 | return ( 11 | 12 | 13 | 14 | 15 | 16 | ) 17 | } 18 | 19 | export default ChangePasswordPage; -------------------------------------------------------------------------------- /pages/account/profile/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { NextPage } from 'next'; 3 | import { MainLayout } from '@components/index'; 4 | import AccountLayout from '@components/layouts/AccountLayout'; 5 | import { ProfileForm } from '@components/forms'; 6 | import { useUser } from '@hooks/query/currentUser'; 7 | 8 | const Profile: NextPage = () => { 9 | const { updateUser, data, isLoading, isUpdateLoading } = useUser(); 10 | return ( 11 | 12 | 13 | <> 14 | {isLoading && <>Loading...} 15 | {!isLoading && } 16 | 17 | 18 | 19 | ) 20 | } 21 | 22 | export default Profile; -------------------------------------------------------------------------------- /pages/api/auth/[...nextauth].ts: -------------------------------------------------------------------------------- 1 | import NextAuth, { Awaitable, NextAuthOptions, User as NextAuthUser } from "next-auth"; 2 | 3 | import { PrismaAdapter } from "@next-auth/prisma-adapter"; 4 | import GoogleProvider from "next-auth/providers/google"; 5 | import GitHubProvider from "next-auth/providers/github"; 6 | import TwitterProvider from 'next-auth/providers/twitter'; 7 | // import FacebookProvider from 'next-auth/providers/facebook'; 8 | 9 | 10 | import CredentialsProvider from "next-auth/providers/credentials"; 11 | 12 | import prisma from "@lib/prisma"; 13 | import { User } from "@prisma/client"; 14 | 15 | import HashUtil from "@utils/HashUtil"; 16 | 17 | export const nextAuthOptions: NextAuthOptions = { 18 | adapter: PrismaAdapter(prisma), 19 | secret: process.env.SECRET, 20 | debug: false, 21 | pages: { 22 | signIn: '/auth/login', 23 | }, 24 | session: { 25 | strategy: 'jwt', 26 | }, 27 | callbacks: { 28 | 29 | jwt: async (params) => { 30 | const { user, token } = params; 31 | if (user) { 32 | token.user = user; 33 | } 34 | return token; 35 | }, 36 | session: async (params) => { 37 | const { session } = params; 38 | if (!session) return session; 39 | const user = await prisma.user.findUnique({ 40 | where: { 41 | email: session.user.email as string 42 | }, 43 | select: { 44 | role: true, 45 | id: true, 46 | name: true, 47 | memberInOrganizations: { 48 | select: { 49 | id: true, 50 | role: true, 51 | } 52 | } 53 | } 54 | }); 55 | 56 | session.user.isOrganizationAdmin = user?.memberInOrganizations.findIndex((org) => org.role === 'org_admin') !== -1; 57 | 58 | // if member detail registered as OrganizationMember it will be true 59 | session.user.isOrganizationUser = user?.memberInOrganizations && user?.memberInOrganizations.length > 0; 60 | 61 | session.user.name = user?.name as string; 62 | session.user.role = user?.role as string; 63 | session.user.id = user?.id as string; 64 | return session; 65 | }, 66 | }, 67 | providers: [ 68 | GoogleProvider({ 69 | clientId: process.env.GOOGLE_CLIENT_ID as string, 70 | clientSecret: process.env.GOOGLE_CLIENT_SECRET as string 71 | }), 72 | // TwitterProvider({ 73 | // clientId: process.env.GOOGLE_CLIENT_ID as string, 74 | // clientSecret: process.env.GOOGLE_CLIENT_SECRET as string 75 | // }), 76 | // FacebookProvider({ 77 | // clientId: process.env.GOOGLE_CLIENT_ID as string, 78 | // clientSecret: process.env.GOOGLE_CLIENT_SECRET as string 79 | // }), 80 | CredentialsProvider({ 81 | type: 'credentials', 82 | credentials: { 83 | username: { label: "Username", type: "text" }, 84 | password: { label: "Password", type: "password" } 85 | }, 86 | authorize: (credentials, _req): Awaitable => { 87 | return new Promise(async (resolve, reject) => { 88 | if (!credentials?.username) { 89 | return reject(new Error('Username empty')) 90 | } 91 | 92 | const user: User | null = await prisma.user.findUnique({ 93 | where: { 94 | email: credentials?.username 95 | } 96 | }); 97 | 98 | if (!user) { 99 | return reject(new Error('Invalid credentials')) 100 | } 101 | 102 | const isValid = await HashUtil.compareHash(credentials.password, user.password as string); 103 | 104 | if (!isValid) { 105 | return reject(new Error('Invalid credentials')) 106 | } 107 | 108 | resolve({ 109 | id: user.id, 110 | email: user.email, 111 | name: user.name 112 | }); 113 | }) 114 | } 115 | }), 116 | 117 | GitHubProvider({ 118 | clientId: process.env.GITHUB_ID as string, 119 | clientSecret: process.env.GITHUB_SECRET as string, 120 | }), 121 | ] 122 | }; 123 | 124 | export default NextAuth(nextAuthOptions); -------------------------------------------------------------------------------- /pages/api/auth/change-password.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiResponse } from 'next' 2 | import nextConnect from 'next-connect'; 3 | import { AuthMiddleWare, NextApiRequestWithSession } from 'middlewares/auth'; 4 | import { Role, User } from '@prisma/client'; 5 | import prisma from '@lib/prisma'; 6 | import { ChangePasswordFormValues } from '@components/forms/ChangePasswordForm/ChangePasswordForm'; 7 | import HashUtil from '@utils/HashUtil'; 8 | 9 | const handler = nextConnect(); 10 | handler.use(AuthMiddleWare([Role.customer, Role.superadmin])); 11 | 12 | interface NextUpdateUserApiRequest extends NextApiRequestWithSession { 13 | body: ChangePasswordFormValues 14 | } 15 | 16 | handler.patch(async ( 17 | req: NextUpdateUserApiRequest, 18 | res: NextApiResponse, 19 | ) => { 20 | const userId = req.session?.user.id; 21 | 22 | const { password, newPassword } = req.body; 23 | 24 | const user: User | null = await prisma.user.findUnique({ 25 | where: { 26 | id: userId as string 27 | } 28 | }); 29 | 30 | if (!user || !user.password) { 31 | res.status(400).json({ message: 'You signed up using a social account, Contact customer support if you need further assistance.' }); 32 | return; 33 | } 34 | 35 | const isValid = await HashUtil.compareHash(password, user.password as string); 36 | 37 | if (!isValid) { 38 | res.status(400).json({ message: 'Wrong password' }); 39 | return; 40 | } 41 | const passwordHash: string = await HashUtil.createHash(newPassword); 42 | 43 | const updated = await prisma.user.update({ 44 | where: { id: userId as string }, 45 | data: { password: passwordHash } 46 | }); 47 | 48 | res.status(200).json({ message: 'password updated' }); 49 | }) 50 | 51 | export default handler; 52 | -------------------------------------------------------------------------------- /pages/api/auth/forgot.ts: -------------------------------------------------------------------------------- 1 | import prisma from "@lib/prisma"; 2 | import type { NextApiRequest, NextApiResponse } from "next"; 3 | import nc from "next-connect"; 4 | 5 | import { User } from "@prisma/client"; 6 | import mail from "@lib/email"; 7 | import { render } from "@react-email/render"; 8 | import ResetPasswordEmail from "@lib/email/templates/ResetPassword"; 9 | 10 | interface ForgotApiRequest extends NextApiRequest { 11 | body: { 12 | email: string; 13 | }; 14 | } 15 | 16 | const handler = nc(); 17 | 18 | handler.post(async (req: ForgotApiRequest, res: NextApiResponse) => { 19 | const { email } = req.body; 20 | 21 | if (!email) { 22 | return res.status(400).json({ message: "Bad request" }); 23 | } 24 | 25 | try { 26 | const existingUser: User | null = await prisma.user.findUnique({ where: { email } }); 27 | 28 | if (!existingUser) { 29 | return res.status(400).json({ message: "User not found" }); 30 | } 31 | 32 | const token = Math.random().toString(36).slice(2); 33 | const expirationTime = new Date(Date.now() + 24 * 3600 * 1000); // Token expires in 24 hours 34 | await prisma.passwordResetToken.create({ 35 | data: { 36 | token, 37 | expires: expirationTime, 38 | user: { connect: { id: existingUser.id } }, 39 | }, 40 | }); 41 | 42 | const resetPasswordLink = `${process.env.NEXTAUTH_URL}/auth/reset-password?token=${token}`; 43 | 44 | await mail.sendEmail({ 45 | to: email, 46 | subject: 'Reset Password Request', 47 | body: render(ResetPasswordEmail({ resetPasswordLink })), 48 | }); 49 | 50 | res.status(200).json({ name: "Password reset link sent to your mail" }); 51 | } catch(e: any) { 52 | res.status(500).json({ message: e.message || "Something went wrong" }) 53 | } 54 | }); 55 | 56 | export default handler; -------------------------------------------------------------------------------- /pages/api/auth/register.ts: -------------------------------------------------------------------------------- 1 | import prisma from "@lib/prisma"; 2 | import type { NextApiRequest, NextApiResponse } from "next"; 3 | import nc from "next-connect"; 4 | 5 | import HashUtil from "@utils/HashUtil"; 6 | import { User } from "@prisma/client"; 7 | 8 | interface RegisterApiRequest extends NextApiRequest { 9 | body: { 10 | name: string; 11 | email: string; 12 | password: string; 13 | }; 14 | } 15 | 16 | const handler = nc(); 17 | 18 | handler.post(async (req: RegisterApiRequest, res: NextApiResponse) => { 19 | const { name, email, password } = req.body; 20 | 21 | if (!name || !email || !password) { 22 | return res.status(400).json({ message: "Bad request" }); 23 | } 24 | 25 | try { 26 | const existingUser: User | null = await prisma.user.findUnique({ where: { email } }); 27 | 28 | if (existingUser) { 29 | return res.status(400).json({ message: "Account already exist" }); 30 | } 31 | 32 | const passwordHash: string = await HashUtil.createHash(password); 33 | 34 | await prisma.user.create({ 35 | data: { 36 | name, 37 | email, 38 | password: passwordHash, 39 | } 40 | }); 41 | 42 | res.status(200).json({ name: "Account created" }); 43 | } catch(e: any) { 44 | res.status(500).json({ message: e.message || "Something went wrong" }) 45 | } 46 | }); 47 | 48 | export default handler; -------------------------------------------------------------------------------- /pages/api/auth/reset.ts: -------------------------------------------------------------------------------- 1 | import prisma from "@lib/prisma"; 2 | import type { NextApiRequest, NextApiResponse } from "next"; 3 | import nc from "next-connect"; 4 | 5 | import HashUtil from "@utils/HashUtil"; 6 | 7 | interface ResetPasswordApiRequest extends NextApiRequest { 8 | body: { 9 | password: string; 10 | token: string; 11 | }; 12 | } 13 | 14 | const handler = nc(); 15 | 16 | handler.post(async (req: ResetPasswordApiRequest, res: NextApiResponse) => { 17 | const { password, token } = req.body; 18 | 19 | if (!password) { 20 | return res.status(400).json({ message: "Bad request" }); 21 | } 22 | 23 | try { 24 | // Find the password reset token 25 | const resetToken = await prisma.passwordResetToken.findUnique({ 26 | where: { token: String(token) }, 27 | include: { user: true }, 28 | }); 29 | 30 | if (!resetToken || resetToken.expires < new Date()) { 31 | return res.status(400).json({ message: 'Invalid or expired token' }); 32 | } 33 | 34 | const passwordHash: string = await HashUtil.createHash(password); 35 | await prisma.user.update({ 36 | where: { id: resetToken.user.id }, 37 | data: { password: passwordHash }, 38 | }); 39 | 40 | await prisma.passwordResetToken.delete({ where: { id: resetToken.id } }); 41 | 42 | res.status(200).json({ success: true }); 43 | 44 | } catch(e: any) { 45 | res.status(500).json({ message: e.message || "Something went wrong" }) 46 | } 47 | }); 48 | 49 | export default handler; -------------------------------------------------------------------------------- /pages/api/checkout/index.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiResponse } from 'next' 2 | import Stripe from 'stripe'; 3 | import { StripeService } from '@lib/payments/stripe/service'; 4 | import nextConnect from 'next-connect'; 5 | import { AuthMiddleWare, NextApiRequestWithSession } from 'middlewares/auth'; 6 | import { Role } from '@prisma/client'; 7 | import prisma from '@lib/prisma'; 8 | import { OrganizationFormValues } from '@components/forms/OrganizationForm/OrganizationForm'; 9 | 10 | export type CheckoutBody = { 11 | priceId: string | undefined; 12 | organization: OrganizationFormValues | undefined; 13 | organizationId: string | undefined; 14 | } 15 | 16 | interface CheckoutApiRequest extends NextApiRequestWithSession { 17 | body: CheckoutBody; 18 | } 19 | 20 | export type CheckoutData = { 21 | session: Stripe.Checkout.Session 22 | } 23 | 24 | const handler = nextConnect(); 25 | handler.use(AuthMiddleWare([Role.customer])); 26 | 27 | handler.post(async ( 28 | req: CheckoutApiRequest, 29 | res: NextApiResponse 30 | ) => { 31 | const user = req.session?.user; 32 | const { priceId, organization, organizationId } = req.body; 33 | 34 | const currentUser = await prisma.user.findUnique({ 35 | where: { id: user?.id as string } 36 | }) 37 | 38 | let createdOrganization = null; 39 | if (organization) { 40 | createdOrganization = await prisma.organization.create({ 41 | data: { 42 | name: organization?.name, 43 | status: 'inactive' 44 | } 45 | }); 46 | } 47 | 48 | const orgId: string | null = createdOrganization && createdOrganization.id ? createdOrganization.id : ( 49 | organizationId ? organizationId: null 50 | ); 51 | 52 | const params: Stripe.Checkout.SessionCreateParams = { 53 | mode: 'subscription', 54 | client_reference_id: user?.id.toString() || undefined, 55 | customer_email: currentUser?.stripeCustomerId ? undefined : user?.email as string, 56 | line_items: [ 57 | { 58 | price: priceId, 59 | quantity: 1, 60 | } 61 | ], 62 | customer: currentUser?.stripeCustomerId || undefined, 63 | subscription_data: { 64 | metadata: { 65 | userId: user?.id || null, 66 | email: user?.email || null, 67 | organizationId: orgId, 68 | } 69 | }, 70 | success_url: orgId ? `${req.headers.origin}/organizations/${orgId}/billing`: `${req.headers.origin}/pricing`, 71 | cancel_url: `${req.headers.origin}/pricing`, 72 | }; 73 | 74 | const checkoutSession: Stripe.Checkout.Session = await StripeService 75 | .getInstance(process.env.STRIPE_SECRET_KEY as string) 76 | .checkout.sessions.create(params); 77 | 78 | res.status(200).json({ session: checkoutSession }) 79 | }); 80 | 81 | export default handler; -------------------------------------------------------------------------------- /pages/api/hello.ts: -------------------------------------------------------------------------------- 1 | // Next.js API route support: https://nextjs.org/docs/api-routes/introduction 2 | import type { NextApiRequest, NextApiResponse } from 'next' 3 | 4 | type Data = { 5 | name: string 6 | } 7 | 8 | export default function handler( 9 | req: NextApiRequest, 10 | res: NextApiResponse 11 | ) { 12 | res.status(200).json({ name: 'John Doe' }) 13 | } 14 | -------------------------------------------------------------------------------- /pages/api/invitations/accept.ts: -------------------------------------------------------------------------------- 1 | import { Role } from "@prisma/client"; 2 | import { AuthMiddleWare, NextApiRequestWithSession } from "middlewares/auth"; 3 | import { NextApiResponse } from "next"; 4 | import nextConnect from "next-connect"; 5 | import jwt, { TokenExpiredError } from 'jsonwebtoken'; 6 | import prisma from "@lib/prisma"; 7 | import { TokenPayload } from "../organizations/[id]/invitations/index"; 8 | 9 | const handler = nextConnect(); 10 | handler.use(AuthMiddleWare([Role.customer, Role.superadmin])); 11 | 12 | handler.post(async (req: NextApiRequestWithSession, res: NextApiResponse) => { 13 | const { token } = req.body; 14 | const { session } = req; 15 | 16 | const userId = session?.user.id; 17 | 18 | try { 19 | const { email, organizationId } = jwt.verify(token, process.env.SECRET!) as TokenPayload; 20 | // Find the invitation in the database by email and organizationId 21 | const invitation = await prisma.organizationMemberInvitation.findUnique({ 22 | where: { 23 | email_organizationId: { 24 | email: email, 25 | organizationId: organizationId, 26 | }, 27 | }, 28 | }); 29 | 30 | // Check if the invitation has expired 31 | const now = new Date(); 32 | if (invitation && invitation.expiresAt && invitation.expiresAt.getTime() <= now.getTime()) { 33 | // The invitation has expired, delete it from the database 34 | await prisma.organizationMemberInvitation.delete({ 35 | where: { id: invitation.id }, 36 | }); 37 | 38 | return res.status(400).json({ message: 'The invitation has expired' }); 39 | } 40 | 41 | // ... continue with accepting the invitation if it's valid ... 42 | await prisma.organizationMember.create({ 43 | data: { 44 | role: invitation?.role, 45 | organization: { 46 | connect: { id: organizationId } 47 | }, 48 | user: { 49 | connect: { id: userId as string } 50 | } 51 | } 52 | }); 53 | 54 | if (invitation) { 55 | await prisma.organizationMemberInvitation.delete({ 56 | where: { id: invitation.id }, 57 | }); 58 | } 59 | 60 | 61 | return res.status(200).json({ message: 'Accepted', data: { organizationId } }); 62 | 63 | } catch (error) { 64 | 65 | if (error instanceof TokenExpiredError) { 66 | const tokenData = jwt.decode(token) as TokenPayload; 67 | 68 | try { 69 | await prisma.organizationMemberInvitation.delete({ 70 | where: { 71 | email_organizationId: { 72 | email: tokenData.email, 73 | organizationId: tokenData.organizationId 74 | } 75 | }, 76 | }); 77 | return res.status(400).json({ message: 'The invitation has expired' }); 78 | } catch(err) { 79 | return res.status(400).json({ message: 'Invalid token' }); 80 | } 81 | } 82 | 83 | return res.status(400).json({ message: 'Invalid token' }); 84 | } 85 | }); 86 | 87 | export default handler; 88 | -------------------------------------------------------------------------------- /pages/api/organizations/[id]/index.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiResponse } from 'next' 2 | import nextConnect from 'next-connect'; 3 | import { AuthMiddleWare, NextApiRequestWithSession } from 'middlewares/auth'; 4 | import { OrganizationRole, Role } from '@prisma/client'; 5 | import { OrganizationMiddleWare } from 'middlewares/organization'; 6 | 7 | 8 | const handler = nextConnect(); 9 | handler.use(AuthMiddleWare([Role.customer, Role.superadmin])); 10 | handler.use(OrganizationMiddleWare([OrganizationRole.org_admin, OrganizationRole.org_user])); 11 | 12 | handler.get(async ( 13 | req: NextApiRequestWithSession, 14 | res: NextApiResponse, 15 | ) => { 16 | 17 | res.status(200).json({ success: 'build something here' }) 18 | }); 19 | 20 | 21 | export default handler; 22 | -------------------------------------------------------------------------------- /pages/api/organizations/[id]/invitations/cancel.ts: -------------------------------------------------------------------------------- 1 | import { NextApiResponse } from "next"; 2 | import nextConnect from "next-connect"; 3 | 4 | import prisma from "@lib/prisma"; 5 | import { OrganizationRole, Role } from "@prisma/client"; 6 | 7 | import { AuthMiddleWare, NextApiRequestWithSession } from "middlewares/auth"; 8 | import { OrganizationMiddleWare } from "middlewares/organization"; 9 | 10 | const handler = nextConnect(); 11 | handler.use(AuthMiddleWare([Role.customer, Role.superadmin])); 12 | handler.use(OrganizationMiddleWare([OrganizationRole.org_admin])); 13 | 14 | interface NextApiRequestWithId extends NextApiRequestWithSession { 15 | query: Partial<{ [key: string]: string | string[] | undefined }> & { id: string }; 16 | body: { 17 | invitationId: string; 18 | } 19 | } 20 | handler.post(async (req: NextApiRequestWithId, res: NextApiResponse) => { 21 | const { body } = req; 22 | const { invitationId } = body; 23 | 24 | await prisma.organizationMemberInvitation.delete({ 25 | where: { 26 | id: invitationId, 27 | } 28 | }) 29 | 30 | res.status(200); 31 | }) 32 | 33 | export default handler; -------------------------------------------------------------------------------- /pages/api/organizations/[id]/invitations/index.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiResponse } from 'next' 2 | import prisma from '@lib/prisma' 3 | import { OrganizationInvitationStatus, OrganizationRole, Prisma, Role } from '@prisma/client'; 4 | import { createPaginator, PaginatedNextApiRequest, PaginatedResult } from '@lib/pagination'; 5 | import nextConnect from 'next-connect'; 6 | import { AuthMiddleWare, NextApiRequestWithSession } from 'middlewares/auth'; 7 | 8 | export interface OrganizationInvitationMemberData { 9 | id: string; 10 | role: OrganizationRole, 11 | email: string, 12 | status: OrganizationInvitationStatus, 13 | createdAt: true, 14 | updatedAt: true, 15 | } 16 | 17 | export type OrganizationInvitationsMembersData = PaginatedResult; 18 | const handler = nextConnect(); 19 | handler.use(AuthMiddleWare([Role.customer, Role.superadmin])); 20 | 21 | const paginate = createPaginator({ perPage: 20 }); 22 | 23 | interface NextApiRequestWithId extends NextApiRequestWithSession { 24 | query: Partial<{ [key: string]: string | string[] | undefined }> & { id: string }; 25 | } 26 | 27 | handler.get(async ( 28 | req: NextApiRequestWithId & PaginatedNextApiRequest & NextApiRequestWithSession, 29 | res: NextApiResponse 30 | ) => { 31 | const { query } = req; 32 | const { page, id } = query; 33 | const result = await paginate( 34 | prisma.organizationMemberInvitation, 35 | { 36 | where: { 37 | organizationId: id, 38 | status: { 39 | in: ['declined', 'pending'] 40 | } 41 | }, 42 | select: { 43 | id: true, 44 | role: true, 45 | email: true, 46 | status: true, 47 | createdAt: true, 48 | updatedAt: true, 49 | } 50 | }, 51 | { page: page } 52 | ); 53 | res.status(200).json(result); 54 | }); 55 | 56 | export default handler; 57 | -------------------------------------------------------------------------------- /pages/api/organizations/[id]/invitations/send.ts: -------------------------------------------------------------------------------- 1 | import { NextApiResponse } from "next"; 2 | import nextConnect from "next-connect"; 3 | import jwt from 'jsonwebtoken'; 4 | import { render } from "@react-email/render"; 5 | import { OrganizationRole, Role } from "@prisma/client"; 6 | 7 | import { AuthMiddleWare, NextApiRequestWithSession } from "middlewares/auth"; 8 | import prisma from "@lib/prisma"; 9 | import { OrganizationMemberInviteFormValues } from "@components/forms/OrganizationMemberInviteForm/OrganizationMemberInviteForm"; 10 | import mail from "@lib/email"; 11 | import OrganizationInviteTeamMemberEmail from "@lib/email/templates/OrganisationInviteTeamMember"; 12 | import { OrganizationMiddleWare } from "middlewares/organization"; 13 | 14 | const handler = nextConnect(); 15 | handler.use(AuthMiddleWare([Role.customer, Role.superadmin])); 16 | handler.use(OrganizationMiddleWare([OrganizationRole.org_admin])); 17 | 18 | export interface TokenPayload { 19 | email: string; 20 | organizationId: string; 21 | } 22 | interface SendInivitationApiRequest extends NextApiRequestWithSession { 23 | body: OrganizationMemberInviteFormValues; 24 | query: Partial<{ [key: string]: string | string[] | undefined }> & { id: string }; 25 | } 26 | 27 | handler.post(async( 28 | req: SendInivitationApiRequest, 29 | res: NextApiResponse 30 | ) => { 31 | const { body, query } = req; 32 | const { id } = query; 33 | const { email } = body; 34 | 35 | const trimmedEmail = email.trim(); 36 | const tokenPayload: TokenPayload = { 37 | email: trimmedEmail, 38 | organizationId: id, 39 | }; 40 | 41 | const isEmailAlreadyMember = await prisma.organizationMember.findMany({ 42 | where: { 43 | organizationId: id, 44 | user: { 45 | email: trimmedEmail, 46 | } 47 | } 48 | }); 49 | 50 | if (isEmailAlreadyMember.length > 0) { 51 | return res.status(400).json({ message: 'This user already a member in the organization!' }); 52 | } 53 | 54 | const isInvitationAlreadySent = await prisma.organizationMemberInvitation.findUnique({ 55 | where: { 56 | email_organizationId: { 57 | email: trimmedEmail, 58 | organizationId: id, 59 | } 60 | } 61 | }); 62 | 63 | if (isInvitationAlreadySent) { 64 | return res.status(400).json({ message: 'Invitation already sent' }); 65 | } 66 | 67 | const now = new Date(); 68 | const expiresInMs = 60 * 60 * 1000; // 1 hour in milliseconds 69 | const expiresAt = new Date(now.getTime() + expiresInMs); 70 | 71 | const token = jwt.sign(tokenPayload, process.env.SECRET!, { expiresIn: '1h' }); 72 | 73 | await prisma.organizationMemberInvitation.create({ 74 | data: { 75 | email: trimmedEmail, 76 | organization: { connect: { id: id } }, 77 | token: token, 78 | expiresAt: expiresAt, 79 | } 80 | }); 81 | 82 | const preparedUrl = `${process.env.NEXTAUTH_URL}/invitation?token=${token}`; 83 | 84 | await mail.sendEmail({ 85 | to: trimmedEmail, 86 | subject: 'Organization Invitaion', 87 | body: render(OrganizationInviteTeamMemberEmail({ url: preparedUrl })), 88 | }); 89 | 90 | console.log(preparedUrl); 91 | res.status(200).json({ message: 'invitation sent' }); 92 | }); 93 | 94 | export default handler; 95 | -------------------------------------------------------------------------------- /pages/api/organizations/[id]/member.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiResponse } from 'next' 2 | import nextConnect from 'next-connect'; 3 | import { AuthMiddleWare, NextApiRequestWithSession } from 'middlewares/auth'; 4 | import { OrganizationRole, Role } from '@prisma/client'; 5 | import prisma from '@lib/prisma'; 6 | 7 | 8 | const handler = nextConnect(); 9 | handler.use(AuthMiddleWare([Role.customer, Role.superadmin])); 10 | 11 | export type OrganizationMemberData = { 12 | data: { 13 | user: { 14 | name: string | null; 15 | email: string | null; 16 | } | null; 17 | id: string; 18 | role: OrganizationRole; 19 | createdAt: Date; 20 | } | null, 21 | message?: string; 22 | } 23 | 24 | interface OrganizationMemberApiRequest extends NextApiRequestWithSession { 25 | query: Partial<{ [key: string]: string | string[] | undefined }> & { id: string }; 26 | } 27 | 28 | handler.get(async ( 29 | req: OrganizationMemberApiRequest, 30 | res: NextApiResponse, 31 | ) => { 32 | const organizationId = req.query.id; 33 | const userId = req.session?.user.id; 34 | 35 | const user = await prisma.organizationMember.findUnique({ 36 | where: { 37 | userId_organizationId: { 38 | userId: userId as string, 39 | organizationId: organizationId 40 | } 41 | }, 42 | select: { 43 | id: true, 44 | role: true, 45 | user: { 46 | select: { 47 | name: true, 48 | email: true, 49 | } 50 | }, 51 | createdAt: true, 52 | } 53 | }); 54 | 55 | res.status(200).json({ message: '', data: user }) 56 | }); 57 | 58 | 59 | export default handler; 60 | -------------------------------------------------------------------------------- /pages/api/organizations/[id]/members/[memberId]/index.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiResponse } from 'next' 2 | import { OrganizationRole, Role } from '@prisma/client'; 3 | import { PaginatedNextApiRequest } from '@lib/pagination'; 4 | import nextConnect from 'next-connect'; 5 | import { AuthMiddleWare, NextApiRequestWithSession } from 'middlewares/auth'; 6 | import { OrganizationMiddleWare } from 'middlewares/organization'; 7 | import prisma from '@lib/prisma'; 8 | 9 | const handler = nextConnect(); 10 | handler.use(AuthMiddleWare([Role.customer, Role.superadmin])); 11 | handler.use(OrganizationMiddleWare([OrganizationRole.org_admin])); 12 | 13 | interface NextApiRequestWithId extends NextApiRequestWithSession { 14 | query: Partial<{ [key: string]: string | string[] | undefined }> & { id: string, memberId: string }; 15 | } 16 | 17 | handler.delete(async ( 18 | req: NextApiRequestWithId & PaginatedNextApiRequest & NextApiRequestWithSession, 19 | res: NextApiResponse 20 | ) => { 21 | const { query } = req; 22 | const { memberId } = query; 23 | 24 | await prisma.organizationMember.delete({ 25 | where: { 26 | id: memberId 27 | } 28 | }); 29 | return res.status(200).json({ message: 'removed' }); 30 | }); 31 | 32 | export default handler; 33 | -------------------------------------------------------------------------------- /pages/api/organizations/[id]/members/index.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiResponse } from 'next' 2 | import prisma from '@lib/prisma' 3 | import { OrganizationRole, Prisma, Role } from '@prisma/client'; 4 | import { createPaginator, PaginatedNextApiRequest, PaginatedResult } from '@lib/pagination'; 5 | import nextConnect from 'next-connect'; 6 | import { AuthMiddleWare, NextApiRequestWithSession } from 'middlewares/auth'; 7 | 8 | export interface OrganizationMemberData { 9 | id: string; 10 | role: OrganizationRole; 11 | user: { 12 | name: string; 13 | email: string; 14 | } 15 | } 16 | 17 | export type OrganizationMembersData = PaginatedResult; 18 | const handler = nextConnect(); 19 | handler.use(AuthMiddleWare([Role.customer, Role.superadmin])); 20 | 21 | const paginate = createPaginator({ perPage: 20 }); 22 | 23 | interface NextApiRequestWithId extends NextApiRequestWithSession { 24 | query: Partial<{ [key: string]: string | string[] | undefined }> & { id: string }; 25 | } 26 | 27 | handler.get(async ( 28 | req: NextApiRequestWithId & PaginatedNextApiRequest & NextApiRequestWithSession, 29 | res: NextApiResponse 30 | ) => { 31 | const { query } = req; 32 | const { page, id } = query; 33 | const result = await paginate( 34 | prisma.organizationMember, 35 | { 36 | where: { 37 | organizationId: id, 38 | }, 39 | select: { 40 | id: true, 41 | role: true, 42 | user: { 43 | select: { 44 | name: true, 45 | email: true 46 | } 47 | }, 48 | } 49 | }, 50 | { page: page } 51 | ); 52 | res.status(200).json(result); 53 | }); 54 | 55 | export default handler; 56 | -------------------------------------------------------------------------------- /pages/api/organizations/[id]/subscription/index.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiResponse } from 'next' 2 | import nextConnect from 'next-connect'; 3 | import { AuthMiddleWare, NextApiRequestWithSession } from 'middlewares/auth'; 4 | import { Product, ProductPrice, Role, Subscription } from '@prisma/client'; 5 | import prisma from '@lib/prisma'; 6 | 7 | export type OrganizationSubscription = { 8 | subscription: (Subscription & { 9 | product: Product; 10 | price: ProductPrice; 11 | }) | undefined | null; 12 | message?: string | undefined; 13 | } 14 | 15 | const handler = nextConnect(); 16 | handler.use(AuthMiddleWare([Role.customer, Role.superadmin])); 17 | 18 | interface NextApiRequestWithId extends NextApiRequestWithSession { 19 | query: Partial<{ [key: string]: string | string[] | undefined }> & { id: string }; 20 | } 21 | 22 | handler.get(async ( 23 | req: NextApiRequestWithId, 24 | res: NextApiResponse, 25 | ) => { 26 | const { id } = req.query; 27 | const userId = req.session?.user.id; 28 | 29 | const subscription = await prisma.subscription.findUnique({ 30 | where: { 31 | organizationId: id, 32 | }, 33 | include: { 34 | product: true, 35 | price: true, 36 | } 37 | }); 38 | 39 | const authorizedUser = await prisma.organizationMember.findUnique({ 40 | where: { 41 | userId_organizationId: { 42 | userId: userId as string, 43 | organizationId: id, 44 | }, 45 | }, 46 | }) 47 | 48 | if (!authorizedUser || authorizedUser.role !== 'org_admin') { 49 | res.status(400).json({ message: 'Bad request', subscription: null }) 50 | } 51 | 52 | res.status(200).json({ subscription }) 53 | }); 54 | 55 | 56 | export default handler; 57 | -------------------------------------------------------------------------------- /pages/api/organizations/index.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiResponse } from 'next' 2 | import prisma from '@lib/prisma' 3 | import { Prisma, Role, Subscription } from '@prisma/client'; 4 | import { createPaginator, PaginatedNextApiRequest, PaginatedResult } from '@lib/pagination'; 5 | import nextConnect from 'next-connect'; 6 | import { AuthMiddleWare, NextApiRequestWithSession } from 'middlewares/auth'; 7 | 8 | export interface OrganizationData { 9 | id: number; 10 | name: string | null; 11 | subscription: Subscription; 12 | _count: { 13 | invitations: number; 14 | members: number; 15 | } 16 | } 17 | 18 | export type OrganizationsData = PaginatedResult; 19 | const handler = nextConnect(); 20 | handler.use(AuthMiddleWare([Role.customer, Role.superadmin])); 21 | 22 | const paginate = createPaginator({ perPage: 20 }); 23 | 24 | handler.get(async ( 25 | req: PaginatedNextApiRequest & NextApiRequestWithSession, 26 | res: NextApiResponse 27 | ) => { 28 | const { query, session } = req; 29 | const { page } = query; 30 | 31 | const { user } = session; 32 | 33 | let whereInput: Prisma.OrganizationWhereInput = { 34 | users: { 35 | some: { 36 | 37 | } 38 | } 39 | }; 40 | 41 | // if (user.role === Role.customer) { 42 | // } 43 | 44 | const result = await paginate( 45 | prisma.organization, 46 | { 47 | where: { 48 | members: { 49 | some: { 50 | userId: user.id as string, 51 | } 52 | } 53 | }, 54 | select: { 55 | id: true, 56 | name: true, 57 | subscription: true, 58 | _count: { 59 | select: { 60 | members: true, 61 | invitations: true, 62 | } 63 | } 64 | }, 65 | }, 66 | { page: page } 67 | ); 68 | 69 | res.status(200).json(result); 70 | }); 71 | 72 | interface CreateOrganizationApiRequest extends NextApiRequestWithSession { 73 | body: { 74 | name: string; 75 | } 76 | } 77 | 78 | handler.post(async ( 79 | req: CreateOrganizationApiRequest, 80 | res: NextApiResponse 81 | ) => { 82 | const userId = req.session?.user.id; 83 | try { 84 | const { name } = req.body; 85 | await prisma.organization.create({ 86 | data: { 87 | name, 88 | users: { 89 | connect: { id: userId as string } 90 | }, 91 | members: { 92 | create: { 93 | user: { 94 | connect: { id: userId as string } 95 | }, 96 | role: 'org_admin', 97 | } 98 | } 99 | } 100 | }); 101 | res.status(200).json({ name: "Organization created" }); 102 | } catch(e: any) { 103 | res.status(500).json({ message: e.message || "Something went wrong" }) 104 | } 105 | }); 106 | 107 | export default handler; 108 | -------------------------------------------------------------------------------- /pages/api/plans/index.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiRequest, NextApiResponse } from 'next' 2 | import prisma from '@lib/prisma' 3 | import { Prisma } from '@prisma/client'; 4 | import { SUBSCRIPTION_PLAN } from '@lib/payments/constants'; 5 | 6 | function sortPlansByUnitAmount(plans: Plan[]): Plan[] { 7 | return plans.sort((a, b) => { 8 | const priceA = a.prices.find((price) => price.active); 9 | const priceB = b.prices.find((price) => price.active); 10 | if (!priceA || !priceB) { 11 | return 0; 12 | } 13 | return priceA.unitAmount - priceB.unitAmount; 14 | }); 15 | } 16 | export interface Price { 17 | active: boolean; 18 | priceId: string; 19 | currency: string; 20 | type: string; 21 | unitAmount: number; 22 | interval: string | null; 23 | interval_count: number | null; 24 | } 25 | 26 | export interface Plan { 27 | active: boolean; 28 | productId: string; 29 | name: string; 30 | defaultPriceId: string | null; 31 | description: string | null; 32 | prices: Price[]; 33 | uniqueIdentifier: string; 34 | } 35 | 36 | export type PlansData = { 37 | data: { 38 | plans: Plan[]; 39 | } 40 | } 41 | 42 | export interface PlansApiRequest extends NextApiRequest { 43 | query: Partial<{ [key: string]: string | string[] | undefined }> & { isOrganization?: boolean }; 44 | } 45 | 46 | export default async function handler( 47 | req: PlansApiRequest, 48 | res: NextApiResponse 49 | ) { 50 | 51 | const { isOrganization } = req.query; 52 | 53 | const where: Prisma.ProductWhereInput = { 54 | active: true, 55 | }; 56 | 57 | if (isOrganization) { 58 | where.uniqueIdentifier = SUBSCRIPTION_PLAN.TEAMS; 59 | } 60 | 61 | const plans: Plan[] = await prisma.product.findMany({ 62 | where, 63 | select: { 64 | productId: true, 65 | name: true, 66 | active: true, 67 | defaultPriceId: true, 68 | description: true, 69 | uniqueIdentifier: true, 70 | prices: { 71 | where: { 72 | active: true, 73 | }, 74 | select: { 75 | priceId: true, 76 | active: true, 77 | currency: true, 78 | interval: true, 79 | interval_count: true, 80 | type: true, 81 | unitAmount: true, 82 | } 83 | } 84 | }, 85 | }) 86 | res.status(200).json({ data: { plans: sortPlansByUnitAmount(plans) } }) 87 | } 88 | -------------------------------------------------------------------------------- /pages/api/products/index.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiResponse } from 'next' 2 | import prisma from '@lib/prisma' 3 | import { Prisma, Product, ProductPrice, Role, User } from '@prisma/client'; 4 | import { createPaginator, PaginatedNextApiRequest, PaginatedResult } from '@lib/pagination'; 5 | 6 | export interface ProductData { 7 | id: string; 8 | name: string; 9 | active: boolean; 10 | uniqueIdentifier: string; 11 | } 12 | 13 | export type ProductsData = PaginatedResult; 14 | 15 | const paginate = createPaginator({ perPage: 20 }); 16 | 17 | export default async function handler( 18 | req: PaginatedNextApiRequest, 19 | res: NextApiResponse 20 | ) { 21 | const { query } = req; 22 | const { page } = query; 23 | const result = await paginate( 24 | prisma.product, 25 | { 26 | select: { 27 | id: true, 28 | name: true, 29 | active: true, 30 | uniqueIdentifier: true 31 | } 32 | }, 33 | { page: page } 34 | ); 35 | res.status(200).json(result); 36 | } 37 | -------------------------------------------------------------------------------- /pages/api/subscription/index.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiResponse } from 'next' 2 | import nextConnect from 'next-connect'; 3 | import { AuthMiddleWare, NextApiRequestWithSession } from 'middlewares/auth'; 4 | import { Product, ProductPrice, Role, Subscription } from '@prisma/client'; 5 | import prisma from '@lib/prisma'; 6 | 7 | export type UserSubscription = { 8 | subscription: (Subscription & { 9 | product: Product; 10 | price: ProductPrice; 11 | }) | undefined | null; 12 | } 13 | 14 | const handler = nextConnect(); 15 | handler.use(AuthMiddleWare([Role.customer, Role.superadmin])); 16 | 17 | handler.get(async ( 18 | req: NextApiRequestWithSession, 19 | res: NextApiResponse, 20 | ) => { 21 | const userId = req.session?.user.id; 22 | 23 | const subscription = await prisma.subscription.findUnique({ 24 | where: { 25 | userId: userId as string, 26 | }, 27 | include: { 28 | product: true, 29 | price: true 30 | } 31 | }); 32 | 33 | res.status(200).json({ subscription }) 34 | }); 35 | 36 | 37 | export default handler; 38 | -------------------------------------------------------------------------------- /pages/api/subscription/manage.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiResponse } from 'next' 2 | import Stripe from 'stripe'; 3 | import { StripeService } from '@lib/payments/stripe/service'; 4 | import nextConnect from 'next-connect'; 5 | import { AuthMiddleWare, NextApiRequestWithSession } from 'middlewares/auth'; 6 | import { Role } from '@prisma/client'; 7 | import prisma from '@lib/prisma'; 8 | 9 | export type ManageBillingData = { 10 | url?: string; 11 | message?: string; 12 | } 13 | 14 | const handler = nextConnect(); 15 | handler.use(AuthMiddleWare([Role.customer])); 16 | 17 | handler.get(async ( 18 | req: NextApiRequestWithSession, 19 | res: NextApiResponse 20 | ) => { 21 | const user = req.session?.user; 22 | 23 | const currentUser = await prisma.user.findUnique({ 24 | where: { id: user?.id as string }, 25 | }) 26 | 27 | if (currentUser?.stripeCustomerId) { 28 | const session = await StripeService.getInstance(process.env.STRIPE_SECRET_KEY as string).billingPortal.sessions.create({ 29 | customer: currentUser?.stripeCustomerId, 30 | return_url: process.env.NEXTAUTH_URL + '/account/billing', 31 | }); 32 | return res.status(200).json({ url: session.url }); 33 | } 34 | 35 | return res.status(400).json({ message: 'No billing associated this account' }) 36 | }); 37 | 38 | export default handler; -------------------------------------------------------------------------------- /pages/api/users/currentUser.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiResponse } from 'next' 2 | import nextConnect from 'next-connect'; 3 | import { AuthMiddleWare, NextApiRequestWithSession } from 'middlewares/auth'; 4 | import { Role } from '@prisma/client'; 5 | import prisma from '@lib/prisma'; 6 | import { ProfileFormValues } from '@components/forms/ProfileForm/ProfileForm'; 7 | 8 | export type UserData = { 9 | name?: string | null | undefined; 10 | } 11 | 12 | const handler = nextConnect(); 13 | handler.use(AuthMiddleWare([Role.customer, Role.superadmin])); 14 | 15 | handler.get(async ( 16 | req: NextApiRequestWithSession, 17 | res: NextApiResponse, 18 | ) => { 19 | const userId = req.session?.user.id; 20 | 21 | const user = await prisma.user.findUnique({ 22 | where: { 23 | id: userId as string 24 | }, 25 | select: { 26 | name: true, 27 | } 28 | }); 29 | 30 | res.status(200).json({ ...user }) 31 | }); 32 | 33 | interface NextUpdateUserApiRequest extends NextApiRequestWithSession { 34 | body: ProfileFormValues 35 | } 36 | 37 | handler.patch(async ( 38 | req: NextUpdateUserApiRequest, 39 | res: NextApiResponse, 40 | ) => { 41 | const userId = req.session?.user.id; 42 | 43 | const { name } = req.body; 44 | const updated = await prisma.user.update({ 45 | where: { id: userId as string }, 46 | data: { name } 47 | }); 48 | 49 | res.status(200).json({ name: updated.name }); 50 | }) 51 | 52 | export default handler; 53 | -------------------------------------------------------------------------------- /pages/api/users/index.ts: -------------------------------------------------------------------------------- 1 | import type { NextApiResponse } from 'next' 2 | import prisma from '@lib/prisma' 3 | import { Prisma, Role, User } from '@prisma/client'; 4 | import { createPaginator, PaginatedNextApiRequest, PaginatedResult } from '@lib/pagination'; 5 | import nextConnect from 'next-connect'; 6 | import { AuthMiddleWare, NextApiRequestWithSession } from 'middlewares/auth'; 7 | 8 | export interface UserData { 9 | id: number; 10 | name: string | null; 11 | email: string | null; 12 | emailVerified: Date | null; 13 | image: string | null; 14 | role: Role; 15 | } 16 | 17 | export type UsersData = PaginatedResult; 18 | const handler = nextConnect(); 19 | handler.use(AuthMiddleWare([Role.superadmin])); 20 | 21 | const paginate = createPaginator({ perPage: 20 }); 22 | 23 | handler.get(async ( 24 | req: PaginatedNextApiRequest & NextApiRequestWithSession, 25 | res: NextApiResponse 26 | ) => { 27 | const { query } = req; 28 | const { page } = query; 29 | const result = await paginate( 30 | prisma.user, 31 | { 32 | select: { 33 | id: true, 34 | name: true, 35 | email: true, 36 | emailVerified: true, 37 | image: true, 38 | role: true, 39 | } 40 | }, 41 | { page: page } 42 | ); 43 | res.status(200).json(result); 44 | }); 45 | 46 | export default handler; 47 | -------------------------------------------------------------------------------- /pages/api/webhook/stripe.ts: -------------------------------------------------------------------------------- 1 | import StripeWebhook from '@lib/payments/stripe/webhook'; 2 | import prisma from '@lib/prisma'; 3 | import { buffer } from 'micro'; 4 | import { NextApiRequest, NextApiResponse } from 'next'; 5 | 6 | export const config = { api: { bodyParser: false } }; 7 | 8 | export default async function handleStripeWebhook( 9 | req: NextApiRequest, 10 | res: NextApiResponse 11 | ): Promise { 12 | const buf = await buffer(req); 13 | const sig = req.headers['stripe-signature'] as string; 14 | const stripeInstace = StripeWebhook.getInstance(process.env.STRIPE_SECRET_KEY as string); 15 | try { 16 | const event = stripeInstace.constructEvent( 17 | buf.toString(), 18 | sig, 19 | process.env.STRIPE_WEBHOOK_SECRET as string 20 | ); 21 | 22 | await prisma.$transaction(async (prismaTrasaction) => { 23 | await stripeInstace.handleEvent(prismaTrasaction, event); 24 | }, { 25 | maxWait: 10000, // 5 seconds max wait to connect to prisma 26 | timeout: 30000, // 20 seconds 27 | }); 28 | res.status(200).send('OK'); 29 | } catch(e: any) { 30 | res.status(400).send(`Webhook Error: ${e?.message || e}`); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /pages/auth/forgot/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { NextPage } from 'next'; 3 | import { AuthLayout } from '@components/index'; 4 | import { ForgotPasswordForm } from '@components/forms'; 5 | 6 | import { useForgotPassword } from '@hooks/query/currentUser'; 7 | 8 | 9 | const ForgotPassword: NextPage = () => { 10 | const { forgot, isLoading } = useForgotPassword(); 11 | return ( 12 | 13 | 14 | 15 | ) 16 | } 17 | 18 | export default ForgotPassword; 19 | -------------------------------------------------------------------------------- /pages/auth/login/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { GetServerSideProps, InferGetServerSidePropsType } from 'next'; 3 | import { AuthLayout } from '@components/index'; 4 | import { LoginForm } from '@components/forms'; 5 | import { ClientSafeProvider, getProviders, LiteralUnion } from 'next-auth/react'; 6 | import { BuiltInProviderType } from 'next-auth/providers'; 7 | import { useSignIn } from '@hooks/query/currentUser'; 8 | 9 | type Data = { 10 | providers: Record, ClientSafeProvider> | null, 11 | } 12 | 13 | const Login = (props: InferGetServerSidePropsType) => { 14 | const { providers } = props; 15 | const { login, isLoading } = useSignIn(); 16 | return ( 17 | 18 | 19 | 20 | ) 21 | } 22 | 23 | export default Login; 24 | 25 | 26 | export const getServerSideProps: GetServerSideProps = async (context) => { 27 | const providers = await getProviders(); 28 | return { 29 | props: { providers }, 30 | } 31 | } -------------------------------------------------------------------------------- /pages/auth/register/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { GetServerSideProps, InferGetServerSidePropsType } from 'next'; 3 | import { AuthLayout } from '@components/index'; 4 | import { RegisterForm } from '@components/forms'; 5 | import { ClientSafeProvider, getCsrfToken, getProviders, LiteralUnion } from 'next-auth/react'; 6 | import { BuiltInProviderType } from 'next-auth/providers'; 7 | import { useSignup } from '@hooks/query/currentUser'; 8 | 9 | type Data = { 10 | providers: Record, ClientSafeProvider> | null, 11 | } 12 | 13 | const Register = (props: InferGetServerSidePropsType) => { 14 | const { providers } = props; 15 | const { signup, isLoading } = useSignup(); 16 | return ( 17 | 18 | 19 | 20 | ) 21 | } 22 | 23 | export default Register; 24 | 25 | 26 | export const getServerSideProps: GetServerSideProps = async (context) => { 27 | const providers = await getProviders(); 28 | return { 29 | props: { providers }, 30 | } 31 | } -------------------------------------------------------------------------------- /pages/auth/reset-password/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { GetServerSideProps, InferGetServerSidePropsType } from 'next'; 3 | import { AuthLayout } from '@components/index'; 4 | import { ResetPasswordForm } from '@components/forms'; 5 | import { useResetPassword } from '@hooks/query/currentUser'; 6 | import prisma from '@lib/prisma'; 7 | 8 | type Data = { 9 | token: string; 10 | } 11 | 12 | const ResetPasswordPage = (props: InferGetServerSidePropsType) => { 13 | const { token } = props; 14 | const { updatePassword, isUpdateLoading } = useResetPassword(); 15 | return ( 16 | 17 | updatePassword(e, token)} loading={isUpdateLoading} /> 18 | 19 | ) 20 | } 21 | 22 | export default ResetPasswordPage; 23 | 24 | 25 | export const getServerSideProps: GetServerSideProps = async (context) => { 26 | const { query } = context; 27 | if (query && query.token) { 28 | const { token } = query; 29 | const _token = Array.isArray(token) ? token[0] : token; 30 | const tokenFound = await prisma.passwordResetToken.findUnique({ where: { token: _token }}); 31 | 32 | if (!tokenFound) { 33 | return { 34 | redirect: { 35 | permanent: false, 36 | destination: "/auth/login", 37 | }, 38 | props:{}, 39 | }; 40 | } 41 | return { 42 | props: { token: _token }, 43 | } 44 | } 45 | 46 | return { 47 | redirect: { 48 | permanent: false, 49 | destination: "/auth/login", 50 | }, 51 | props:{}, 52 | }; 53 | } -------------------------------------------------------------------------------- /pages/dashboard/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { MainLayout } from '@components/index'; 4 | import { NextPageWithProps } from '@pages/_app'; 5 | 6 | const Dashboard: NextPageWithProps = () => { 7 | return ( 8 | 9 | 10 | 11 | ) 12 | } 13 | 14 | Dashboard.requireAuth = true; 15 | export default Dashboard; 16 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import PricingComponent from '@components/PricingComponent' 2 | import type { NextPage } from 'next' 3 | import Head from 'next/head' 4 | import Image from 'next/image' 5 | 6 | const Home: NextPage = () => { 7 | return ( 8 |

    9 | 10 | Next.js Serverless Boilerplate 11 | 12 | 13 |

    Next.js Serverless Boilerplate

    14 | 15 |
    16 | ) 17 | } 18 | 19 | export default Home 20 | -------------------------------------------------------------------------------- /pages/invitation/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react' 2 | import { useRouter } from 'next/router'; 3 | import { Role } from '@prisma/client'; 4 | import { NextPageWithProps } from '@pages/_app'; 5 | import jsonwebtoken from 'jsonwebtoken'; 6 | import { useAcceptInvitation } from '@hooks/query/organizations'; 7 | import { Button } from '@components/elements'; 8 | import Flex from '@components/Flex'; 9 | 10 | const OrganizationInvitation: NextPageWithProps = () => { 11 | const router = useRouter(); 12 | const { query } = router; 13 | 14 | const token: string = query.token as string; 15 | 16 | const { accept } = useAcceptInvitation({ token }); 17 | 18 | useEffect(() => { 19 | if (!router.isReady) return; 20 | const { query } = router; 21 | if (query && !query.token) { 22 | router.replace('/dashboard'); 23 | return; 24 | } 25 | }, [router.isReady]); 26 | 27 | return ( 28 | 29 | 30 | 31 | ) 32 | } 33 | 34 | OrganizationInvitation.requireAuth = true; 35 | OrganizationInvitation.roles = [Role.customer]; 36 | export default OrganizationInvitation; 37 | -------------------------------------------------------------------------------- /pages/organizations/[organizationId]/billing/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { NextPage } from 'next'; 3 | 4 | import { CurrentPlanCard, MainLayout } from '@components/index'; 5 | import OrganizationLayout from '@components/layouts/OrganizationLayout'; 6 | import { useRouter } from 'next/router'; 7 | 8 | const Billing: NextPage = () => { 9 | const router = useRouter(); 10 | 11 | const { query } = router; 12 | return ( 13 | 14 | 15 | 16 | 17 | 18 | ) 19 | } 20 | 21 | export default Billing; 22 | -------------------------------------------------------------------------------- /pages/organizations/[organizationId]/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useRouter } from 'next/router'; 3 | 4 | 5 | import { Flex, MainLayout } from '@components/index'; 6 | import { NextPageWithProps } from '@pages/_app'; 7 | import { SUBSCRIPTION_PLAN } from '@lib/payments/constants'; 8 | import OrganizationLayout from '@components/layouts/OrganizationLayout'; 9 | 10 | 11 | const Organization: NextPageWithProps = () => { 12 | 13 | return ( 14 | 15 | 16 | 17 | 18 | Build something here 19 | 20 | 21 | 22 | 23 | ) 24 | } 25 | 26 | Organization.requireAuth = true; 27 | Organization.requireSubscription = true; 28 | Organization.plans = [SUBSCRIPTION_PLAN.TEAMS]; 29 | 30 | export default Organization; 31 | -------------------------------------------------------------------------------- /pages/organizations/[organizationId]/members/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | import { NextPage } from 'next'; 3 | 4 | import { DialogComponent, Flex, MainLayout, Table } from '@components/index'; 5 | import { createColumnHelper } from '@tanstack/react-table'; 6 | import OrganizationLayout from '@components/layouts/OrganizationLayout'; 7 | import { 8 | useCancelMemberInvitation, 9 | useInviteMemberToOrganization, 10 | useOrganizationInvitationsMembers, 11 | useOrganizationMembers, 12 | useRemoveOrganizationMember, 13 | } from '@hooks/query/organizations'; 14 | import { useRouter } from 'next/router'; 15 | import { OrganizationMemberData } from '@pages/api/organizations/[id]/members'; 16 | import { OrganizationInvitationMemberData } from '@pages/api/organizations/[id]/invitations'; 17 | 18 | import { OrganizationMemberInviteForm } from '@components/forms'; 19 | import { Button } from '@components/elements'; 20 | import { OrganizationRole } from '@prisma/client'; 21 | 22 | const Users: NextPage = () => { 23 | const router = useRouter(); 24 | const { query } = router; 25 | const { organizationId } = query; 26 | 27 | const { data } = useOrganizationMembers({ organizationId: organizationId as string }); 28 | const { data: inivitaionMembers } = useOrganizationInvitationsMembers({ organizationId: organizationId as string }); 29 | 30 | const { invite, isLoading: inviteLoading } = useInviteMemberToOrganization({ organizationId: organizationId as string }); 31 | 32 | const { cancelInvitation, isCancelLoading } = useCancelMemberInvitation({ organizationId: organizationId as string }); 33 | 34 | const memberColumnHelper = createColumnHelper(); 35 | const membersColumns = [ 36 | memberColumnHelper.accessor('user.email', { 37 | header: () => 'Email', 38 | cell: info => info.getValue(), 39 | }), 40 | memberColumnHelper.accessor('user.name', { 41 | header: () => 'Name', 42 | cell: info => info.getValue(), 43 | }), 44 | memberColumnHelper.accessor('role', { 45 | header: () => 'Role', 46 | cell: info => info.getValue().toString(), 47 | }), 48 | memberColumnHelper.accessor('actions', { 49 | header: () => 'Actions', 50 | cell: (data) => { 51 | const { remove, isLoading } = useRemoveOrganizationMember({ 52 | organizationId: organizationId as string, 53 | memberId: data.row.original.id, 54 | }) 55 | if ( data.row.original.role !== OrganizationRole.org_admin ) { 56 | return ( 57 | 67 | ); 68 | } 69 | return <>; 70 | }, 71 | }), 72 | ]; 73 | 74 | const columnHelper = createColumnHelper(); 75 | const invitationsColumns = [ 76 | columnHelper.accessor('email', { 77 | header: () => 'Email', 78 | cell: info => info.getValue(), 79 | }), 80 | columnHelper.accessor('role', { 81 | header: () => 'Name', 82 | cell: info => info.getValue(), 83 | }), 84 | columnHelper.accessor('status', { 85 | header: () => 'Status', 86 | cell: info => info.getValue(), 87 | }), 88 | columnHelper.accessor('actions', { 89 | header: () => 'Actions', 90 | cell: (data) => ( 91 | 101 | ), 102 | }), 103 | ]; 104 | 105 | const [inviteForm, setInviteForm] = useState(false); 106 | return ( 107 | 108 | 109 | 110 | 116 | 117 | 118 |
    119 | 124 | invite(values, formInstance, setInviteForm)} loading={inviteLoading} /> 125 | 126 |
    127 |
    133 | 134 | 135 | 136 | 137 | ) 138 | } 139 | 140 | export default Users; 141 | -------------------------------------------------------------------------------- /pages/organizations/index.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react' 2 | 3 | import { DialogComponent, Flex, MainLayout, Table } from '@components/index'; 4 | import { createColumnHelper } from '@tanstack/react-table'; 5 | import { NextPageWithProps } from '@pages/_app'; 6 | 7 | import { SUBSCRIPTION_PLAN } from '@lib/payments/constants'; 8 | import { OrganizationForm } from '@components/forms'; 9 | import { useCreateOrganization, useOrganizations } from '@hooks/query/organizations'; 10 | import { OrganizationData } from '@pages/api/organizations'; 11 | import Link from 'next/link'; 12 | import { useSession } from 'next-auth/react'; 13 | 14 | const Organizations: NextPageWithProps = () => { 15 | const { data } = useOrganizations(); 16 | const { data: sessionData } = useSession(); 17 | 18 | const [open, setOpen] = useState(false); 19 | const { createOrganization, isLoading: createLoading } = useCreateOrganization(); 20 | 21 | const columnHelper = createColumnHelper(); 22 | const columns = [ 23 | columnHelper.accessor('name', { 24 | cell: (data) => { 25 | return ( 26 | {data.getValue()} 27 | ) 28 | }, 29 | }), 30 | columnHelper.accessor('_count.invitations', { 31 | header: () => 'Invitations', 32 | cell: info => info.getValue(), 33 | }), 34 | columnHelper.accessor('_count.members', { 35 | header: () => 'Members', 36 | cell: info => info.getValue(), 37 | }), 38 | columnHelper.accessor('subscription.status', { 39 | header: () => 'Subscription Status', 40 | cell: info => info.getValue() ? info.getValue(): 'No Plan', 41 | }), 42 | ]; 43 | 44 | return ( 45 | 46 | 47 | { 48 | sessionData?.user.isOrganizationAdmin && 49 |
    50 | 51 | createOrganization(e, setOpen)} loading={createLoading} /> 52 | 53 |
    54 | } 55 |
    61 | 62 | 63 | ) 64 | } 65 | 66 | Organizations.requireAuth = true; 67 | Organizations.requireSubscription = true; 68 | Organizations.plans = [SUBSCRIPTION_PLAN.TEAMS]; 69 | 70 | export default Organizations; 71 | -------------------------------------------------------------------------------- /pages/pricing/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | import { CurrentPlanCard, MainLayout } from '@components/index'; 4 | import { NextPageWithProps } from '@pages/_app'; 5 | 6 | const PrcingPage: NextPageWithProps = () => { 7 | 8 | return ( 9 | 10 | 11 | 12 | ) 13 | } 14 | 15 | PrcingPage.requireAuth = true; 16 | export default PrcingPage; -------------------------------------------------------------------------------- /pages/products/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | 3 | import { MainLayout, Table } from '@components/index'; 4 | import { createColumnHelper } from '@tanstack/react-table'; 5 | import { useProducts } from '@hooks/query/plans'; 6 | import { ProductData } from '@pages/api/products'; 7 | import { NextPageWithProps } from '@pages/_app'; 8 | import { Role } from '@prisma/client'; 9 | 10 | const Users: NextPageWithProps = () => { 11 | const { data } = useProducts(); 12 | 13 | const columnHelper = createColumnHelper(); 14 | const columns = [ 15 | columnHelper.accessor('id', { 16 | cell: info => info.getValue(), 17 | }), 18 | columnHelper.accessor('name', { 19 | cell: info => info.getValue(), 20 | }), 21 | columnHelper.accessor('uniqueIdentifier', { 22 | cell: info => info.getValue(), 23 | }), 24 | columnHelper.accessor('active', { 25 | cell: info => info.getValue() ? 'ACTIVE': 'INACTIVE', 26 | }), 27 | ]; 28 | 29 | return ( 30 | 31 |
    37 | 38 | ) 39 | } 40 | 41 | Users.requireAuth = true; 42 | Users.roles = [Role.superadmin]; 43 | export default Users; 44 | -------------------------------------------------------------------------------- /pages/users/index.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { NextPage } from 'next'; 3 | 4 | import { MainLayout, Table } from '@components/index'; 5 | import { createColumnHelper } from '@tanstack/react-table'; 6 | import { useUsers } from '@hooks/query/users'; 7 | import { UserData } from '@pages/api/users'; 8 | 9 | const Users: NextPage = () => { 10 | const { data } = useUsers(); 11 | 12 | const columnHelper = createColumnHelper(); 13 | const columns = [ 14 | columnHelper.accessor('email', { 15 | cell: info => info.getValue(), 16 | }), 17 | columnHelper.accessor('name', { 18 | cell: info => info.getValue(), 19 | }), 20 | columnHelper.accessor('role', { 21 | cell: info => info.getValue().toString(), 22 | }), 23 | columnHelper.accessor('emailVerified', { 24 | header: info => 'Email Verified', 25 | cell: info => info.getValue(), 26 | }), 27 | ]; 28 | 29 | return ( 30 | 31 |
    37 | 38 | ) 39 | } 40 | 41 | export default Users; 42 | -------------------------------------------------------------------------------- /plop-templates/Component/Component.js.hbs: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface {{pascalCase name}}Props { 4 | 5 | } 6 | 7 | const {{pascalCase name}}: React.FC<{{pascalCase name}}Props> = (props: {{pascalCase name}}Props) => { 8 | return ( 9 |
    10 | {{pascalCase name}} 11 |
    12 | ); 13 | }; 14 | 15 | {{pascalCase name}}.defaultProps = { 16 | 17 | }; 18 | 19 | {{pascalCase name}}.propTypes = { 20 | 21 | }; 22 | 23 | export default {{pascalCase name}}; 24 | -------------------------------------------------------------------------------- /plop-templates/Component/Component.module.css.hbs: -------------------------------------------------------------------------------- 1 | .root { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /plop-templates/Component/Component.test.js.hbs: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {{pascalCase name}} from './{{pascalCase name}}'; 3 | 4 | describe('{{pascalCase name}}', () => { 5 | it('renders without error', () => { 6 | 7 | }); 8 | }); -------------------------------------------------------------------------------- /plop-templates/Component/index.js.hbs: -------------------------------------------------------------------------------- 1 | import {{pascalCase name}} from './{{pascalCase name}}'; 2 | 3 | export default {{pascalCase name}}; 4 | -------------------------------------------------------------------------------- /plop-templates/Form/Form.js.hbs: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useForm, FormProvider, Controller } from 'react-hook-form'; 3 | 4 | import { Button } from '@components/elements'; 5 | import useYupValidationResolver from '@hooks/useYupValidationResolver'; 6 | import * as yup from 'yup'; 7 | 8 | export type {{pascalCase name}}Values = { 9 | 10 | }; 11 | 12 | interface {{pascalCase name}}Props { 13 | onSubmit: (updatedValues: {{pascalCase name}}Values) => void; 14 | loading?: boolean; 15 | } 16 | 17 | const validationSchema: yup.ObjectSchema<{{pascalCase name}}Values> = yup.object({ 18 | 19 | }); 20 | 21 | const {{pascalCase name}}: React.FC<{{pascalCase name}}Props> = (props: {{pascalCase name}}Props) => { 22 | const { onSubmit, loading } = props; 23 | const resolver = useYupValidationResolver(validationSchema); 24 | 25 | const methods = useForm<{{pascalCase name}}Values>({ 26 | defaultValues: {}, 27 | resolver 28 | }); 29 | 30 | return ( 31 | 32 |
    33 |
    34 |
    35 | {{pascalCase name}} 36 |
    37 |
    38 | 39 | 40 |
    41 | ); 42 | }; 43 | 44 | {{pascalCase name}}.defaultProps = { 45 | 46 | }; 47 | 48 | {{pascalCase name}}.propTypes = { 49 | 50 | }; 51 | 52 | export default {{pascalCase name}}; 53 | -------------------------------------------------------------------------------- /plop-templates/Form/Form.module.css.hbs: -------------------------------------------------------------------------------- 1 | .root { 2 | 3 | } 4 | -------------------------------------------------------------------------------- /plop-templates/Form/Form.test.js.hbs: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import {{pascalCase name}} from './{{pascalCase name}}'; 3 | 4 | describe('{{pascalCase name}}', () => { 5 | it('renders without error', () => { 6 | 7 | }); 8 | }); -------------------------------------------------------------------------------- /plop-templates/Form/index.js.hbs: -------------------------------------------------------------------------------- 1 | import {{pascalCase name}} from './{{pascalCase name}}'; 2 | 3 | export default {{pascalCase name}}; 4 | -------------------------------------------------------------------------------- /plop-templates/hook.js.hbs: -------------------------------------------------------------------------------- 1 | 2 | const {{camelCase name}} = () => { 3 | 4 | }; 5 | 6 | export default {{camelCase name}}; -------------------------------------------------------------------------------- /plop-templates/injectable-index.js.hbs: -------------------------------------------------------------------------------- 1 | /* PLOP_INJECT_IMPORT */ 2 | 3 | export { 4 | /* PLOP_INJECT_EXPORT */ 5 | } -------------------------------------------------------------------------------- /plopfile.js: -------------------------------------------------------------------------------- 1 | module.exports = (plop) => { 2 | // create your generators here 3 | plop.setGenerator('component', { 4 | description: 'Nextjs Boilerplate', 5 | prompts: [ 6 | { 7 | type: 'input', 8 | name: 'name', 9 | message: 'Enter component name?', 10 | } 11 | ], // array of inquirer prompts 12 | actions: [ 13 | { 14 | type: 'add', 15 | path: 'components/{{pascalCase name}}/{{pascalCase name}}.tsx', 16 | templateFile: 17 | 'plop-templates/Component/Component.js.hbs', 18 | }, 19 | { 20 | type: 'add', 21 | path: 22 | 'components/{{pascalCase name}}/{{pascalCase name}}.module.css', 23 | templateFile: 24 | 'plop-templates/Component/Component.module.css.hbs', 25 | }, 26 | { 27 | type: 'add', 28 | path: 'components/{{pascalCase name}}/index.ts', 29 | templateFile: 'plop-templates/Component/index.js.hbs', 30 | }, 31 | { 32 | // Adds an index.js file if it does not already exist 33 | type: 'add', 34 | path: 'components/index.ts', 35 | templateFile: 'plop-templates/injectable-index.js.hbs', 36 | 37 | // If index.js already exists in this location, skip this action 38 | skipIfExists: true, 39 | }, 40 | { 41 | // Action type 'append' injects a template into an existing file 42 | type: 'append', 43 | path: 'components/index.ts', 44 | // Pattern tells plop where in the file to inject the template 45 | pattern: '/* PLOP_INJECT_IMPORT */', 46 | template: 'import {{pascalCase name}} from \'./{{pascalCase name}}\';', 47 | }, 48 | { 49 | type: 'append', 50 | path: 'components/index.ts', 51 | pattern: '/* PLOP_INJECT_EXPORT */', 52 | template: '\t{{pascalCase name}},', 53 | }, 54 | ] // array of actions 55 | }); 56 | 57 | plop.setGenerator('element', { 58 | description: 'Nextjs Boilerplate', 59 | prompts: [ 60 | { 61 | type: 'input', 62 | name: 'name', 63 | message: 'Enter elements name?', 64 | } 65 | ], // array of inquirer prompts 66 | actions: [ 67 | { 68 | type: 'add', 69 | path: 'components/elements/{{pascalCase name}}/{{pascalCase name}}.tsx', 70 | templateFile: 71 | 'plop-templates/Component/Component.js.hbs', 72 | }, 73 | { 74 | type: 'add', 75 | path: 76 | 'components/elements/{{pascalCase name}}/{{pascalCase name}}.module.css', 77 | templateFile: 78 | 'plop-templates/Component/Component.module.css.hbs', 79 | }, 80 | { 81 | type: 'add', 82 | path: 'components/elements/{{pascalCase name}}/index.ts', 83 | templateFile: 'plop-templates/Component/index.js.hbs', 84 | }, 85 | { 86 | // Adds an index.js file if it does not already exist 87 | type: 'add', 88 | path: 'components/elements/index.ts', 89 | templateFile: 'plop-templates/injectable-index.js.hbs', 90 | 91 | // If index.js already exists in this location, skip this action 92 | skipIfExists: true, 93 | }, 94 | { 95 | // Action type 'append' injects a template into an existing file 96 | type: 'append', 97 | path: 'components/elements/index.ts', 98 | // Pattern tells plop where in the file to inject the template 99 | pattern: '/* PLOP_INJECT_IMPORT */', 100 | template: 'import {{pascalCase name}} from \'./{{pascalCase name}}\';', 101 | }, 102 | { 103 | type: 'append', 104 | path: 'components/elements/index.ts', 105 | pattern: '/* PLOP_INJECT_EXPORT */', 106 | template: '\t{{pascalCase name}},', 107 | }, 108 | ] // array of actions 109 | }); 110 | 111 | // create your generators here 112 | plop.setGenerator('form', { 113 | description: 'Nextjs Boilerplate', 114 | prompts: [ 115 | { 116 | type: 'input', 117 | name: 'name', 118 | message: 'Enter form name?', 119 | } 120 | ], // array of inquirer prompts 121 | actions: [ 122 | { 123 | type: 'add', 124 | path: 'components/forms/{{pascalCase name}}/{{pascalCase name}}.tsx', 125 | templateFile: 126 | 'plop-templates/Form/Form.js.hbs', 127 | }, 128 | { 129 | type: 'add', 130 | path: 131 | 'components/forms/{{pascalCase name}}/{{pascalCase name}}.module.css', 132 | templateFile: 133 | 'plop-templates/Form/Form.module.css.hbs', 134 | }, 135 | { 136 | type: 'add', 137 | path: 'components/forms/{{pascalCase name}}/index.ts', 138 | templateFile: 'plop-templates/Form/index.js.hbs', 139 | }, 140 | { 141 | // Adds an index.js file if it does not already exist 142 | type: 'add', 143 | path: 'components/forms/index.ts', 144 | templateFile: 'plop-templates/injectable-index.js.hbs', 145 | 146 | // If index.js already exists in this location, skip this action 147 | skipIfExists: true, 148 | }, 149 | { 150 | // Action type 'append' injects a template into an existing file 151 | type: 'append', 152 | path: 'components/forms/index.ts', 153 | // Pattern tells plop where in the file to inject the template 154 | pattern: '/* PLOP_INJECT_IMPORT */', 155 | template: 'import {{pascalCase name}} from \'./{{pascalCase name}}\';', 156 | }, 157 | { 158 | type: 'append', 159 | path: 'components/forms/index.ts', 160 | pattern: '/* PLOP_INJECT_EXPORT */', 161 | template: '\t{{pascalCase name}},', 162 | }, 163 | ] // array of actions 164 | }); 165 | }; -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | binaryTargets = ["native", "rhel-openssl-1.0.x"] 7 | } 8 | 9 | datasource db { 10 | provider = "postgresql" 11 | url = env("DATABASE_URL") 12 | } 13 | 14 | enum Role { 15 | superadmin 16 | customer 17 | } 18 | 19 | enum OrganizationStatus { 20 | active 21 | inactive 22 | } 23 | 24 | enum UserStatus { 25 | active 26 | inactive 27 | } 28 | model Account { 29 | id String @id @default(cuid()) 30 | userId String 31 | type String 32 | provider String 33 | providerAccountId String 34 | refresh_token String? @db.Text 35 | access_token String? @db.Text 36 | expires_at Int? 37 | token_type String? 38 | scope String? 39 | id_token String? @db.Text 40 | session_state String? 41 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 42 | 43 | @@unique([provider, providerAccountId]) 44 | } 45 | 46 | model Session { 47 | id String @id @default(cuid()) 48 | sessionToken String @unique 49 | expires DateTime 50 | userId String 51 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 52 | } 53 | 54 | model User { 55 | id String @id @default(cuid()) 56 | name String? 57 | password String? 58 | email String? @unique 59 | emailVerified DateTime? 60 | image String? 61 | accounts Account[] 62 | sessions Session[] 63 | role Role @default(customer) 64 | subscription Subscription? 65 | 66 | organizations Organization[] 67 | invitations OrganizationMemberInvitation[] 68 | 69 | memberInOrganizations OrganizationMember[] 70 | 71 | resetToken PasswordResetToken[] 72 | 73 | stripeCustomerId String? 74 | 75 | status UserStatus @default(active) 76 | } 77 | 78 | model PasswordResetToken { 79 | id String @id @default(cuid()) 80 | expires DateTime 81 | 82 | token String @unique 83 | 84 | userId String 85 | user User @relation(fields: [userId], references: [id]) 86 | } 87 | 88 | model VerificationToken { 89 | identifier String 90 | token String @unique 91 | expires DateTime 92 | 93 | @@unique([identifier, token]) 94 | } 95 | 96 | enum OrganizationRole { 97 | org_admin 98 | org_user 99 | } 100 | 101 | model Organization { 102 | id String @id @default(cuid()) 103 | name String 104 | createdAt DateTime @default(now()) 105 | 106 | status OrganizationStatus @default(active) 107 | 108 | users User[] 109 | members OrganizationMember[] 110 | 111 | invitations OrganizationMemberInvitation[] 112 | 113 | subscription Subscription? 114 | } 115 | 116 | model OrganizationMember { 117 | id String @id @default(cuid()) 118 | user User? @relation(fields: [userId], references: [id]) 119 | userId String? 120 | 121 | role OrganizationRole @default(org_user) 122 | 123 | organization Organization @relation(fields: [organizationId], references: [id]) 124 | organizationId String 125 | createdAt DateTime @default(now()) 126 | updatedAt DateTime? @updatedAt 127 | 128 | @@unique([userId, organizationId]) 129 | } 130 | 131 | enum OrganizationInvitationStatus { 132 | pending 133 | accepted 134 | declined 135 | } 136 | 137 | model OrganizationMemberInvitation { 138 | id String @id @default(cuid()) 139 | email String 140 | userId String? 141 | 142 | organization Organization @relation(fields: [organizationId], references: [id]) 143 | organizationId String 144 | 145 | token String @unique 146 | role OrganizationRole @default(org_user) 147 | 148 | senderId String? 149 | sender User? @relation(fields: [senderId], references: [id]) 150 | 151 | status OrganizationInvitationStatus @default(pending) 152 | createdAt DateTime @default(now()) 153 | updatedAt DateTime @updatedAt 154 | 155 | expiresAt DateTime 156 | 157 | @@unique([email, organizationId]) 158 | } 159 | 160 | enum ProductType { 161 | individual 162 | organization 163 | } 164 | 165 | model Product { 166 | id String @id @default(cuid()) 167 | 168 | productId String @unique 169 | 170 | uniqueIdentifier String 171 | 172 | name String 173 | 174 | type ProductType @default(individual) 175 | 176 | active Boolean @default(false) 177 | 178 | defaultPriceId String? @unique 179 | defaultPrice ProductPrice? @relation(name: "defaultPrice", fields: [defaultPriceId], references: [priceId]) 180 | 181 | description String? 182 | 183 | createdAt DateTime @default(now()) 184 | 185 | updateAt DateTime @default(now()) 186 | 187 | prices ProductPrice[] @relation(name: "prices") 188 | subscriptions Subscription[] 189 | 190 | @@index([productId]) 191 | @@index([uniqueIdentifier]) 192 | } 193 | 194 | model ProductPrice { 195 | id String @id @default(cuid()) 196 | 197 | priceId String @unique 198 | 199 | productId String 200 | product Product @relation(name: "prices", fields: [productId], references: [productId], onDelete: Cascade) 201 | 202 | active Boolean @default(false) 203 | description String 204 | 205 | currency String @default("USD") 206 | 207 | type String 208 | 209 | interval String? 210 | 211 | interval_count Int? 212 | 213 | unitAmount Float 214 | 215 | createdAt DateTime @default(now()) 216 | 217 | updateAt DateTime @default(now()) 218 | 219 | defaultPrice Product? @relation(name: "defaultPrice") 220 | 221 | subscriptions Subscription[] 222 | 223 | @@index([priceId, productId]) 224 | } 225 | 226 | model Subscription { 227 | id String @id @default(cuid()) 228 | 229 | subscriptionId String @unique 230 | 231 | user User? @relation(fields: [userId], references: [id]) 232 | 233 | userId String? @unique 234 | 235 | organization Organization? @relation(fields: [organizationId], references: [id]) 236 | 237 | organizationId String? @unique 238 | 239 | product Product @relation(fields: [productId], references: [productId]) 240 | 241 | productId String 242 | 243 | price ProductPrice @relation(fields: [priceId], references: [priceId]) 244 | 245 | priceId String 246 | 247 | status SubscriptionStatus @default(active) 248 | 249 | paymentCustomerId String? 250 | 251 | startDate DateTime @default(now()) 252 | 253 | endDate DateTime? 254 | 255 | createdAt DateTime @default(now()) 256 | 257 | updateAt DateTime @default(now()) 258 | 259 | @@index([subscriptionId]) 260 | @@unique([subscriptionId, userId]) 261 | @@unique([subscriptionId, userId, organizationId]) 262 | } 263 | 264 | enum SubscriptionStatus { 265 | active 266 | canceled 267 | paused 268 | incomplete 269 | incomplete_expired 270 | past_due 271 | trialing 272 | unpaid 273 | } -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/aceternity/nextjs-boilerplate/11224908b7c263f0fbaeece3912b8875d5a3f088/public/favicon.ico -------------------------------------------------------------------------------- /public/vercel.svg: -------------------------------------------------------------------------------- 1 | 3 | 4 | -------------------------------------------------------------------------------- /styles/globals.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('tailwindcss').Config} */ 2 | module.exports = { 3 | content: [ 4 | './pages/**/*.{js,ts,jsx,tsx}', 5 | './components/**/*.{js,ts,jsx,tsx}', 6 | './app/**/*.{js,ts,jsx,tsx}', 7 | ], 8 | darkMode: 'class', 9 | theme: { 10 | extend: { 11 | colors: { 12 | primary: {"50":"#eff6ff","100":"#dbeafe","200":"#bfdbfe","300":"#93c5fd","400":"#60a5fa","500":"#3b82f6","600":"#2563eb","700":"#1d4ed8","800":"#1e40af","900":"#1e3a8a"} 13 | } 14 | }, 15 | fontFamily: { 16 | 'body': [ 17 | 'Inter', 18 | 'ui-sans-serif', 19 | 'system-ui', 20 | '-apple-system', 21 | 'system-ui', 22 | 'Segoe UI', 23 | 'Roboto', 24 | 'Helvetica Neue', 25 | 'Arial', 26 | 'Noto Sans', 27 | 'sans-serif', 28 | 'Apple Color Emoji', 29 | 'Segoe UI Emoji', 30 | 'Segoe UI Symbol', 31 | 'Noto Color Emoji' 32 | ], 33 | 'sans': [ 34 | 'Inter', 35 | 'ui-sans-serif', 36 | 'system-ui', 37 | '-apple-system', 38 | 'system-ui', 39 | 'Segoe UI', 40 | 'Roboto', 41 | 'Helvetica Neue', 42 | 'Arial', 43 | 'Noto Sans', 44 | 'sans-serif', 45 | 'Apple Color Emoji', 46 | 'Segoe UI Emoji', 47 | 'Segoe UI Symbol', 48 | 'Noto Color Emoji' 49 | ] 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": true, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "baseUrl": "./", 18 | "paths": { 19 | "@components/*": ["components/*"], 20 | "@styles/*": ["styles/*"], 21 | "@public/*": ["public/*"], 22 | "@lib/*": ["lib/*"], 23 | "@utils/*": ["utils/*"], 24 | "@types/*": ["types/*"], 25 | "@layout/*": ["layout/*"], 26 | "@constants/*": ["constants/*"], 27 | "@hooks/*": ["hooks/*"], 28 | "@pages/*": ["pages/*"], 29 | "@errors/*": ["errors/*"], 30 | } 31 | }, 32 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"], 33 | "exclude": ["node_modules"] 34 | } 35 | -------------------------------------------------------------------------------- /types/next-auth.d.ts: -------------------------------------------------------------------------------- 1 | import { Role } from "@prisma/client"; 2 | import NextAuth, { DefaultSession, DefaultUser } from "next-auth" 3 | 4 | declare module "next-auth" { 5 | interface Session { 6 | user: { 7 | role: string 8 | id: string | number; 9 | isOrganizationUser?: boolean; 10 | isOrganizationAdmin?: boolean; 11 | } & DefaultSession["user"] 12 | } 13 | 14 | interface User extends DefaultUser { 15 | id: string | number; 16 | } 17 | } -------------------------------------------------------------------------------- /utils/HashUtil.ts: -------------------------------------------------------------------------------- 1 | import bcrypt from 'bcryptjs'; 2 | 3 | class HashUtil { 4 | static async createHash(plaintext: string): Promise { 5 | const saltRounds = 10; 6 | const hash = await bcrypt.hash(plaintext, saltRounds); 7 | return hash; 8 | } 9 | 10 | static async compareHash(plaintext: string, hash: string): Promise { 11 | const match = await bcrypt.compare(plaintext, hash); 12 | return match; 13 | } 14 | } 15 | 16 | export default HashUtil; -------------------------------------------------------------------------------- /utils/pricing.ts: -------------------------------------------------------------------------------- 1 | const getPrice = (price: number, currency: string) => { 2 | const priceData = price / 100; 3 | return new Intl.NumberFormat("en-IN", { style: "currency", currency: currency.toUpperCase() }).format( 4 | priceData, 5 | ).replace(/\D00(?=\D*$)/, '') 6 | } 7 | 8 | export { 9 | getPrice 10 | } --------------------------------------------------------------------------------