├── .eslintrc.json
├── .gitignore
├── .vscode
└── settings.json
├── README.md
├── app
├── (site)
│ ├── account
│ │ └── profile
│ │ │ ├── layout.tsx
│ │ │ └── page.tsx
│ ├── admin
│ │ ├── client-permissions
│ │ │ ├── components
│ │ │ │ ├── columns.tsx
│ │ │ │ └── schema.ts
│ │ │ ├── layout.tsx
│ │ │ ├── loading.tsx
│ │ │ └── page.tsx
│ │ ├── database
│ │ │ ├── layout.tsx
│ │ │ ├── loading.tsx
│ │ │ └── page.tsx
│ │ ├── permissions
│ │ │ ├── components
│ │ │ │ ├── columns.tsx
│ │ │ │ └── schema.ts
│ │ │ ├── layout.tsx
│ │ │ ├── loading.tsx
│ │ │ └── page.tsx
│ │ ├── roles
│ │ │ ├── components
│ │ │ │ ├── columns.tsx
│ │ │ │ └── schema.ts
│ │ │ ├── layout.tsx
│ │ │ ├── loading.tsx
│ │ │ └── page.tsx
│ │ └── users
│ │ │ ├── components
│ │ │ ├── columns.tsx
│ │ │ └── schema.ts
│ │ │ ├── layout.tsx
│ │ │ ├── loading.tsx
│ │ │ └── page.tsx
│ ├── auth
│ │ ├── forgot-password
│ │ │ ├── layout.tsx
│ │ │ └── page.tsx
│ │ ├── login
│ │ │ ├── layout.tsx
│ │ │ └── page.tsx
│ │ ├── register
│ │ │ ├── layout.tsx
│ │ │ └── page.tsx
│ │ ├── reset-password
│ │ │ └── [token]
│ │ │ │ ├── layout.tsx
│ │ │ │ └── page.tsx
│ │ └── verification
│ │ │ └── [token]
│ │ │ ├── layout.tsx
│ │ │ └── page.tsx
│ └── favicon.ico
├── api
│ ├── auth
│ │ ├── forgot-password
│ │ │ └── route.ts
│ │ ├── login
│ │ │ └── route.ts
│ │ ├── register
│ │ │ └── route.ts
│ │ ├── reset-password
│ │ │ └── route.ts
│ │ └── verification
│ │ │ └── route.ts
│ ├── client-permissions
│ │ ├── [id]
│ │ │ └── route.ts
│ │ └── route.ts
│ ├── databases
│ │ ├── backup
│ │ │ └── route.ts
│ │ ├── download
│ │ │ └── route.ts
│ │ └── route.ts
│ ├── permissions
│ │ ├── [id]
│ │ │ └── route.ts
│ │ └── route.ts
│ ├── profile
│ │ ├── [id]
│ │ │ └── route.ts
│ │ └── route.ts
│ ├── roles
│ │ ├── [id]
│ │ │ └── route.ts
│ │ └── route.ts
│ ├── seeds
│ │ └── route.ts
│ ├── uploads
│ │ └── route.ts
│ └── users
│ │ ├── [id]
│ │ └── route.ts
│ │ └── route.ts
├── globals.css
├── layout.tsx
├── loading.tsx
├── not-found.tsx
└── page.tsx
├── components.json
├── components
├── confirm-dialog.tsx
├── confirm.tsx
├── custom-form.tsx
├── data-table.tsx
├── dropdown-checkbox.tsx
├── footer.tsx
├── form-container.tsx
├── form-view.tsx
├── format-number.tsx
├── message.tsx
├── navigation.tsx
├── pagination.tsx
├── search.tsx
├── skeleton.tsx
├── spinner.tsx
├── top-loading-bar.tsx
└── ui
│ ├── alert-dialog.tsx
│ ├── avatar.tsx
│ ├── badge.tsx
│ ├── button.tsx
│ ├── card.tsx
│ ├── checkbox.tsx
│ ├── command.tsx
│ ├── dialog.tsx
│ ├── drawer.tsx
│ ├── dropdown-menu.tsx
│ ├── form.tsx
│ ├── input.tsx
│ ├── label.tsx
│ ├── menubar.tsx
│ ├── popover.tsx
│ ├── select.tsx
│ ├── sonner.tsx
│ ├── switch.tsx
│ ├── table.tsx
│ ├── textarea.tsx
│ ├── toast.tsx
│ ├── toaster.tsx
│ └── use-toast.ts
├── config
└── data.ts
├── emails
├── ResetPassword.tsx
└── VerifyAccount.tsx
├── global.d.ts
├── hooks
├── useAuthorization.ts
└── useInterval.ts
├── lib
├── auth.ts
├── capitalize.ts
├── currency.ts
├── dateTime.ts
├── email-helper.ts
├── getDevice.ts
├── helpers.ts
├── meta.ts
├── numberFormat.ts
├── prisma.db.ts
├── provider.tsx
├── setting.ts
└── utils.ts
├── next.config.js
├── package.json
├── pnpm-lock.yaml
├── postcss.config.mjs
├── prisma
├── migrations
│ ├── 20250331142435_nanoid
│ │ └── migration.sql
│ ├── 20250522135703_add_accesstoken_to_user
│ │ └── migration.sql
│ └── migration_lock.toml
└── schema.prisma
├── public
├── next.svg
└── vercel.svg
├── server
├── index.js
└── socket.js
├── services
└── api.tsx
├── tsconfig.json
├── types
└── index.ts
└── zustand
├── dataStore.ts
├── resetStore.ts
├── useTempStore.ts
└── userStore.ts
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "next/core-web-vitals"
3 | }
4 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env*.local
29 | .env
30 |
31 | # vercel
32 | .vercel
33 |
34 | # typescript
35 | *.tsbuildinfo
36 | next-env.d.ts
37 |
38 | # user defined
39 | *.http
40 | /app/api.db
41 |
42 | # uploads
43 | /public/uploads/*.png
44 | /public/uploads/*.jpg
45 | /public/uploads/*.jpeg
46 | /public/uploads/*.svg
47 | /public/uploads/*.pdf
48 | /public/uploads/*.docx
49 | /public/uploads/*.doc
50 |
51 | /db/*.zip
52 |
53 | *.md
--------------------------------------------------------------------------------
/.vscode/settings.json:
--------------------------------------------------------------------------------
1 | {
2 | "editor.wordWrap": "wordWrapColumn",
3 | "editor.wordWrapColumn": 80
4 | }
5 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # NEXTjs Boilerplate
2 |
3 | This is a boilerplate project for a Next.js 15 app using TypeScript, Tailwind CSS, Shadcn UI, Prisma, and Postgres, React Table, React Query. It includes features such as login, user management, user roles, user permissions, user profile, forgot password, reset password, nodemailer and more.
4 |
5 | ## Getting started
6 |
7 | To use this boilerplate, clone the repository and install the dependencies:
8 |
9 | ```bash
10 | git clone https://github.com/ahmedibra28/NEXTjs-boilerplate.git
11 | cd NEXTjs-boilerplate
12 | npm install
13 | ```
14 |
15 | ### Environment setup
16 |
17 | Create a `.env.local` file in the root directory of the project with the following variables:
18 |
19 | ```
20 | DATABASE_URL=postgres://user:password@localhost:5432/db_name
21 | SMTP_SERVER=smtp.host.com
22 | SMTP_PORT=465
23 | SMTP_USER=user@host.com
24 | SMTP_KEY=password
25 | ```
26 |
27 | Make sure to replace `user`, `password`, and `db_name` with your own Postgres credentials, and `smtp.host.com`, `user@host.com`, and `password` with your own SMTP credentials.
28 |
29 | # Nano ID for postgres
30 |
31 | Creating a blank migration for Custom function to the PostgreSQL instance using:
32 |
33 | ```bash
34 | npx prisma migrate dev --create-only
35 | ```
36 |
37 | You can name this migration 'nanoid'. Open the file created by the migration and paste the nanoid function
38 |
39 | ```bash
40 | CREATE EXTENSION IF NOT EXISTS pgcrypto;
41 |
42 | CREATE OR REPLACE FUNCTION nanoid(size int DEFAULT 21)
43 | RETURNS text AS $$
44 | DECLARE
45 | id text := '';
46 | i int := 0;
47 | urlAlphabet char(64) := 'ModuleSymbhasOwnPr-0123456789ABCDEFGHNRVfgctiUvz_KqYTJkLxpZXIjQW';
48 | bytes bytea := gen_random_bytes(size);
49 | byte int;
50 | pos int;
51 | BEGIN
52 | WHILE i < size LOOP
53 | byte := get_byte(bytes, i);
54 | pos := (byte & 63) + 1; -- + 1 because substr starts at 1 for some reason
55 | id := id || substr(urlAlphabet, pos, 1);
56 | i = i + 1;
57 | END LOOP;
58 | RETURN id;
59 | END
60 | $$ LANGUAGE PLPGSQL STABLE;
61 | ```
62 |
63 | Then you can run this migration using:
64 |
65 | ```bash
66 | npx prisma migrate dev
67 | ```
68 |
69 | ### Starting the development server
70 |
71 | To start the development server, run:
72 |
73 | ```bash
74 | npm run dev
75 | ```
76 |
77 | ### Seeding data
78 |
79 | To seed data, make a GET request to `http://localhost:3000/api/seeds?secret=ts&option=reset` in your browser or with a tool like Postman. This will create default user roles and permissions and create a default admin user with the email `info@ahmedibra.com` and password `123456`.
80 |
81 | ## Contributing
82 |
83 | Contributions are welcome! Feel free to open an issue or submit a pull request.
84 |
85 | ## License
86 |
87 | This project is licensed under the MIT License.
88 |
--------------------------------------------------------------------------------
/app/(site)/account/profile/layout.tsx:
--------------------------------------------------------------------------------
1 | import meta from '@/lib/meta'
2 | import { logo, siteName } from '@/lib/setting'
3 |
4 | export const metadata = meta({
5 | title: 'Profile',
6 | description: `Profile at ${siteName}.`,
7 | openGraphImage: logo,
8 | })
9 |
10 | export default function ProfileLayout({
11 | children,
12 | }: {
13 | children: React.ReactNode
14 | }) {
15 | return
{children}
16 | }
17 |
--------------------------------------------------------------------------------
/app/(site)/admin/client-permissions/components/columns.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import { FaSort } from 'react-icons/fa'
5 | import { BsThreeDots } from 'react-icons/bs'
6 | import { ColumnDef } from '@tanstack/react-table'
7 |
8 | import { Button } from '@/components/ui/button'
9 | import {
10 | DropdownMenu,
11 | DropdownMenuContent,
12 | DropdownMenuItem,
13 | DropdownMenuLabel,
14 | DropdownMenuTrigger,
15 | } from '@/components/ui/dropdown-menu'
16 |
17 | import { ClientPermission } from '@prisma/client'
18 | import DateTime from '@/lib/dateTime'
19 | import { AlertDialog, AlertDialogTrigger } from '@/components/ui/alert-dialog'
20 | import { PencilIcon, XIcon } from 'lucide-react'
21 | import ConfirmDialog from '@/components/confirm'
22 |
23 | export const useColumns = ({
24 | editHandler,
25 | deleteHandler,
26 | }: {
27 | editHandler: (data: ClientPermission) => void
28 | deleteHandler: (id: any) => void
29 | }) => {
30 | const columns: ColumnDef[] = [
31 | {
32 | accessorKey: 'name',
33 | cell: ({ row }) => {row.getValue('name')},
34 |
35 | header: ({ column }) => {
36 | return (
37 |
44 | )
45 | },
46 | },
47 | { header: 'Menu', accessorKey: 'menu' },
48 | { header: 'Sort', accessorKey: 'sort' },
49 | { header: 'Path', accessorKey: 'path' },
50 | { header: 'Description', accessorKey: 'description' },
51 | {
52 | accessorKey: 'createdAt',
53 | header: 'DateTime',
54 | cell: ({ row }) => (
55 |
56 | {DateTime(row.getValue('createdAt')).format('YYYY-MM-DD HH:mm') ||
57 | '-'}
58 |
59 | ),
60 | },
61 | {
62 | id: 'actions',
63 | enableHiding: false,
64 | cell: ({ row }) => {
65 | const item = row.original
66 |
67 | return (
68 |
69 |
70 |
74 |
75 |
76 | Actions
77 | navigator.clipboard.writeText(item.id)}
79 | >
80 | Copy client permission ID
81 |
82 | editHandler(item)}>
83 |
84 | Edit
85 |
86 | {deleteHandler && (
87 |
88 |
89 |
90 | Delete
91 |
92 |
93 | deleteHandler(item.id)} />
94 |
95 | )}
96 |
97 |
98 | )
99 | },
100 | },
101 | ]
102 |
103 | return {
104 | columns,
105 | }
106 | }
107 |
--------------------------------------------------------------------------------
/app/(site)/admin/client-permissions/components/schema.ts:
--------------------------------------------------------------------------------
1 | import * as z from 'zod'
2 |
3 | export const FormSchema = z.object({
4 | name: z.string().refine((value) => value !== '', {
5 | message: 'Name is required',
6 | }),
7 | menu: z.string().refine((value) => value !== '', {
8 | message: 'Menu is required',
9 | }),
10 | sort: z.string(),
11 | path: z.string().refine((value) => value !== '', {
12 | message: 'Path is required',
13 | }),
14 | description: z.string().optional(),
15 | })
16 |
--------------------------------------------------------------------------------
/app/(site)/admin/client-permissions/layout.tsx:
--------------------------------------------------------------------------------
1 | import meta from '@/lib/meta'
2 | import { logo, siteName } from '@/lib/setting'
3 |
4 | export const metadata = meta({
5 | title: 'Client Permissions',
6 | description: `List of client permissions at ${siteName}.`,
7 | openGraphImage: logo,
8 | })
9 |
10 | export default function ClientPermissionsLayout({
11 | children,
12 | }: {
13 | children: React.ReactNode
14 | }) {
15 | return {children}
16 | }
17 |
--------------------------------------------------------------------------------
/app/(site)/admin/client-permissions/loading.tsx:
--------------------------------------------------------------------------------
1 | import Skeleton from "@/components/skeleton";
2 | import React from "react";
3 |
4 | const Loading = () => {
5 | return ;
6 | };
7 |
8 | export default Loading;
9 |
--------------------------------------------------------------------------------
/app/(site)/admin/database/layout.tsx:
--------------------------------------------------------------------------------
1 | import meta from '@/lib/meta'
2 | import { logo, siteName } from '@/lib/setting'
3 |
4 | export const metadata = meta({
5 | title: 'Databases',
6 | description: `List of databases at ${siteName}.`,
7 | openGraphImage: logo,
8 | })
9 |
10 | export default function DatabasesLayout({
11 | children,
12 | }: {
13 | children: React.ReactNode
14 | }) {
15 | return {children}
16 | }
17 |
--------------------------------------------------------------------------------
/app/(site)/admin/database/loading.tsx:
--------------------------------------------------------------------------------
1 | import Skeleton from '@/components/skeleton'
2 | import React from 'react'
3 |
4 | const Loading = () => {
5 | return
6 | }
7 |
8 | export default Loading
9 |
--------------------------------------------------------------------------------
/app/(site)/admin/database/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import React, { useEffect } from 'react'
3 | import {
4 | Table,
5 | TableBody,
6 | TableCaption,
7 | TableCell,
8 | TableHead,
9 | TableHeader,
10 | TableRow,
11 | } from '@/components/ui/table'
12 | import Spinner from '@/components/spinner'
13 | import Message from '@/components/message'
14 | import { TopLoadingBar } from '@/components/top-loading-bar'
15 | import { FaDatabase } from 'react-icons/fa6'
16 | import useAuthorization from '@/hooks/useAuthorization'
17 | import { useRouter } from 'next/navigation'
18 |
19 | import JSZip from 'jszip'
20 | import useUserInfoStore from '@/zustand/userStore'
21 | import dynamic from 'next/dynamic'
22 | import { FormButton } from '@/components/custom-form'
23 | import ApiCall from '@/services/api'
24 | import { baseUrl } from '@/lib/setting'
25 |
26 | function Page() {
27 | const path = useAuthorization()
28 | const router = useRouter()
29 |
30 | const {
31 | userInfo: { token },
32 | } = useUserInfoStore((state) => state)
33 |
34 | useEffect(() => {
35 | if (path) {
36 | router.push(path)
37 | }
38 | }, [path, router])
39 |
40 | const zip = new JSZip()
41 |
42 | const getApi = ApiCall({
43 | key: ['databases'],
44 | method: 'GET',
45 | url: `databases`,
46 | })?.get
47 |
48 | const postApi = ApiCall({
49 | key: ['databases'],
50 | method: 'POST',
51 | url: `databases/backup`,
52 | })?.post
53 |
54 | useEffect(() => {
55 | if (postApi?.isSuccess) {
56 | getApi?.refetch()
57 | }
58 | // eslint-disable-next-line
59 | }, [postApi?.isSuccess])
60 |
61 | const downloadDBHandler = async (db: string) => {
62 | return fetch(baseUrl + '/api/databases/download', {
63 | method: 'POST',
64 | headers: {
65 | 'Content-Type': 'application/zip',
66 | Authorization: `Bearer ${token}`,
67 | },
68 | body: JSON.stringify({ db }),
69 | })
70 | .then((response) => response.blob())
71 | .then((blob) => {
72 | zip.file(db, blob)
73 | zip
74 | .generateAsync({
75 | type: 'blob',
76 | streamFiles: true,
77 | })
78 | .then((data) => {
79 | const link = document.createElement('a')
80 | link.href = window.URL.createObjectURL(data)
81 | link.download = db
82 | link.click()
83 | })
84 | })
85 | }
86 |
87 | return (
88 |
89 | {postApi?.isError &&
}
90 |
91 |
96 |
97 |
98 | postApi?.mutateAsync({})}
100 | label='Backup Database'
101 | icon={}
102 | />
103 |
104 |
105 | {getApi?.isPending ? (
106 |
107 | ) : getApi?.isError ? (
108 |
109 | ) : getApi?.data?.data?.length > 0 ? (
110 |
111 | A list of your recent databases.
112 |
113 |
114 | Database
115 | Download
116 |
117 |
118 |
119 | {getApi?.data?.data?.map((db: any) => (
120 |
121 | {db}
122 |
123 | downloadDBHandler(db)}
125 | label='Download'
126 | icon={}
127 | size='sm'
128 | variant='secondary'
129 | />
130 |
131 |
132 | ))}
133 |
134 |
135 | ) : (
136 |
137 | No databases found
138 |
139 | )}
140 |
141 | )
142 | }
143 |
144 | export default dynamic(() => Promise.resolve(Page), { ssr: false })
145 |
--------------------------------------------------------------------------------
/app/(site)/admin/permissions/components/columns.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import { FaSort } from 'react-icons/fa'
5 | import { BsThreeDots } from 'react-icons/bs'
6 | import { ColumnDef } from '@tanstack/react-table'
7 |
8 | import { Button } from '@/components/ui/button'
9 | import {
10 | DropdownMenu,
11 | DropdownMenuContent,
12 | DropdownMenuItem,
13 | DropdownMenuLabel,
14 | DropdownMenuTrigger,
15 | } from '@/components/ui/dropdown-menu'
16 |
17 | import { Permission } from '@prisma/client'
18 | import DateTime from '@/lib/dateTime'
19 | import { AlertDialog, AlertDialogTrigger } from '@/components/ui/alert-dialog'
20 | import { PencilIcon, XIcon } from 'lucide-react'
21 | import ConfirmDialog from '@/components/confirm'
22 |
23 | export const useColumns = ({
24 | editHandler,
25 | deleteHandler,
26 | }: {
27 | editHandler: (data: Permission) => void
28 | deleteHandler: (id: any) => void
29 | }) => {
30 | const columns: ColumnDef[] = [
31 | {
32 | accessorKey: 'name',
33 | cell: ({ row }) => {row.getValue('name')},
34 |
35 | header: ({ column }) => {
36 | return (
37 |
44 | )
45 | },
46 | },
47 | {
48 | accessorKey: 'method',
49 | header: 'Method',
50 | cell: ({ row: { original } }: any) =>
51 | original?.method === 'GET' ? (
52 | {original?.method}
53 | ) : original?.method === 'POST' ? (
54 | {original?.method}
55 | ) : original?.method === 'PUT' ? (
56 | {original?.method}
57 | ) : (
58 | {original?.method}
59 | ),
60 | },
61 | { header: 'Route', accessorKey: 'route' },
62 | {
63 | accessorKey: 'createdAt',
64 | header: 'DateTime',
65 | cell: ({ row }) => (
66 |
67 | {DateTime(row.getValue('createdAt')).format('YYYY-MM-DD HH:mm') ||
68 | '-'}
69 |
70 | ),
71 | },
72 | {
73 | id: 'actions',
74 | enableHiding: false,
75 | cell: ({ row }) => {
76 | const item = row.original
77 |
78 | return (
79 |
80 |
81 |
85 |
86 |
87 | Actions
88 | navigator.clipboard.writeText(item.id)}
90 | >
91 | Copy permission ID
92 |
93 | editHandler(item)}>
94 |
95 | Edit
96 |
97 | {deleteHandler && (
98 |
99 |
100 |
101 | Delete
102 |
103 |
104 | deleteHandler(item.id)} />
105 |
106 | )}
107 |
108 |
109 | )
110 | },
111 | },
112 | ]
113 |
114 | return {
115 | columns,
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/app/(site)/admin/permissions/components/schema.ts:
--------------------------------------------------------------------------------
1 | import * as z from 'zod'
2 |
3 | export const FormSchema = z.object({
4 | name: z.string().refine((value) => value !== '', {
5 | message: 'name must be at least 2 characters',
6 | }),
7 | method: z.string().refine((value) => value !== '', {
8 | message: 'method is required',
9 | }),
10 | route: z.string().refine((value) => value !== '', {
11 | message: 'route is required',
12 | }),
13 | description: z.string().optional(),
14 | })
15 |
--------------------------------------------------------------------------------
/app/(site)/admin/permissions/layout.tsx:
--------------------------------------------------------------------------------
1 | import meta from '@/lib/meta'
2 | import { logo, siteName } from '@/lib/setting'
3 |
4 | export const metadata = meta({
5 | title: 'Permissions',
6 | description: `List of permissions at ${siteName}.`,
7 | openGraphImage: logo,
8 | })
9 |
10 | export default function PermissionsLayout({
11 | children,
12 | }: {
13 | children: React.ReactNode
14 | }) {
15 | return {children}
16 | }
17 |
--------------------------------------------------------------------------------
/app/(site)/admin/permissions/loading.tsx:
--------------------------------------------------------------------------------
1 | import Skeleton from "@/components/skeleton";
2 | import React from "react";
3 |
4 | const Loading = () => {
5 | return ;
6 | };
7 |
8 | export default Loading;
9 |
--------------------------------------------------------------------------------
/app/(site)/admin/roles/components/columns.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import { FaSort } from 'react-icons/fa'
5 | import { BsThreeDots } from 'react-icons/bs'
6 | import { ColumnDef } from '@tanstack/react-table'
7 |
8 | import { Button } from '@/components/ui/button'
9 | import {
10 | DropdownMenu,
11 | DropdownMenuContent,
12 | DropdownMenuItem,
13 | DropdownMenuLabel,
14 | DropdownMenuTrigger,
15 | } from '@/components/ui/dropdown-menu'
16 |
17 | import { ClientPermission, Permission, Role } from '@prisma/client'
18 | import DateTime from '@/lib/dateTime'
19 | import { AlertDialog, AlertDialogTrigger } from '@/components/ui/alert-dialog'
20 | import { PencilIcon, XIcon } from 'lucide-react'
21 | import ConfirmDialog from '@/components/confirm'
22 |
23 | export const useColumns = ({
24 | editHandler,
25 | deleteHandler,
26 | }: {
27 | editHandler: (
28 | item: ClientPermission & {
29 | role: { id: string }
30 | permissions: Permission[]
31 | clientPermissions: ClientPermission[]
32 | }
33 | ) => void
34 | deleteHandler: (id: any) => void
35 | }) => {
36 | const columns: ColumnDef[] = [
37 | {
38 | accessorKey: 'name',
39 | cell: ({ row }) => {row.getValue('name')},
40 |
41 | header: ({ column }) => {
42 | return (
43 |
50 | )
51 | },
52 | },
53 | {
54 | header: 'Type',
55 | accessorKey: 'type',
56 | cell: ({ row: { original } }: any) => original?.type?.toUpperCase(),
57 | },
58 | { header: 'Description', accessorKey: 'description' },
59 | {
60 | accessorKey: 'createdAt',
61 | header: 'DateTime',
62 | cell: ({ row }) => (
63 |
64 | {DateTime(row.getValue('createdAt')).format('YYYY-MM-DD HH:mm') ||
65 | '-'}
66 |
67 | ),
68 | },
69 | {
70 | id: 'actions',
71 | enableHiding: false,
72 | cell: ({ row }) => {
73 | const item = row.original
74 |
75 | return (
76 |
77 |
78 |
82 |
83 |
84 | Actions
85 | navigator.clipboard.writeText(item.id)}
87 | >
88 | Copy role ID
89 |
90 | editHandler(item as any)}>
91 |
92 | Edit
93 |
94 | {deleteHandler && (
95 |
96 |
97 |
98 | Delete
99 |
100 |
101 | deleteHandler(item.id)} />
102 |
103 | )}
104 |
105 |
106 | )
107 | },
108 | },
109 | ]
110 |
111 | return {
112 | columns,
113 | }
114 | }
115 |
--------------------------------------------------------------------------------
/app/(site)/admin/roles/components/schema.ts:
--------------------------------------------------------------------------------
1 | import * as z from 'zod'
2 |
3 | export const FormSchema = z.object({
4 | name: z.string().refine((value) => value !== '', {
5 | message: 'Name is required',
6 | }),
7 | description: z.string().optional(),
8 | permissions: z
9 | .array(z.string())
10 | .refine((value) => value.some((item) => item), {
11 | message: 'You have to select at least one item.',
12 | }),
13 | clientPermissions: z
14 | .array(z.string())
15 | .refine((value) => value.some((item) => item), {
16 | message: 'You have to select at least one item.',
17 | }),
18 | })
19 |
--------------------------------------------------------------------------------
/app/(site)/admin/roles/layout.tsx:
--------------------------------------------------------------------------------
1 | import meta from '@/lib/meta'
2 | import { logo, siteName } from '@/lib/setting'
3 |
4 | export const metadata = meta({
5 | title: 'Roles',
6 | description: `List of roles at ${siteName}.`,
7 | openGraphImage: logo,
8 | })
9 |
10 | export default function RolesLayout({
11 | children,
12 | }: {
13 | children: React.ReactNode
14 | }) {
15 | return {children}
16 | }
17 |
--------------------------------------------------------------------------------
/app/(site)/admin/roles/loading.tsx:
--------------------------------------------------------------------------------
1 | import Skeleton from "@/components/skeleton";
2 | import React from "react";
3 |
4 | const Loading = () => {
5 | return ;
6 | };
7 |
8 | export default Loading;
9 |
--------------------------------------------------------------------------------
/app/(site)/admin/users/components/columns.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import { FaSort } from 'react-icons/fa'
5 | import { BsThreeDots } from 'react-icons/bs'
6 | import { ColumnDef } from '@tanstack/react-table'
7 |
8 | import { Button } from '@/components/ui/button'
9 | import {
10 | DropdownMenu,
11 | DropdownMenuContent,
12 | DropdownMenuItem,
13 | DropdownMenuLabel,
14 | DropdownMenuTrigger,
15 | } from '@/components/ui/dropdown-menu'
16 |
17 | import { Role, User } from '@prisma/client'
18 | import DateTime from '@/lib/dateTime'
19 | import { AlertDialog, AlertDialogTrigger } from '@/components/ui/alert-dialog'
20 | import { PencilIcon, XIcon } from 'lucide-react'
21 | import ConfirmDialog from '@/components/confirm'
22 | import { FaCircleCheck, FaCircleXmark } from 'react-icons/fa6'
23 |
24 | export const useColumns = ({
25 | editHandler,
26 | deleteHandler,
27 | }: {
28 | editHandler: (data: User & { role: Role }) => void
29 | deleteHandler: (id: any) => void
30 | }) => {
31 | const columns: ColumnDef[] = [
32 | {
33 | accessorKey: 'name',
34 | cell: ({ row }) => {row.getValue('name')},
35 |
36 | header: ({ column }) => {
37 | return (
38 |
45 | )
46 | },
47 | },
48 | { header: 'Email', accessorKey: 'email' },
49 | { header: 'Role', accessorKey: 'role.name' },
50 | {
51 | header: 'Status',
52 | accessorKey: 'status',
53 | cell: ({ row: { original } }: any) =>
54 | original?.status === 'ACTIVE' ? (
55 |
56 | ) : original?.status === 'PENDING_VERIFICATION' ? (
57 |
58 | ) : (
59 |
60 | ),
61 | },
62 | {
63 | accessorKey: 'createdAt',
64 | header: 'DateTime',
65 | cell: ({ row }) => (
66 |
67 | {DateTime(row.getValue('createdAt')).format('YYYY-MM-DD HH:mm') ||
68 | '-'}
69 |
70 | ),
71 | },
72 | {
73 | id: 'actions',
74 | enableHiding: false,
75 | cell: ({ row }) => {
76 | const item = row.original
77 |
78 | return (
79 |
80 |
81 |
85 |
86 |
87 | Actions
88 | navigator.clipboard.writeText(item.id)}
90 | >
91 | Copy user ID
92 |
93 | editHandler(item as any)}>
94 |
95 | Edit
96 |
97 | {deleteHandler && (
98 |
99 |
100 |
101 | Delete
102 |
103 |
104 | deleteHandler(item.id)} />
105 |
106 | )}
107 |
108 |
109 | )
110 | },
111 | },
112 | ]
113 |
114 | return {
115 | columns,
116 | }
117 | }
118 |
--------------------------------------------------------------------------------
/app/(site)/admin/users/components/schema.ts:
--------------------------------------------------------------------------------
1 | import * as z from 'zod'
2 |
3 | export const FormSchema = z
4 | .object({
5 | name: z.string().refine((value) => value !== '', {
6 | message: 'Name is required',
7 | }),
8 | email: z
9 | .string()
10 | .email()
11 | .refine((value) => value !== '', {
12 | message: 'Email is required',
13 | }),
14 | roleId: z.string().refine((value) => value !== '', {
15 | message: 'Role is required',
16 | }),
17 | status: z.string().refine((value) => value !== '', {
18 | message: 'Status is required',
19 | }),
20 | password: z.string().refine((val) => val.length === 0 || val.length > 6, {
21 | message: "Password can't be less than 6 characters",
22 | }),
23 | confirmPassword: z
24 | .string()
25 | .refine((val) => val.length === 0 || val.length > 6, {
26 | message: "Confirm password can't be less than 6 characters",
27 | }),
28 | })
29 | .refine((data) => data.password === data.confirmPassword, {
30 | message: 'Password do not match',
31 | path: ['confirmPassword'],
32 | })
33 |
--------------------------------------------------------------------------------
/app/(site)/admin/users/layout.tsx:
--------------------------------------------------------------------------------
1 | import meta from '@/lib/meta'
2 | import { logo, siteName } from '@/lib/setting'
3 |
4 | export const metadata = meta({
5 | title: 'Users',
6 | description: `List of users at ${siteName}.`,
7 | openGraphImage: logo,
8 | })
9 |
10 | export default function UsersLayout({
11 | children,
12 | }: {
13 | children: React.ReactNode
14 | }) {
15 | return {children}
16 | }
17 |
--------------------------------------------------------------------------------
/app/(site)/admin/users/loading.tsx:
--------------------------------------------------------------------------------
1 | import Skeleton from "@/components/skeleton";
2 | import React from "react";
3 |
4 | const Loading = () => {
5 | return ;
6 | };
7 |
8 | export default Loading;
9 |
--------------------------------------------------------------------------------
/app/(site)/auth/forgot-password/layout.tsx:
--------------------------------------------------------------------------------
1 | import meta from '@/lib/meta'
2 | import { logo, siteName } from '@/lib/setting'
3 |
4 | export const metadata = meta({
5 | title: 'Forgot password',
6 | description: `Forgot password at ${siteName}.`,
7 | openGraphImage: logo,
8 | })
9 |
10 | export default function ForgotPasswordLayout({
11 | children,
12 | }: {
13 | children: React.ReactNode
14 | }) {
15 | return {children}
16 | }
17 |
--------------------------------------------------------------------------------
/app/(site)/auth/forgot-password/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import React, { useEffect } from 'react'
3 | import { useRouter } from 'next/navigation'
4 | import { useForm } from 'react-hook-form'
5 | import Head from 'next/head'
6 | import useUserInfoStore from '@/zustand/userStore'
7 | import FormContainer from '@/components/form-container'
8 | import Message from '@/components/message'
9 | import * as z from 'zod'
10 | import { zodResolver } from '@hookform/resolvers/zod'
11 | import { Form } from '@/components/ui/form'
12 | import CustomFormField, { FormButton } from '@/components/custom-form'
13 | import ApiCall from '@/services/api'
14 |
15 | const Page = () => {
16 | const router = useRouter()
17 | const { userInfo } = useUserInfoStore((state) => state)
18 |
19 | const FormSchema = z.object({
20 | email: z.string().email(),
21 | })
22 |
23 | const form = useForm>({
24 | resolver: zodResolver(FormSchema),
25 | defaultValues: {
26 | email: '',
27 | },
28 | })
29 |
30 | function onSubmit(values: z.infer) {
31 | postApi?.mutateAsync(values)
32 | }
33 | const postApi = ApiCall({
34 | key: ['forgot-password'],
35 | method: 'POST',
36 | url: `auth/forgot-password`,
37 | })?.post
38 |
39 | useEffect(() => {
40 | postApi?.isSuccess && form.reset()
41 | // eslint-disable-next-line
42 | }, [postApi?.isSuccess, form.reset])
43 |
44 | useEffect(() => {
45 | userInfo.id && router.push('/')
46 | }, [router, userInfo.id])
47 |
48 | return (
49 |
50 |
51 | Forgot
52 |
53 |
54 | {postApi?.isSuccess && }
55 | {postApi?.isError && }
56 |
57 |
72 |
73 |
74 | {postApi?.isSuccess && (
75 |
76 | Please check your email to reset your password
77 |
78 | )}
79 |
80 | )
81 | }
82 |
83 | export default Page
84 |
--------------------------------------------------------------------------------
/app/(site)/auth/login/layout.tsx:
--------------------------------------------------------------------------------
1 | import meta from '@/lib/meta'
2 | import { logo, siteName } from '@/lib/setting'
3 |
4 | export const metadata = meta({
5 | title: 'Login',
6 | description: `Login at ${siteName}.`,
7 | openGraphImage: logo,
8 | })
9 |
10 | export default function LoginLayout({
11 | children,
12 | }: {
13 | children: React.ReactNode
14 | }) {
15 | return {children}
16 | }
17 |
--------------------------------------------------------------------------------
/app/(site)/auth/login/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import React, { useEffect } from 'react'
3 | import Link from 'next/link'
4 | import { useRouter, useSearchParams } from 'next/navigation'
5 | import { useForm } from 'react-hook-form'
6 | import useUserInfoStore from '@/zustand/userStore'
7 | import FormContainer from '@/components/form-container'
8 | import Message from '@/components/message'
9 |
10 | import { zodResolver } from '@hookform/resolvers/zod'
11 | import * as z from 'zod'
12 | import { Form } from '@/components/ui/form'
13 | import CustomFormField, { FormButton } from '@/components/custom-form'
14 | import ApiCall from '@/services/api'
15 |
16 | const Page = () => {
17 | const router = useRouter()
18 | const params = useSearchParams().get('next')
19 |
20 | const { userInfo, updateUserInfo } = useUserInfoStore((state) => state)
21 |
22 | const postApi = ApiCall({
23 | key: ['login'],
24 | method: 'POST',
25 | url: `auth/login`,
26 | })?.post
27 |
28 | useEffect(() => {
29 | if (postApi?.isSuccess) {
30 | const { id, email, menu, routes, token, name, mobile, role, image } =
31 | postApi.data
32 | updateUserInfo({
33 | id,
34 | email,
35 | menu,
36 | routes,
37 | token,
38 | name,
39 | mobile,
40 | role,
41 | image,
42 | })
43 | }
44 | // eslint-disable-next-line react-hooks/exhaustive-deps
45 | }, [postApi?.isSuccess])
46 |
47 | useEffect(() => {
48 | userInfo.id && router.push((params as string) || '/')
49 | // eslint-disable-next-line react-hooks/exhaustive-deps
50 | }, [router, userInfo.id])
51 |
52 | const FormSchema = z.object({
53 | email: z.string().email(),
54 | password: z.string().min(6),
55 | })
56 |
57 | const form = useForm>({
58 | resolver: zodResolver(FormSchema),
59 | defaultValues: {
60 | email: '',
61 | password: '',
62 | },
63 | })
64 |
65 | function onSubmit(values: z.infer) {
66 | postApi?.mutateAsync(values)
67 | }
68 |
69 | return (
70 |
71 | {postApi?.isError && }
72 |
73 |
102 |
103 |
104 |
105 |
106 |
107 | Forgot Password?
108 |
109 |
110 |
111 |
112 | )
113 | }
114 |
115 | export default Page
116 |
--------------------------------------------------------------------------------
/app/(site)/auth/register/layout.tsx:
--------------------------------------------------------------------------------
1 | import meta from '@/lib/meta'
2 | import { logo, siteName } from '@/lib/setting'
3 |
4 | export const metadata = meta({
5 | title: 'Register',
6 | description: `Register at ${siteName}.`,
7 | openGraphImage: logo,
8 | })
9 |
10 | export default function RegisterLayout({
11 | children,
12 | }: {
13 | children: React.ReactNode
14 | }) {
15 | return {children}
16 | }
17 |
--------------------------------------------------------------------------------
/app/(site)/auth/register/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import React, { useEffect } from 'react'
3 | import { useRouter, useSearchParams } from 'next/navigation'
4 | import { useForm } from 'react-hook-form'
5 | import useUserInfoStore from '@/zustand/userStore'
6 | import FormContainer from '@/components/form-container'
7 | import Message from '@/components/message'
8 |
9 | import { zodResolver } from '@hookform/resolvers/zod'
10 | import * as z from 'zod'
11 | import { Form } from '@/components/ui/form'
12 | import CustomFormField, { FormButton } from '@/components/custom-form'
13 | import ApiCall from '@/services/api'
14 |
15 | const FormSchema = z
16 | .object({
17 | name: z.string().refine((value) => value !== '', {
18 | message: 'Name is required',
19 | }),
20 | email: z.string().email(),
21 | password: z.string().refine((val) => val.length > 6, {
22 | message: "Password can't be less than 6 characters",
23 | }),
24 | confirmPassword: z.string().refine((val) => val.length > 6, {
25 | message: "Confirm password can't be less than 6 characters",
26 | }),
27 | })
28 | .refine((data) => data.password === data.confirmPassword, {
29 | message: 'Password do not match',
30 | path: ['confirmPassword'],
31 | })
32 |
33 | const Page = () => {
34 | const router = useRouter()
35 | const params = useSearchParams().get('next')
36 |
37 | const { userInfo } = useUserInfoStore((state) => state)
38 |
39 | const postApi = ApiCall({
40 | key: ['register'],
41 | method: 'POST',
42 | url: `auth/register`,
43 | })?.post
44 |
45 | useEffect(() => {
46 | userInfo.id && router.push((params as string) || '/')
47 | // eslint-disable-next-line react-hooks/exhaustive-deps
48 | }, [router, userInfo.id])
49 |
50 | useEffect(() => {
51 | if (postApi?.isSuccess) {
52 | form.reset()
53 | }
54 | // eslint-disable-next-line
55 | }, [postApi?.isSuccess, router])
56 |
57 | const form = useForm>({
58 | resolver: zodResolver(FormSchema),
59 | defaultValues: {
60 | email: '',
61 | name: '',
62 | password: '',
63 | confirmPassword: '',
64 | },
65 | })
66 |
67 | function onSubmit(values: z.infer) {
68 | postApi?.mutateAsync(values)
69 | }
70 |
71 | return (
72 |
73 | {postApi?.isError && }
74 |
75 |
118 |
119 |
120 | {postApi?.isSuccess && (
121 |
122 | Please check your email to verify your account
123 |
124 | )}
125 |
126 | )
127 | }
128 |
129 | export default Page
130 |
--------------------------------------------------------------------------------
/app/(site)/auth/reset-password/[token]/layout.tsx:
--------------------------------------------------------------------------------
1 | import meta from '@/lib/meta'
2 | import { logo, siteName } from '@/lib/setting'
3 |
4 | export const metadata = meta({
5 | title: 'Reset password',
6 | description: `Reset password at ${siteName}.`,
7 | openGraphImage: logo,
8 | })
9 |
10 | export default function ResetPasswordLayout({
11 | children,
12 | }: {
13 | children: React.ReactNode
14 | }) {
15 | return {children}
16 | }
17 |
--------------------------------------------------------------------------------
/app/(site)/auth/reset-password/[token]/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import React, { useEffect, use } from 'react';
3 | import { useForm } from 'react-hook-form'
4 | import Head from 'next/head'
5 | import useUserInfoStore from '@/zustand/userStore'
6 | import FormContainer from '@/components/form-container'
7 | import Message from '@/components/message'
8 | import { useRouter } from 'next/navigation'
9 |
10 | import { zodResolver } from '@hookform/resolvers/zod'
11 | import * as z from 'zod'
12 | import { Form } from '@/components/ui/form'
13 | import CustomFormField, { FormButton } from '@/components/custom-form'
14 | import ApiCall from '@/services/api'
15 |
16 | const Reset = (
17 | props: {
18 | params: Promise<{
19 | token: string
20 | }>
21 | }
22 | ) => {
23 | const params = use(props.params);
24 | const router = useRouter()
25 | const { token } = params
26 | const { userInfo } = useUserInfoStore((state) => state)
27 |
28 | const postApi = ApiCall({
29 | key: ['reset-password'],
30 | method: 'POST',
31 | url: `auth/reset-password`,
32 | })?.post
33 |
34 | const FormSchema = z
35 | .object({
36 | password: z.string().min(6),
37 | confirmPassword: z.string().min(6),
38 | })
39 | .refine((data) => data.password === data.confirmPassword, {
40 | message: 'Password do not match',
41 | path: ['confirmPassword'],
42 | })
43 |
44 | const form = useForm>({
45 | resolver: zodResolver(FormSchema),
46 | defaultValues: {
47 | password: '',
48 | confirmPassword: '',
49 | },
50 | })
51 |
52 | function onSubmit(values: z.infer) {
53 | const password = values.password
54 | postApi?.mutateAsync({ password, resetToken: token })
55 | }
56 |
57 | useEffect(() => {
58 | if (postApi?.isSuccess) {
59 | form.reset()
60 | router.push('/auth/login')
61 | }
62 | // eslint-disable-next-line
63 | }, [postApi?.isSuccess, form.reset, router])
64 |
65 | useEffect(() => {
66 | userInfo.id && router.push('/')
67 | }, [router, userInfo.id])
68 |
69 | return (
70 |
71 |
72 | Reset
73 |
74 |
75 | {postApi?.isSuccess && }
76 |
77 | {postApi?.isError && }
78 |
79 |
103 |
104 |
105 | )
106 | }
107 |
108 | export default Reset
109 |
--------------------------------------------------------------------------------
/app/(site)/auth/verification/[token]/layout.tsx:
--------------------------------------------------------------------------------
1 | import meta from '@/lib/meta'
2 | import { logo, siteName } from '@/lib/setting'
3 |
4 | export const metadata = meta({
5 | title: 'Account verification',
6 | description: `Account verification at ${siteName}.`,
7 | openGraphImage: logo,
8 | })
9 |
10 | export default function AccountVerificationLayout({
11 | children,
12 | }: {
13 | children: React.ReactNode
14 | }) {
15 | return {children}
16 | }
17 |
--------------------------------------------------------------------------------
/app/(site)/auth/verification/[token]/page.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import React, { useEffect, use } from 'react';
3 | import { useForm } from 'react-hook-form'
4 | import Head from 'next/head'
5 | import useUserInfoStore from '@/zustand/userStore'
6 | import FormContainer from '@/components/form-container'
7 | import Message from '@/components/message'
8 | import { useRouter } from 'next/navigation'
9 |
10 | import { zodResolver } from '@hookform/resolvers/zod'
11 | import * as z from 'zod'
12 | import { Form } from '@/components/ui/form'
13 | import CustomFormField, { FormButton } from '@/components/custom-form'
14 | import ApiCall from '@/services/api'
15 |
16 | const Verification = (
17 | props: {
18 | params: Promise<{
19 | token: string
20 | }>
21 | }
22 | ) => {
23 | const params = use(props.params);
24 | const router = useRouter()
25 | const { token } = params
26 | const { userInfo } = useUserInfoStore((state) => state)
27 |
28 | const postApi = ApiCall({
29 | key: ['verification'],
30 | method: 'POST',
31 | url: `auth/verification`,
32 | })?.post
33 |
34 | function onSubmit() {
35 | postApi?.mutateAsync({ verificationToken: token })
36 | }
37 |
38 | useEffect(() => {
39 | if (postApi?.isSuccess) {
40 | setTimeout(() => {
41 | router.push('/auth/login')
42 | }, 3000)
43 | }
44 | // eslint-disable-next-line
45 | }, [postApi?.isSuccess, router])
46 |
47 | useEffect(() => {
48 | userInfo.id && router.push('/')
49 | }, [router, userInfo.id])
50 |
51 | return (
52 |
53 |
54 | Verification
55 |
56 |
57 | {postApi?.isSuccess && }
58 |
59 | {postApi?.isError && }
60 |
61 |
67 |
68 | )
69 | }
70 |
71 | export default Verification
72 |
--------------------------------------------------------------------------------
/app/(site)/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ahmedibra28/NEXTjs-boilerplate/80344f2f5badfdc19f4f0f19ce5bd1b60fc88318/app/(site)/favicon.ico
--------------------------------------------------------------------------------
/app/api/auth/forgot-password/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server'
2 | import { getErrorResponse, getResetPasswordToken } from '@/lib/helpers'
3 | import { prisma } from '@/lib/prisma.db'
4 | import { render } from '@react-email/render'
5 | import { handleEmailFire } from '@/lib/email-helper'
6 | import ResetPassword from '@/emails/ResetPassword'
7 | import { getDevice } from '@/lib/getDevice'
8 |
9 | export async function POST(req: NextApiRequestExtended) {
10 | try {
11 | const { email } = await req.json()
12 |
13 | if (!email) return getErrorResponse('Please enter your email', 400)
14 |
15 | const user = await prisma.user.findUnique({
16 | where: { email: email.toLowerCase() },
17 | })
18 |
19 | if (!user)
20 | return getErrorResponse(`There is no user with email ${email}`, 404)
21 |
22 | const reset = await getResetPasswordToken()
23 |
24 | await prisma.user.update({
25 | where: { id: user.id },
26 | data: {
27 | resetPasswordToken: reset.resetPasswordToken,
28 | resetPasswordExpire: reset.resetPasswordExpire,
29 | },
30 | })
31 |
32 | const device = await getDevice({
33 | req,
34 | hasIp: true,
35 | })
36 |
37 | const result = await handleEmailFire({
38 | to: email,
39 | subject: 'Reset Password Request',
40 | html: render(
41 | ResetPassword({
42 | clientName: device.clientName,
43 | osName: device.osName,
44 | token: reset.resetToken,
45 | company: 'Book Driving',
46 | ip: device.ip,
47 | baseUrl: device.url,
48 | })
49 | ),
50 | })
51 |
52 | if (result)
53 | return NextResponse.json({
54 | message: `An email has been sent to ${email} with further instructions.`,
55 | })
56 | } catch (error: any) {
57 | const { status = 500, message } = error
58 | return getErrorResponse(message, status, error, req)
59 | }
60 | }
61 |
--------------------------------------------------------------------------------
/app/api/auth/login/route.ts:
--------------------------------------------------------------------------------
1 | import { generateToken, getErrorResponse, matchPassword } from '@/lib/helpers'
2 | import { NextResponse } from 'next/server'
3 | import { prisma } from '@/lib/prisma.db'
4 |
5 | export async function POST(req: Request) {
6 | try {
7 | const { email, password } = await req.json()
8 |
9 | const user = await prisma.user.findUnique({
10 | where: {
11 | email: email.toLowerCase(),
12 | },
13 | })
14 |
15 | if (!user) return getErrorResponse('Invalid email or password', 401)
16 |
17 | const match = await matchPassword({
18 | enteredPassword: password,
19 | password: user.password,
20 | })
21 |
22 | if (!match) return getErrorResponse('Invalid email or password', 401)
23 |
24 | if (user.status === 'PENDING_VERIFICATION')
25 | return getErrorResponse('Please verify your account', 403)
26 |
27 | if (user.status === 'INACTIVE')
28 | return getErrorResponse('User is inactive', 403)
29 |
30 | const role =
31 | user.roleId &&
32 | (await prisma.role.findFirst({
33 | where: {
34 | id: user.roleId,
35 | },
36 | include: {
37 | clientPermissions: {
38 | select: {
39 | menu: true,
40 | sort: true,
41 | path: true,
42 | name: true,
43 | },
44 | },
45 | },
46 | }))
47 |
48 | if (!role) return getErrorResponse('Role not found', 404)
49 |
50 | const routes = role.clientPermissions
51 |
52 | interface Route {
53 | menu?: string
54 | name?: string
55 | path?: string
56 | open?: boolean
57 | sort?: number
58 | }
59 | interface RouteChildren extends Route {
60 | children?: { menu?: string; name?: string; path?: string }[] | any
61 | }
62 | const formatRoutes = (routes: Route[]) => {
63 | const formattedRoutes: RouteChildren[] = []
64 |
65 | routes.forEach((route) => {
66 | if (route.menu === 'hidden') return null
67 | if (route.menu === 'profile') return null
68 |
69 | if (route.menu === 'normal') {
70 | formattedRoutes.push({
71 | name: route.name,
72 | path: route.path,
73 | sort: route.sort,
74 | })
75 | } else {
76 | const found = formattedRoutes.find((r) => r.name === route.menu)
77 | if (found) {
78 | found.children.push({ name: route.name, path: route.path })
79 | } else {
80 | formattedRoutes.push({
81 | name: route.menu,
82 | sort: route.sort,
83 | open: false,
84 | children: [{ name: route.name, path: route.path }],
85 | })
86 | }
87 | }
88 | })
89 |
90 | return formattedRoutes
91 | }
92 |
93 | const sortMenu: any = (menu: any[]) => {
94 | const sortedMenu = menu.sort((a, b) => {
95 | if (a.sort === b.sort) {
96 | if (a.name < b.name) {
97 | return -1
98 | } else {
99 | return 1
100 | }
101 | } else {
102 | return a.sort - b.sort
103 | }
104 | })
105 |
106 | return sortedMenu.map((m) => {
107 | if (m.children) {
108 | return {
109 | ...m,
110 | children: sortMenu(m.children),
111 | }
112 | } else {
113 | return m
114 | }
115 | })
116 | }
117 |
118 | const accessToken = await generateToken(user.id)
119 | await prisma.user.update({
120 | where: {
121 | id: user.id,
122 | },
123 | data: {
124 | accessToken,
125 | },
126 | })
127 |
128 | return NextResponse.json({
129 | id: user.id,
130 | name: user.name,
131 | email: user.email,
132 | status: user.status,
133 | image: user.image,
134 | role: role.type,
135 | routes,
136 | menu: sortMenu(formatRoutes(routes) as any[]),
137 | token: accessToken,
138 | message: 'User has been logged in successfully',
139 | })
140 | } catch (error: any) {
141 | const { status = 500, message } = error
142 | return getErrorResponse(message, status, error, req)
143 | }
144 | }
145 |
--------------------------------------------------------------------------------
/app/api/auth/register/route.ts:
--------------------------------------------------------------------------------
1 | import {
2 | encryptPassword,
3 | getErrorResponse,
4 | getResetPasswordToken,
5 | } from '@/lib/helpers'
6 | import { NextResponse } from 'next/server'
7 | import { prisma } from '@/lib/prisma.db'
8 | import { handleEmailFire } from '@/lib/email-helper'
9 | import { render } from '@react-email/render'
10 | import VerifyAccount from '@/emails/VerifyAccount'
11 | import { roles } from '@/config/data'
12 | import { getDevice } from '@/lib/getDevice'
13 |
14 | export async function POST(req: Request) {
15 | try {
16 | const { name, email, password } = await req.json()
17 |
18 | const user =
19 | email &&
20 | (await prisma.user.findFirst({
21 | where: { email: email.toLowerCase() },
22 | }))
23 | if (user) {
24 | if (user.status === 'INACTIVE')
25 | return getErrorResponse('User is inactive', 403)
26 |
27 | if (user.status === 'ACTIVE')
28 | return getErrorResponse('User is already active', 409)
29 | }
30 |
31 | const reset = await getResetPasswordToken(4320)
32 |
33 | const roleId = roles.find((item) => item.type === 'AUTHENTICATED')?.id
34 |
35 | await prisma.user.upsert({
36 | where: { email: email.toLowerCase() },
37 | create: {
38 | name,
39 | email: email.toLowerCase(),
40 | status: 'PENDING_VERIFICATION',
41 | roleId: `${roleId}`,
42 | image: `https://ui-avatars.com/api/?uppercase=true&name=${name}&background=random&color=random&size=128`,
43 | password: await encryptPassword({ password }),
44 | resetPasswordToken: reset.resetPasswordToken,
45 | resetPasswordExpire: reset.resetPasswordExpire,
46 | },
47 | update: {
48 | status: 'PENDING_VERIFICATION',
49 | resetPasswordToken: reset.resetPasswordToken,
50 | resetPasswordExpire: reset.resetPasswordExpire,
51 | password: await encryptPassword({ password }),
52 | },
53 | })
54 |
55 | const device = await getDevice({
56 | req,
57 | hasIp: true,
58 | })
59 |
60 | const result = await handleEmailFire({
61 | to: email,
62 | subject: 'Verify your email',
63 | html: render(
64 | VerifyAccount({
65 | clientName: device.clientName,
66 | osName: device.osName,
67 | token: reset.resetToken,
68 | company: 'Ahmed Ibra',
69 | ip: device.ip,
70 | baseUrl: device.url,
71 | })
72 | ),
73 | })
74 |
75 | if (result)
76 | return NextResponse.json({
77 | message: `An email has been sent to ${email} with further instructions to verify your account.`,
78 | })
79 |
80 | return getErrorResponse('Something went wrong', 500)
81 | } catch (error: any) {
82 | const { status = 500, message } = error
83 | return getErrorResponse(message, status, error, req)
84 | }
85 | }
86 |
--------------------------------------------------------------------------------
/app/api/auth/reset-password/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server'
2 | import crypto from 'crypto'
3 | import { encryptPassword, getErrorResponse } from '@/lib/helpers'
4 | import { prisma } from '@/lib/prisma.db'
5 |
6 | export async function POST(req: NextApiRequestExtended) {
7 | try {
8 | const { password, resetToken } = await req.json()
9 |
10 | if (!resetToken || !password)
11 | return getErrorResponse('Invalid request', 401)
12 |
13 | const resetPasswordToken = crypto
14 | .createHash('sha256')
15 | .update(resetToken)
16 | .digest('hex')
17 |
18 | const user =
19 | resetPasswordToken &&
20 | (await prisma.user.findFirst({
21 | where: {
22 | resetPasswordToken,
23 | resetPasswordExpire: { gt: Date.now() },
24 | },
25 | }))
26 |
27 | if (!user) return getErrorResponse('Invalid token or expired', 401)
28 |
29 | const u = await prisma.user.update({
30 | where: { id: user.id },
31 | data: {
32 | resetPasswordToken: null,
33 | resetPasswordExpire: null,
34 | password: await encryptPassword({ password }),
35 | },
36 | })
37 |
38 | return NextResponse.json({ message: 'Password has been reset' })
39 | } catch (error: any) {
40 | const { status = 500, message } = error
41 | return getErrorResponse(message, status, error, req)
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/app/api/auth/verification/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server'
2 | import crypto from 'crypto'
3 | import { getErrorResponse } from '@/lib/helpers'
4 | import { prisma } from '@/lib/prisma.db'
5 |
6 | export async function POST(req: NextApiRequestExtended) {
7 | try {
8 | const { verificationToken } = await req.json()
9 |
10 | if (!verificationToken) return getErrorResponse('Invalid request', 401)
11 |
12 | const resetPasswordToken = crypto
13 | .createHash('sha256')
14 | .update(verificationToken)
15 | .digest('hex')
16 |
17 | const user =
18 | resetPasswordToken &&
19 | (await prisma.user.findFirst({
20 | where: {
21 | resetPasswordToken,
22 | resetPasswordExpire: { gt: Date.now() },
23 | },
24 | }))
25 |
26 | if (!user)
27 | return getErrorResponse(
28 | 'Invalid token or expired, please register your account again',
29 | 401
30 | )
31 |
32 | await prisma.user.update({
33 | where: { id: user.id },
34 | data: {
35 | resetPasswordToken: null,
36 | resetPasswordExpire: null,
37 | status: 'ACTIVE',
38 | },
39 | })
40 |
41 | return NextResponse.json({
42 | message: 'Account has been verified successfully',
43 | })
44 | } catch (error: any) {
45 | const { status = 500, message } = error
46 | return getErrorResponse(message, status, error, req)
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/app/api/client-permissions/[id]/route.ts:
--------------------------------------------------------------------------------
1 | import { isAuth } from '@/lib/auth'
2 | import { getErrorResponse } from '@/lib/helpers'
3 | import { NextResponse } from 'next/server'
4 | import { prisma } from '@/lib/prisma.db'
5 |
6 | interface Params {
7 | params: Promise<{
8 | id: string
9 | }>
10 | }
11 |
12 | export async function PUT(req: Request, props: Params) {
13 | const params = await props.params
14 | try {
15 | await isAuth(req, params)
16 |
17 | const { name, sort, menu, path, description } = await req.json()
18 |
19 | const clientPermissionObj = await prisma.clientPermission.findUnique({
20 | where: { id: params.id },
21 | })
22 | if (!clientPermissionObj)
23 | return getErrorResponse('Client permission not found', 404)
24 |
25 | const checkExistence =
26 | path &&
27 | params.id &&
28 | (await prisma.clientPermission.findFirst({
29 | where: {
30 | path: path.toLowerCase(),
31 | id: { not: params.id },
32 | },
33 | }))
34 | if (checkExistence)
35 | return getErrorResponse('Client permission already exist')
36 |
37 | await prisma.clientPermission.update({
38 | where: { id: params.id },
39 | data: {
40 | name,
41 | sort: Number(sort),
42 | menu,
43 | description,
44 | path: path.toLowerCase(),
45 | },
46 | })
47 |
48 | return NextResponse.json({
49 | ...clientPermissionObj,
50 | message: 'Client permission has been updated successfully',
51 | })
52 | } catch (error: any) {
53 | const { status = 500, message } = error
54 | return getErrorResponse(message, status, error, req)
55 | }
56 | }
57 |
58 | export async function DELETE(req: Request, props: Params) {
59 | const params = await props.params
60 | try {
61 | await isAuth(req, params)
62 |
63 | const clientPermissionObj = await prisma.clientPermission.delete({
64 | where: { id: params.id },
65 | })
66 | if (!clientPermissionObj)
67 | return getErrorResponse('Client permission not found', 404)
68 |
69 | return NextResponse.json({
70 | ...clientPermissionObj,
71 | message: 'Client permission has been removed successfully',
72 | })
73 | } catch (error: any) {
74 | const { status = 500, message } = error
75 | return getErrorResponse(message, status, error, req)
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/app/api/client-permissions/route.ts:
--------------------------------------------------------------------------------
1 | import { isAuth } from '@/lib/auth'
2 | import { getErrorResponse } from '@/lib/helpers'
3 | import { NextResponse } from 'next/server'
4 | import { QueryMode, prisma } from '@/lib/prisma.db'
5 |
6 | export async function GET(req: Request) {
7 | try {
8 | await isAuth(req)
9 |
10 | const { searchParams } = new URL(req.url)
11 | const q = searchParams.get('q')
12 |
13 | const query = q
14 | ? { name: { contains: q, mode: QueryMode.insensitive } }
15 | : {}
16 |
17 | const page = parseInt(searchParams.get('page') as string) || 1
18 | const pageSize = parseInt(searchParams.get('limit') as string) || 25
19 | const skip = (page - 1) * pageSize
20 |
21 | const [result, total] = await Promise.all([
22 | prisma.clientPermission.findMany({
23 | where: query,
24 | skip,
25 | take: pageSize,
26 | orderBy: { createdAt: 'desc' },
27 | }),
28 | prisma.clientPermission.count({ where: query }),
29 | ])
30 |
31 | const pages = Math.ceil(total / pageSize)
32 |
33 | return NextResponse.json({
34 | startIndex: skip + 1,
35 | endIndex: skip + result.length,
36 | count: result.length,
37 | page,
38 | pages,
39 | total,
40 | data: result,
41 | })
42 | } catch (error: any) {
43 | const { status = 500, message } = error
44 | return getErrorResponse(message, status, error, req)
45 | }
46 | }
47 |
48 | export async function POST(req: Request) {
49 | try {
50 | await isAuth(req)
51 |
52 | const { name, sort, menu, path, description } = await req.json()
53 |
54 | const checkExistence =
55 | path &&
56 | (await prisma.clientPermission.findFirst({
57 | where: { path: path.toLowerCase() },
58 | }))
59 | if (checkExistence)
60 | return getErrorResponse('Client permission already exist')
61 |
62 | const clientPermissionObj = await prisma.clientPermission.create({
63 | data: {
64 | name,
65 | description,
66 | sort: Number(sort),
67 | menu,
68 | path: path.toLowerCase(),
69 | },
70 | })
71 |
72 | return NextResponse.json({
73 | ...clientPermissionObj,
74 | message: 'Client permission created successfully',
75 | })
76 | } catch (error: any) {
77 | const { status = 500, message } = error
78 | return getErrorResponse(message, status, error, req)
79 | }
80 | }
81 |
--------------------------------------------------------------------------------
/app/api/databases/backup/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server'
2 | import {
3 | getEnvVariable,
4 | getErrorResponse,
5 | getBackupDirectory,
6 | } from '@/lib/helpers'
7 | import { exec } from 'child_process'
8 | import { promisify } from 'util'
9 | import { zip } from 'zip-a-folder'
10 | // import { isAuth } from '@/lib/auth'
11 | import { readdirSync, existsSync, mkdirSync } from 'fs'
12 | import { join } from 'path'
13 |
14 | const execAsync = promisify(exec)
15 |
16 | export async function POST(req: Request) {
17 | try {
18 | // await isAuth(req)
19 |
20 | const currentDate = new Date().toISOString().slice(0, 10)
21 | const currentHour = new Date().getHours().toString().padStart(2, '0')
22 | // const currentMinute = new Date().getMinutes().toString().padStart(2, '0');
23 |
24 | const baseBackupDir = getBackupDirectory() // Use the consistent path, e.g., /app/db_backups
25 | const backupFolderName = `${currentDate}_${currentHour}`
26 | const backupDir = join(baseBackupDir, backupFolderName) // Full path to the temp dump folder
27 | const zipFilePath = `${backupDir}.zip` // Full path to the final zip file
28 |
29 | // Ensure the base backup directory exists (important for the first run or if volume is empty)
30 | if (!existsSync(baseBackupDir)) {
31 | mkdirSync(baseBackupDir, { recursive: true })
32 | console.log(`Created base backup directory: ${baseBackupDir}`)
33 | }
34 |
35 | // Create the specific backup directory for this run
36 | await execAsync(`mkdir -p "${backupDir}"`) // Use quotes for paths
37 |
38 | // Clean up old zip files (logic remains similar, but uses baseBackupDir)
39 | const allFilesInBackupDir = readdirSync(baseBackupDir)
40 | const dbZipFiles = allFilesInBackupDir
41 | ?.filter((item) => item.endsWith('.zip'))
42 | .sort()
43 |
44 | if (dbZipFiles && dbZipFiles.length > 23) {
45 | const keepZippedDbs = dbZipFiles.slice(-23)
46 | const deleteZippedDbs = dbZipFiles.filter(
47 | (item) => !keepZippedDbs.includes(item)
48 | )
49 |
50 | // IMPORTANT: Fix async execution in loop
51 | await Promise.all(
52 | deleteZippedDbs.map(async (dbZip) => {
53 | const fullPathToDelete = join(baseBackupDir, dbZip)
54 | console.log(`Deleting old backup: ${fullPathToDelete}`)
55 | await execAsync(`rm -f "${fullPathToDelete}"`) // Use rm -f for files
56 | })
57 | )
58 | }
59 |
60 | const POSTGRES_USER = getEnvVariable('POSTGRES_USER')
61 | const POSTGRES_PASSWORD = getEnvVariable('POSTGRES_PASSWORD')
62 | // Use DB_HOST and DB_PORT from environment variables (set in docker-compose)
63 | const DB_HOST = getEnvVariable('DB_HOST')
64 | const DB_PORT = getEnvVariable('DB_PORT') // Should be 5432 typically for container-to-container
65 | const POSTGRES_DB = getEnvVariable('POSTGRES_DB') // Assuming 'toptayo' comes from env
66 |
67 | const execute = (dbName: string) =>
68 | `PGPASSWORD=${POSTGRES_PASSWORD} pg_dump -h ${DB_HOST} -p ${DB_PORT} -U ${POSTGRES_USER} -d ${dbName} -F c -f "${join(backupDir, `${dbName}.dump`)}"`
69 |
70 | // Assuming only one DB for now based on env var
71 | await execAsync(execute(POSTGRES_DB))
72 |
73 | // Convert folder to zip
74 | await zip(backupDir, zipFilePath)
75 | console.log(`Created backup zip: ${zipFilePath}`)
76 |
77 | // Delete folder after zip
78 | await execAsync(`rm -rf "${backupDir}"`)
79 | console.log(`Deleted temporary backup folder: ${backupDir}`)
80 |
81 | // Optional: Send backed up db to cloud
82 | // ...
83 |
84 | return NextResponse.json({
85 | message: `Database backup successfully created: ${backupFolderName}.zip`,
86 | })
87 | } catch (error: any) {
88 | console.error('Backup failed:', error) // Log the actual error server-side
89 | const { status = 500, message } = error
90 | return getErrorResponse(message || 'Backup failed', status, error)
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/app/api/databases/download/route.ts:
--------------------------------------------------------------------------------
1 | import { getErrorResponse, getBackupDirectory } from '@/lib/helpers'
2 | import { join } from 'path'
3 | import { readFileSync, existsSync } from 'fs'
4 | import { isAuth } from '@/lib/auth'
5 |
6 | export async function POST(req: Request) {
7 | try {
8 | await isAuth(req)
9 |
10 | const { db: requestedFileName } = await req.json()
11 |
12 | if (
13 | !requestedFileName ||
14 | typeof requestedFileName !== 'string' ||
15 | !requestedFileName.endsWith('.zip')
16 | ) {
17 | return getErrorResponse('Invalid file name specified', 400)
18 | }
19 |
20 | const backupDirPath = getBackupDirectory() // Get the consistent path
21 | const fullFilePath = join(backupDirPath, requestedFileName)
22 |
23 | // Security check: Ensure the requested file is directly within the backup directory
24 | // and doesn't contain path traversal characters ('..')
25 | if (
26 | fullFilePath.indexOf(backupDirPath) !== 0 ||
27 | requestedFileName.includes('..')
28 | ) {
29 | return getErrorResponse('Invalid file path', 400)
30 | }
31 |
32 | if (!existsSync(fullFilePath)) {
33 | console.error(`Download failed: File not found at ${fullFilePath}`)
34 | return getErrorResponse('File not found', 404)
35 | }
36 |
37 | const buffer = readFileSync(fullFilePath) as Buffer
38 |
39 | const headers = new Headers()
40 | headers.append(
41 | 'Content-Disposition',
42 | `attachment; filename="${requestedFileName}"`
43 | ) // Quote filename
44 | headers.append('Content-Type', 'application/zip')
45 | headers.append('Content-Length', buffer.length.toString()) // Good practice to add length
46 |
47 | return new Response(buffer, {
48 | headers,
49 | })
50 | } catch (error: any) {
51 | console.error('Download failed:', error)
52 | const { status = 500, message } = error
53 | return getErrorResponse(message || 'Failed to download file', status, error)
54 | }
55 | }
56 |
--------------------------------------------------------------------------------
/app/api/databases/route.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server'
2 | import { getErrorResponse, getBackupDirectory } from '@/lib/helpers'
3 | import { readdirSync, existsSync, mkdirSync } from 'fs'
4 | import { isAuth } from '@/lib/auth'
5 |
6 | export async function GET(req: Request) {
7 | try {
8 | await isAuth(req)
9 |
10 | const backupDirPath = getBackupDirectory() // Get the consistent path
11 |
12 | // Ensure the directory exists before trying to read it
13 | if (!existsSync(backupDirPath)) {
14 | // If it doesn't exist, it means no backups have been made yet.
15 | // Optionally create it here, or just return an empty list.
16 | mkdirSync(backupDirPath, { recursive: true }) // Create if doesn't exist
17 | console.log(`Created base backup directory during GET: ${backupDirPath}`)
18 | return NextResponse.json({ data: [] }) // Return empty list
19 | }
20 |
21 | // Read only .zip files
22 | const databases = readdirSync(backupDirPath).filter((file) =>
23 | file.endsWith('.zip')
24 | )
25 |
26 | return NextResponse.json({ data: databases?.reverse() || [] })
27 | } catch (error: any) {
28 | console.error('Failed to list backups:', error)
29 | const { status = 500, message } = error
30 | return getErrorResponse(message || 'Failed to list backups', status, error)
31 | }
32 | }
33 |
--------------------------------------------------------------------------------
/app/api/permissions/[id]/route.ts:
--------------------------------------------------------------------------------
1 | import { isAuth } from '@/lib/auth'
2 | import { getErrorResponse } from '@/lib/helpers'
3 | import { NextResponse } from 'next/server'
4 | import { prisma } from '@/lib/prisma.db'
5 |
6 | interface Params {
7 | params: Promise<{
8 | id: string
9 | }>
10 | }
11 |
12 | export async function PUT(req: Request, props: Params) {
13 | const params = await props.params
14 | try {
15 | await isAuth(req, params)
16 |
17 | const { name, method, route, description } = await req.json()
18 |
19 | const permissionObj = await prisma.permission.findUnique({
20 | where: { id: params.id },
21 | })
22 |
23 | if (!permissionObj) return getErrorResponse('Permission not found', 404)
24 |
25 | const checkExistence =
26 | method &&
27 | route &&
28 | params.id &&
29 | (await prisma.permission.findFirst({
30 | where: {
31 | method: method.toUpperCase(),
32 | route: route.toLowerCase(),
33 | id: { not: params.id },
34 | },
35 | }))
36 | if (checkExistence) return getErrorResponse('Permission already exist')
37 |
38 | await prisma.permission.update({
39 | where: { id: params.id },
40 | data: {
41 | name,
42 | method: method.toUpperCase(),
43 | description,
44 | route: route.toLowerCase(),
45 | },
46 | })
47 |
48 | return NextResponse.json({
49 | ...permissionObj,
50 | message: 'Permission has been updated successfully',
51 | })
52 | } catch (error: any) {
53 | const { status = 500, message } = error
54 | return getErrorResponse(message, status, error, req)
55 | }
56 | }
57 |
58 | export async function DELETE(req: Request, props: Params) {
59 | const params = await props.params
60 | try {
61 | await isAuth(req, params)
62 |
63 | const permissionObj = await prisma.permission.delete({
64 | where: { id: params.id },
65 | })
66 |
67 | if (!permissionObj) return getErrorResponse('Permission not removed', 404)
68 |
69 | return NextResponse.json({
70 | ...permissionObj,
71 | message: 'Permission has been removed successfully',
72 | })
73 | } catch (error: any) {
74 | const { status = 500, message } = error
75 | return getErrorResponse(message, status, error, req)
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/app/api/permissions/route.ts:
--------------------------------------------------------------------------------
1 | import { isAuth } from '@/lib/auth'
2 | import { getErrorResponse } from '@/lib/helpers'
3 | import { NextResponse } from 'next/server'
4 | import { QueryMode, prisma } from '@/lib/prisma.db'
5 |
6 | export async function GET(req: Request) {
7 | try {
8 | await isAuth(req)
9 |
10 | const { searchParams } = new URL(req.url)
11 | const q = searchParams.get('q')
12 |
13 | const query = q
14 | ? { name: { contains: q, mode: QueryMode.insensitive } }
15 | : {}
16 |
17 | const page = parseInt(searchParams.get('page') as string) || 1
18 | const pageSize = parseInt(searchParams.get('limit') as string) || 25
19 | const skip = (page - 1) * pageSize
20 |
21 | const [result, total] = await Promise.all([
22 | prisma.permission.findMany({
23 | where: query,
24 | skip,
25 | take: pageSize,
26 | orderBy: { createdAt: 'desc' },
27 | }),
28 | prisma.permission.count({ where: query }),
29 | ])
30 |
31 | const pages = Math.ceil(total / pageSize)
32 |
33 | return NextResponse.json({
34 | startIndex: skip + 1,
35 | endIndex: skip + result.length,
36 | count: result.length,
37 | page,
38 | pages,
39 | total,
40 | data: result,
41 | })
42 | } catch (error: any) {
43 | const { status = 500, message } = error
44 | return getErrorResponse(message, status, error, req)
45 | }
46 | }
47 |
48 | export async function POST(req: Request) {
49 | try {
50 | await isAuth(req)
51 |
52 | const { name, method, route, description } = await req.json()
53 |
54 | const checkExistence =
55 | method &&
56 | route &&
57 | (await prisma.permission.findFirst({
58 | where: {
59 | method: method.toUpperCase(),
60 | route: route.toLowerCase(),
61 | },
62 | }))
63 | if (checkExistence) return getErrorResponse('Permission already exist')
64 |
65 | const permissionObj = await prisma.permission.create({
66 | data: {
67 | name,
68 | method: method.toUpperCase(),
69 | route: route.toLowerCase(),
70 | description,
71 | },
72 | })
73 |
74 | return NextResponse.json({
75 | ...permissionObj,
76 | message: 'Permission created successfully',
77 | })
78 | } catch (error: any) {
79 | const { status = 500, message } = error
80 | return getErrorResponse(message, status, error, req)
81 | }
82 | }
83 |
--------------------------------------------------------------------------------
/app/api/profile/[id]/route.ts:
--------------------------------------------------------------------------------
1 | import { isAuth } from '@/lib/auth'
2 | import { encryptPassword, getErrorResponse } from '@/lib/helpers'
3 | import { NextResponse } from 'next/server'
4 | import { prisma } from '@/lib/prisma.db'
5 |
6 | interface Params {
7 | params: Promise<{
8 | id: string
9 | }>
10 | }
11 |
12 | export async function PUT(req: Request, props: Params) {
13 | const params = await props.params
14 | try {
15 | await isAuth(req, params)
16 |
17 | const { name, address, mobile, bio, image, password } = await req.json()
18 |
19 | const object = await prisma.user.findUnique({
20 | where: { id: params.id },
21 | })
22 |
23 | if (!object) return getErrorResponse('User profile not found', 404)
24 |
25 | if (password) {
26 | const regex =
27 | /^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$/
28 | if (!regex.test(password))
29 | return getErrorResponse(
30 | 'Password must be at least 8 characters long and contain at least one lowercase letter, one uppercase letter, one number and one special character',
31 | 400
32 | )
33 | }
34 |
35 | const result = await prisma.user.update({
36 | where: { id: params.id },
37 | data: {
38 | ...(password && { password: await encryptPassword({ password }) }),
39 | name: name || object.name,
40 | mobile: mobile || object.mobile,
41 | address: address || object.address,
42 | image: image || object.image,
43 | bio: bio || object.bio,
44 | },
45 | })
46 |
47 | return NextResponse.json({
48 | name: result.name,
49 | email: result.email,
50 | image: result.image,
51 | mobile: result.mobile,
52 | message: 'Profile has been updated successfully',
53 | })
54 | } catch (error: any) {
55 | const { status = 500, message } = error
56 | return getErrorResponse(message, status, error, req)
57 | }
58 | }
59 |
--------------------------------------------------------------------------------
/app/api/profile/route.ts:
--------------------------------------------------------------------------------
1 | import { isAuth } from '@/lib/auth'
2 | import { getErrorResponse } from '@/lib/helpers'
3 | import { NextResponse } from 'next/server'
4 | import { prisma } from '@/lib/prisma.db'
5 |
6 | export async function GET(req: NextApiRequestExtended) {
7 | try {
8 | await isAuth(req)
9 |
10 | const userObj = await prisma.user.findUnique({
11 | where: { id: req.user.id },
12 | select: {
13 | id: true,
14 | name: true,
15 | email: true,
16 | mobile: true,
17 | image: true,
18 | bio: true,
19 | address: true,
20 | },
21 | })
22 |
23 | return NextResponse.json(userObj)
24 | } catch (error: any) {
25 | const { status = 500, message } = error
26 | return getErrorResponse(message, status, error, req)
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/app/api/roles/[id]/route.ts:
--------------------------------------------------------------------------------
1 | import { isAuth } from '@/lib/auth'
2 | import { getErrorResponse } from '@/lib/helpers'
3 | import { NextResponse } from 'next/server'
4 | import { QueryMode, prisma } from '@/lib/prisma.db'
5 |
6 | interface Params {
7 | params: Promise<{
8 | id: string
9 | }>
10 | }
11 |
12 | export async function PUT(req: Request, props: Params) {
13 | const params = await props.params
14 | try {
15 | await isAuth(req, params)
16 |
17 | const {
18 | name,
19 | permissions: permissionRequest,
20 | clientPermissions: clientPermissionRequest,
21 | description,
22 | } = await req.json()
23 |
24 | let type
25 | let permission = []
26 | let clientPermission = []
27 | if (name) type = name?.toUpperCase().trim().replace(/\s+/g, '_')
28 |
29 | if (permissionRequest) {
30 | if (Array.isArray(permissionRequest)) {
31 | permission = permissionRequest
32 | } else {
33 | permission = [permissionRequest]
34 | }
35 | }
36 |
37 | if (clientPermissionRequest) {
38 | if (Array.isArray(clientPermissionRequest)) {
39 | clientPermission = clientPermissionRequest
40 | } else {
41 | clientPermission = [clientPermissionRequest]
42 | }
43 | }
44 |
45 | permission = permission?.filter((per) => per)
46 | clientPermission = clientPermission?.filter((client) => client)
47 |
48 | const object = await prisma.role.findUnique({
49 | where: { id: params.id },
50 | })
51 | if (!object) return getErrorResponse('Role not found', 400)
52 |
53 | const checkExistence =
54 | name &&
55 | type &&
56 | params.id &&
57 | (await prisma.role.findFirst({
58 | where: {
59 | name: { equals: name, mode: QueryMode.insensitive },
60 | type: { equals: type, mode: QueryMode.insensitive },
61 | id: { not: params.id },
62 | },
63 | }))
64 | if (checkExistence) return getErrorResponse('Role already exist')
65 |
66 | // prepare for disconnect
67 | const oldPermissions = await prisma.role.findUnique({
68 | where: { id: params.id },
69 | select: {
70 | permissions: { select: { id: true } },
71 | clientPermissions: { select: { id: true } },
72 | },
73 | })
74 |
75 | await prisma.role.update({
76 | where: { id: params.id },
77 | data: {
78 | name,
79 | description,
80 | type,
81 | permissions: {
82 | disconnect: oldPermissions?.permissions?.map((pre) => ({
83 | id: pre.id,
84 | })),
85 | connect: permission?.map((pre) => ({ id: pre })),
86 | },
87 | clientPermissions: {
88 | disconnect: oldPermissions?.clientPermissions?.map((client) => ({
89 | id: client.id,
90 | })),
91 | connect: clientPermission?.map((client) => ({ id: client })),
92 | },
93 | },
94 | })
95 |
96 | return NextResponse.json({
97 | ...object,
98 | message: 'Role updated successfully',
99 | })
100 | } catch (error: any) {
101 | const { status = 500, message } = error
102 | return getErrorResponse(message, status, error, req)
103 | }
104 | }
105 |
106 | export async function DELETE(req: Request, props: Params) {
107 | const params = await props.params
108 | try {
109 | await isAuth(req, params)
110 |
111 | const checkIfSuperAdmin = await prisma.role.findUnique({
112 | where: { id: params.id },
113 | })
114 | if (checkIfSuperAdmin && checkIfSuperAdmin?.type === 'SUPER_ADMIN')
115 | return getErrorResponse('Role is super admin', 400)
116 |
117 | const object = await prisma.role.delete({
118 | where: { id: params.id },
119 | })
120 |
121 | if (!object) return getErrorResponse('Role not found', 404)
122 |
123 | return NextResponse.json({
124 | ...object,
125 | message: 'Role deleted successfully',
126 | })
127 | } catch (error: any) {
128 | const { status = 500, message } = error
129 | return getErrorResponse(message, status, error, req)
130 | }
131 | }
132 |
--------------------------------------------------------------------------------
/app/api/roles/route.ts:
--------------------------------------------------------------------------------
1 | import { isAuth } from '@/lib/auth'
2 | import { getErrorResponse } from '@/lib/helpers'
3 | import { NextResponse } from 'next/server'
4 | import { QueryMode, prisma } from '@/lib/prisma.db'
5 |
6 | export async function GET(req: Request) {
7 | try {
8 | await isAuth(req)
9 |
10 | const { searchParams } = new URL(req.url)
11 | const q = searchParams.get('q')
12 |
13 | const query = q
14 | ? { name: { contains: q, mode: QueryMode.insensitive } }
15 | : {}
16 |
17 | const page = parseInt(searchParams.get('page') as string) || 1
18 | const pageSize = parseInt(searchParams.get('limit') as string) || 25
19 | const skip = (page - 1) * pageSize
20 |
21 | const [result, total] = await Promise.all([
22 | prisma.role.findMany({
23 | where: query,
24 | include: {
25 | permissions: true,
26 | clientPermissions: true,
27 | },
28 | skip,
29 | take: pageSize,
30 | orderBy: { createdAt: 'desc' },
31 | }),
32 | prisma.role.count({ where: query }),
33 | ])
34 |
35 | const pages = Math.ceil(total / pageSize)
36 |
37 | return NextResponse.json({
38 | startIndex: skip + 1,
39 | endIndex: skip + result.length,
40 | count: result.length,
41 | page,
42 | pages,
43 | total,
44 | data: result,
45 | })
46 | } catch (error: any) {
47 | const { status = 500, message } = error
48 | return getErrorResponse(message, status, error, req)
49 | }
50 | }
51 |
52 | export async function POST(req: Request) {
53 | try {
54 | await isAuth(req)
55 |
56 | const {
57 | name,
58 | description,
59 | permissions: permissionRequest,
60 | clientPermissions: clientPermissionRequest,
61 | } = await req.json()
62 |
63 | let type
64 | let permission = []
65 | let clientPermission = []
66 | if (name) type = name.toUpperCase().trim().replace(/\s+/g, '_')
67 |
68 | if (permissionRequest) {
69 | if (Array.isArray(permissionRequest)) {
70 | permission = permissionRequest
71 | } else {
72 | permission = [permissionRequest]
73 | }
74 | }
75 |
76 | if (clientPermissionRequest) {
77 | if (Array.isArray(clientPermissionRequest)) {
78 | clientPermission = clientPermissionRequest
79 | } else {
80 | clientPermission = [clientPermissionRequest]
81 | }
82 | }
83 |
84 | permission = permission?.filter((per) => per)
85 | clientPermission = clientPermission?.filter((client) => client)
86 |
87 | const checkExistence =
88 | name &&
89 | (await prisma.role.findFirst({
90 | where: { name: { equals: name, mode: QueryMode.insensitive } },
91 | }))
92 | if (checkExistence) return getErrorResponse('Role already exist')
93 |
94 | const object = await prisma.role.create({
95 | data: {
96 | name,
97 | description,
98 | type,
99 | permissions: {
100 | connect: permission?.map((pre) => ({ id: pre })),
101 | },
102 | clientPermissions: {
103 | connect: clientPermission?.map((client) => ({ id: client })),
104 | },
105 | },
106 | })
107 |
108 | return NextResponse.json({
109 | ...object,
110 | message: 'Role created successfully',
111 | })
112 | } catch (error: any) {
113 | const { status = 500, message } = error
114 | return getErrorResponse(message, status, error, req)
115 | }
116 | }
117 |
--------------------------------------------------------------------------------
/app/api/seeds/route.ts:
--------------------------------------------------------------------------------
1 | import { clientPermissions, permissions, roles, users } from '@/config/data'
2 |
3 | import { encryptPassword, getErrorResponse } from '@/lib/helpers'
4 | import { NextResponse } from 'next/server'
5 | import { prisma } from '@/lib/prisma.db'
6 |
7 | export async function GET(req: Request) {
8 | try {
9 | const { searchParams } = new URL(req.url)
10 | const secret = searchParams.get('secret')
11 | const option = searchParams.get('option')
12 |
13 | if (!secret || secret !== 'ts')
14 | return getErrorResponse('Invalid secret', 401)
15 |
16 | // Check duplicate permissions
17 | permissions.map((p) => {
18 | if (p.method && p.route) {
19 | const duplicate = permissions.filter(
20 | (p2) => p2.method === p.method && p2.route === p.route
21 | )
22 | if (duplicate.length > 1) {
23 | return getErrorResponse(
24 | `Duplicate permission: ${p.method} ${p.route}`,
25 | 500
26 | )
27 | }
28 | }
29 | })
30 |
31 | // Delete all existing data if option is reset
32 | if (option === 'reset') {
33 | await prisma.user.deleteMany({})
34 | await prisma.permission.deleteMany({})
35 | await prisma.clientPermission.deleteMany({})
36 | await prisma.role.deleteMany({})
37 | }
38 |
39 | // Create roles or update if exists
40 | await prisma.$transaction(async (prisma) => {
41 | await Promise.all(
42 | roles?.map(
43 | async (obj) =>
44 | await prisma.role.upsert({
45 | where: { id: obj.id },
46 | update: obj,
47 | create: obj,
48 | })
49 | )
50 | )
51 | })
52 |
53 | // Create users or update if exists
54 | await prisma.user.upsert({
55 | where: { id: users.id },
56 | create: {
57 | ...users,
58 | password: await encryptPassword({ password: users.password }),
59 | roleId: roles[0].id,
60 | status: 'ACTIVE',
61 | },
62 | update: {
63 | ...users,
64 | roleId: roles[0].id,
65 | password: await encryptPassword({ password: users.password }),
66 | status: 'ACTIVE',
67 | },
68 | })
69 |
70 | await prisma.user.update({
71 | data: {
72 | roleId: roles[0].id,
73 | },
74 | where: { id: users.id },
75 | })
76 |
77 | // Create permissions
78 | await Promise.all(
79 | permissions?.map(
80 | async (obj) =>
81 | await prisma.permission.upsert({
82 | where: { id: obj.id },
83 | update: obj as any,
84 | create: obj as any,
85 | })
86 | )
87 | )
88 |
89 | // Create client permissions
90 | await Promise.all(
91 | clientPermissions?.map(
92 | async (obj) =>
93 | await prisma.clientPermission.upsert({
94 | where: { id: obj.id },
95 | update: obj,
96 | create: obj,
97 | })
98 | )
99 | )
100 |
101 | // Create roles or update if exists
102 | await Promise.all(
103 | roles?.map(
104 | async (obj) =>
105 | await prisma.role.upsert({
106 | where: { id: obj.id },
107 | update: {
108 | ...obj,
109 | ...(obj.type === 'SUPER_ADMIN' && {
110 | permissions: {
111 | connect: permissions.map((p) => ({ id: p.id })),
112 | },
113 | }),
114 | ...(obj.type === 'SUPER_ADMIN' && {
115 | clientPermissions: {
116 | connect: clientPermissions.map((p) => ({
117 | id: p.id,
118 | })),
119 | },
120 | }),
121 | },
122 | create: {
123 | ...obj,
124 | ...(obj.type === 'SUPER_ADMIN' && {
125 | permissions: {
126 | connect: permissions.map((p) => ({ id: p.id })),
127 | },
128 | }),
129 | ...(obj.type === 'SUPER_ADMIN' && {
130 | clientPermissions: {
131 | connect: clientPermissions.map((p) => ({
132 | id: p.id,
133 | })),
134 | },
135 | }),
136 | },
137 | })
138 | )
139 | )
140 |
141 | return NextResponse.json({
142 | message: 'Database seeded successfully',
143 | users: await prisma.user.count({}),
144 | permissions: await prisma.permission.count({}),
145 | clientPermissions: await prisma.clientPermission.count({}),
146 | roles: await prisma.role.count({}),
147 | })
148 | } catch (error: any) {
149 | const { status = 500, message } = error
150 | return getErrorResponse(message, status, error, req)
151 | }
152 | }
153 |
--------------------------------------------------------------------------------
/app/api/uploads/route.ts:
--------------------------------------------------------------------------------
1 | import { isAuth } from '@/lib/auth'
2 | import { getEnvVariable, getErrorResponse } from '@/lib/helpers'
3 | import { NextResponse } from 'next/server'
4 | import { PutObjectCommand, S3Client } from '@aws-sdk/client-s3'
5 | import sharp from 'sharp'
6 |
7 | const uploadObject = async (fileName: string, data: any, bucket: string) => {
8 | const s3Client = new S3Client({
9 | endpoint: getEnvVariable('AWS_DO_ENDPOINT'),
10 | forcePathStyle: true,
11 | region: 'us-east-1',
12 | credentials: {
13 | accessKeyId: getEnvVariable('AWS_DO_ACCESS_KEY_ID'),
14 | secretAccessKey: getEnvVariable('AWS_DO_ACCESS_KEY'),
15 | } as {
16 | accessKeyId: string
17 | secretAccessKey: string
18 | },
19 | })
20 |
21 | const params = {
22 | Bucket: 'eballan',
23 | Key: fileName,
24 | Body: data,
25 | ACL: 'public-read',
26 | Metadata: {
27 | 'x-amz-meta-my-key': 'your-value',
28 | },
29 | }
30 |
31 | try {
32 | // @ts-ignore
33 | const data = await s3Client.send(new PutObjectCommand(params))
34 |
35 | return data
36 | } catch (err: any) {
37 | console.log('Error', err?.message)
38 | throw {
39 | message: err?.message,
40 | status: 500,
41 | }
42 | }
43 | }
44 |
45 | export async function POST(req: Request) {
46 | try {
47 | await isAuth(req)
48 |
49 | const { searchParams } = new URL(req.url)
50 | const type = searchParams.get('type')
51 |
52 | const data = await req.formData()
53 | const files: File[] | null = data.getAll('file') as unknown as File[]
54 |
55 | const allowedImageTypes = ['.png', '.jpg', '.jpeg', '.gif']
56 | const allowedDocumentTypes = ['.pdf', '.doc', '.docx', '.txt']
57 | const allowedTypes = ['document', 'image']
58 |
59 | if (!allowedTypes.includes(type as string))
60 | return getErrorResponse('Invalid file type', 400)
61 |
62 | const isAllowed = files.every((file) => {
63 | const ext = file.name.split('.').pop()?.toLowerCase()
64 | if (type === 'image') return allowedImageTypes.includes(`.${ext}`)
65 | if (type === 'document') return allowedDocumentTypes.includes(`.${ext}`)
66 | })
67 |
68 | if (!isAllowed) return getErrorResponse('Invalid file type', 400)
69 |
70 | const promises = files.map(async (file) => {
71 | const ext = file.name.split('.').pop()?.toLowerCase()
72 | const fileName = `${file.name.split('.')[0]}-${Date.now()}.${ext}`
73 | let buffer = Buffer.from(
74 | new Uint8Array(await file.arrayBuffer())
75 | ) as Buffer
76 |
77 | if (type === 'image') {
78 | const size = Buffer.byteLength(Buffer.from(buffer))
79 | if (size > 1024000) {
80 | buffer = await sharp(buffer)
81 | .resize(800, 800, { fit: 'inside' })
82 | .webp({ quality: 50 })
83 | .toBuffer()
84 | }
85 | }
86 |
87 | // await writeFile(filePath, buffer)
88 | await uploadObject(fileName, buffer, 'images')
89 | return fileName
90 | })
91 | const fileUrls = await Promise.all(promises)
92 | return NextResponse.json({
93 | message: 'File uploaded successfully',
94 | data: fileUrls?.map((url) => ({
95 | url: `https://farshaxan.blr1.cdn.digitaloceanspaces.com/eballan/${url.replace(
96 | /\s/g,
97 | '%20'
98 | )}`,
99 | })),
100 | })
101 | } catch (error: any) {
102 | const { status = 500, message } = error
103 | return getErrorResponse(message, status, error, req)
104 | }
105 | }
106 |
--------------------------------------------------------------------------------
/app/api/users/route.ts:
--------------------------------------------------------------------------------
1 | import { isAuth } from '@/lib/auth'
2 | import { encryptPassword, getErrorResponse } from '@/lib/helpers'
3 | import { NextResponse } from 'next/server'
4 | import { QueryMode, prisma } from '@/lib/prisma.db'
5 |
6 | export async function GET(req: Request) {
7 | try {
8 | await isAuth(req)
9 |
10 | const { searchParams } = new URL(req.url)
11 | const q = searchParams.get('q')
12 |
13 | const query = q
14 | ? { email: { contains: q, mode: QueryMode.insensitive } }
15 | : {}
16 |
17 | const page = parseInt(searchParams.get('page') as string) || 1
18 | const pageSize = parseInt(searchParams.get('limit') as string) || 25
19 | const skip = (page - 1) * pageSize
20 |
21 | const [result, total] = await Promise.all([
22 | prisma.user.findMany({
23 | where: query,
24 | skip,
25 | take: pageSize,
26 | orderBy: { createdAt: 'desc' },
27 | select: {
28 | id: true,
29 | name: true,
30 | email: true,
31 | status: true,
32 | createdAt: true,
33 | role: {
34 | select: {
35 | id: true,
36 | type: true,
37 | name: true,
38 | },
39 | },
40 | },
41 | }),
42 | prisma.user.count({ where: query }),
43 | ])
44 |
45 | const pages = Math.ceil(total / pageSize)
46 |
47 | return NextResponse.json({
48 | startIndex: skip + 1,
49 | endIndex: skip + result.length,
50 | count: result.length,
51 | page,
52 | pages,
53 | total,
54 | data: result,
55 | })
56 | } catch (error: any) {
57 | const { status = 500, message } = error
58 | return getErrorResponse(message, status, error, req)
59 | }
60 | }
61 |
62 | export async function POST(req: Request) {
63 | try {
64 | await isAuth(req)
65 |
66 | const { name, email, password, status, roleId } = await req.json()
67 |
68 | const role =
69 | roleId && (await prisma.role.findFirst({ where: { id: roleId } }))
70 | if (!role) return getErrorResponse('Role not found', 404)
71 |
72 | const user =
73 | email &&
74 | (await prisma.user.findFirst({
75 | where: { email: email.toLowerCase() },
76 | }))
77 | if (user) return getErrorResponse('User already exists', 409)
78 |
79 | const userObj = await prisma.user.create({
80 | data: {
81 | name,
82 | email: email.toLowerCase(),
83 | status,
84 | roleId: role.id,
85 | image: `https://ui-avatars.com/api/?uppercase=true&name=${name}&background=random&color=random&size=128`,
86 | password: await encryptPassword({ password }),
87 | },
88 | })
89 |
90 | userObj.password = undefined as any
91 |
92 | return NextResponse.json({
93 | userObj,
94 | message: 'User has been created successfully',
95 | })
96 | } catch (error: any) {
97 | const { status = 500, message } = error
98 | return getErrorResponse(message, status, error, req)
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/app/layout.tsx:
--------------------------------------------------------------------------------
1 | import './globals.css'
2 | import { Roboto } from 'next/font/google'
3 | import Navigation from '@/components/navigation'
4 | import Providers from '@/lib/provider'
5 | import Footer from '@/components/footer'
6 | import Link from 'next/link'
7 | import Image from 'next/image'
8 | import meta from '@/lib/meta'
9 | import { logo, siteName } from '@/lib/setting'
10 | import { Toaster } from '@/components/ui/toaster'
11 |
12 | const roboto = Roboto({
13 | subsets: ['latin'],
14 | weight: ['100', '300', '500', '700', '900'],
15 | })
16 |
17 | export const metadata = meta({
18 | title: `${siteName} - Everything You Need & More!`,
19 | description: `Find a vast selection of products across various categories at ${siteName}. From electronics and clothing to furniture and fresh produce, we offer everything you need for your home and lifestyle.`,
20 | openGraphImage: logo,
21 | })
22 |
23 | export default function RootLayout({
24 | children,
25 | }: {
26 | children: React.ReactNode
27 | }) {
28 | return (
29 |
30 |
34 |
35 |
36 |
37 |
38 |
45 |
46 |
47 |
48 |
49 |
50 | {children}
51 |
52 |
53 |
54 |
55 |
56 |
57 | )
58 | }
59 |
--------------------------------------------------------------------------------
/app/loading.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 |
3 | const Loading = () => {
4 | return (
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 | )
29 | }
30 |
31 | export default Loading
32 |
--------------------------------------------------------------------------------
/app/not-found.tsx:
--------------------------------------------------------------------------------
1 | import FormContainer from '@/components/form-container'
2 | import Navigation from '@/components/navigation'
3 | import Link from 'next/link'
4 | import meta from '@/lib/meta'
5 | import { logo, siteName } from '@/lib/setting'
6 |
7 | export const metadata = meta({
8 | title: `${siteName} - 404`,
9 | description: `This page does not exist at ${siteName}.`,
10 | openGraphImage: logo,
11 | })
12 |
13 | export default function NotFound() {
14 | return (
15 | <>
16 |
17 | This page does not exist.
18 |
19 | Please go back to the home page.
20 |
21 |
22 |
23 |
24 | Go Back
25 |
26 |
27 |
28 | >
29 | )
30 | }
31 |
--------------------------------------------------------------------------------
/app/page.tsx:
--------------------------------------------------------------------------------
1 | import FormContainer from '@/components/form-container'
2 |
3 | export default function Home() {
4 | return (
5 |
6 |
7 | Welcome to
8 |
9 | Next.JS 15
10 |
11 | boilerplate
12 |
13 |
14 | )
15 | }
16 |
--------------------------------------------------------------------------------
/components.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://ui.shadcn.com/schema.json",
3 | "style": "default",
4 | "rsc": true,
5 | "tsx": true,
6 | "tailwind": {
7 | "config": "tailwind.config.js",
8 | "css": "app/globals.css",
9 | "baseColor": "slate",
10 | "cssVariables": true,
11 | "prefix": ""
12 | },
13 | "aliases": {
14 | "components": "@/components",
15 | "utils": "@/lib/utils"
16 | }
17 | }
--------------------------------------------------------------------------------
/components/confirm-dialog.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | AlertDialogAction,
3 | AlertDialogCancel,
4 | AlertDialogContent,
5 | AlertDialogDescription,
6 | AlertDialogFooter,
7 | AlertDialogHeader,
8 | AlertDialogTitle,
9 | } from '@/components/ui/alert-dialog'
10 |
11 | export default function ConfirmDialog({
12 | onClick,
13 | message,
14 | }: {
15 | onClick: any
16 | message?: string
17 | }) {
18 | return (
19 |
20 |
21 | Are you absolutely sure?
22 |
23 | {message ||
24 | `This action cannot be undone. This will permanently delete your data from the database.`}
25 |
26 |
27 |
28 | Cancel
29 | Continue
30 |
31 |
32 | )
33 | }
34 |
--------------------------------------------------------------------------------
/components/confirm.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | AlertDialogAction,
3 | AlertDialogCancel,
4 | AlertDialogContent,
5 | AlertDialogDescription,
6 | AlertDialogFooter,
7 | AlertDialogHeader,
8 | AlertDialogTitle,
9 | } from '@/components/ui/alert-dialog'
10 |
11 | export default function ConfirmDialog({
12 | onClick,
13 | message,
14 | }: {
15 | onClick: any
16 | message?: string
17 | }) {
18 | return (
19 |
20 |
21 | Are you absolutely sure?
22 |
23 | {message ||
24 | `This action cannot be undone. This will permanently delete your data from the database.`}
25 |
26 |
27 |
28 | Cancel
29 | Continue
30 |
31 |
32 | )
33 | }
34 |
--------------------------------------------------------------------------------
/components/dropdown-checkbox.tsx:
--------------------------------------------------------------------------------
1 | import React, { Fragment } from 'react'
2 | import {
3 | Menubar,
4 | MenubarCheckboxItem,
5 | MenubarContent,
6 | MenubarMenu,
7 | MenubarTrigger,
8 | } from '@/components/ui/menubar'
9 |
10 | const DropdownCheckbox = ({
11 | visibleColumns,
12 | setVisibleColumns,
13 | }: {
14 | visibleColumns: { header: string; active: boolean }[]
15 | setVisibleColumns: React.Dispatch<
16 | React.SetStateAction<{ header: string; active: boolean }[]>
17 | >
18 | }) => {
19 | const handleCheckboxChange = (index: number) => {
20 | const updatedColumns = [...visibleColumns]
21 | updatedColumns[index].active = !updatedColumns[index].active
22 | setVisibleColumns(updatedColumns)
23 | }
24 |
25 | return (
26 |
27 |
28 |
29 | Filter
30 |
31 | {visibleColumns?.map((item, i: number) => (
32 | handleCheckboxChange(i)}
36 | checked={item.active}
37 | >
38 | {item.header}
39 |
40 | ))}
41 |
42 |
43 |
44 |
45 | )
46 | }
47 | export default DropdownCheckbox
48 |
--------------------------------------------------------------------------------
/components/footer.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import moment from 'moment'
4 |
5 | const Footer = () => {
6 | const year = moment().format('YYYY')
7 | return (
8 |
23 | )
24 | }
25 |
26 | export default Footer
27 |
--------------------------------------------------------------------------------
/components/form-container.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { ReactNode } from 'react'
3 |
4 | type Props = {
5 | children: ReactNode
6 | title?: string
7 | margin?: string
8 | }
9 |
10 | const FormContainer: React.FC = ({ children, title, margin = '' }) => {
11 | return (
12 |
13 |
14 |
15 | {title && (
16 |
19 | )}
20 | {children}
21 |
22 |
32 |
33 |
34 |
35 | )
36 | }
37 |
38 | export default FormContainer
39 |
--------------------------------------------------------------------------------
/components/form-view.tsx:
--------------------------------------------------------------------------------
1 | "use client";
2 |
3 | import {
4 | Dialog,
5 | DialogClose,
6 | DialogContent,
7 | DialogFooter,
8 | DialogHeader,
9 | DialogTitle,
10 | } from "@/components/ui/dialog";
11 | import { Button } from "./ui/button";
12 | import React from "react";
13 | import useDataStore from "@/zustand/dataStore";
14 | import { FormButton } from "./custom-form";
15 |
16 | interface Props {
17 | form: any;
18 | loading?: boolean;
19 | handleSubmit: (data: any) => () => void;
20 | submitHandler: (data: any) => void;
21 | label: string;
22 | height?: string;
23 | width?: string;
24 | edit: boolean;
25 | }
26 |
27 | const FormView = ({
28 | form,
29 | loading,
30 | handleSubmit,
31 | submitHandler,
32 | label,
33 | height,
34 | width,
35 | edit,
36 | }: Props) => {
37 | const { dialogOpen, setDialogOpen } = useDataStore((state) => state);
38 |
39 | return (
40 |
65 | );
66 | };
67 |
68 | export default FormView;
69 |
--------------------------------------------------------------------------------
/components/format-number.tsx:
--------------------------------------------------------------------------------
1 | import { currency } from '@/lib/currency'
2 | import { numberFormat } from '@/lib/numberFormat'
3 |
4 | export const FormatNumber = ({
5 | value,
6 | isCurrency,
7 | }: {
8 | value: number
9 | isCurrency: boolean
10 | }) => {
11 | const isN = typeof value === 'number' && !isNaN(value)
12 |
13 | return isN ? (
14 | isCurrency ? (
15 | {currency(value)}
16 | ) : (
17 | {numberFormat(value)}
18 | )
19 | ) : (
20 |
21 | )
22 | }
23 |
--------------------------------------------------------------------------------
/components/message.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { useEffect, useState } from 'react'
3 | import { FaCircleCheck, FaCircleXmark } from 'react-icons/fa6'
4 |
5 | interface Props {
6 | value: string | any
7 | }
8 |
9 | import { toast } from 'sonner'
10 |
11 | import { Toaster } from './ui/sonner'
12 | import DateTime from '@/lib/dateTime'
13 |
14 | const Message = ({ value = 'Internal Server Error!' }: Props) => {
15 | const [alert, setAlert] = useState(true)
16 |
17 | useEffect(() => {
18 | toast.message(value, {
19 | description: DateTime().format('ddd D MMM YYYY HH:mm:ss'),
20 | action: {
21 | label: 'Close',
22 | onClick: () => {},
23 | },
24 | })
25 |
26 | const timeId = setTimeout(() => {
27 | setAlert(false)
28 | }, 10000)
29 |
30 | return () => {
31 | clearTimeout(timeId)
32 | }
33 | // eslint-disable-next-line
34 | }, [alert])
35 |
36 | return (
37 | <>
38 | {alert && (
39 |
40 |
41 |
42 | )}
43 | >
44 | )
45 | }
46 |
47 | export default Message
48 |
--------------------------------------------------------------------------------
/components/pagination.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { FaChevronLeft, FaChevronRight } from 'react-icons/fa6'
3 |
4 | interface Props {
5 | data: {
6 | startIndex: number
7 | endIndex: number
8 | total: number
9 | page: number
10 | pages: number
11 | }
12 | setPage: (page: number) => void
13 | }
14 |
15 | const Pagination = ({ data, setPage }: Props) => {
16 | return data ? (
17 |
18 |
19 | {data.startIndex} - {data.endIndex} of {data.total}
20 |
21 |
28 |
35 |
36 | ) : null
37 | }
38 |
39 | export default Pagination
40 |
--------------------------------------------------------------------------------
/components/search.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import { FormEvent } from 'react'
3 | import { FaMagnifyingGlass } from 'react-icons/fa6'
4 | import { Input } from '@/components/ui/input'
5 | import { Button } from './ui/button'
6 |
7 | interface Props {
8 | q: string
9 | setQ: (value: string) => void
10 | placeholder: string
11 | searchHandler: (e: FormEvent) => void
12 | type?: string
13 | }
14 |
15 | const Search = ({
16 | q,
17 | setQ,
18 | placeholder,
19 | searchHandler,
20 | type = 'text',
21 | }: Props) => {
22 | return (
23 |
36 | )
37 | }
38 |
39 | export default Search
40 |
--------------------------------------------------------------------------------
/components/skeleton.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import { Card } from '@/components/ui/card'
3 |
4 | const Skeleton = () => {
5 | return (
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 | {[1, 2, 3, 4].map((item) => (
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | ))}
32 |
33 |
34 | )
35 | }
36 |
37 | export default Skeleton
38 |
--------------------------------------------------------------------------------
/components/spinner.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import LoadingIcons from 'react-loading-icons'
3 |
4 | interface Props {
5 | height?: string
6 | stroke?: string
7 | }
8 |
9 | const Spinner = (props: Props) => {
10 | const { height = '3em', stroke = '#06bcee' } = props
11 | return (
12 |
13 |
14 |
19 |
Loading...
20 |
21 |
22 | )
23 | }
24 |
25 | export default Spinner
26 |
--------------------------------------------------------------------------------
/components/top-loading-bar.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react'
2 | import LoadingBar, { LoadingBarRef } from 'react-top-loading-bar'
3 |
4 | export const TopLoadingBar = ({ isFetching }: { isFetching?: boolean }) => {
5 | const ref: React.Ref | undefined = React.useRef(null)
6 |
7 | React.useEffect(() => {
8 | if (isFetching) ref.current?.staticStart()
9 | else ref.current?.complete()
10 | }, [isFetching])
11 |
12 | return
13 | }
14 |
--------------------------------------------------------------------------------
/components/ui/alert-dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog"
5 |
6 | import { cn } from "@/lib/utils"
7 | import { buttonVariants } from "@/components/ui/button"
8 |
9 | const AlertDialog = AlertDialogPrimitive.Root
10 |
11 | const AlertDialogTrigger = AlertDialogPrimitive.Trigger
12 |
13 | const AlertDialogPortal = AlertDialogPrimitive.Portal
14 |
15 | const AlertDialogOverlay = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, ...props }, ref) => (
19 |
27 | ))
28 | AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName
29 |
30 | const AlertDialogContent = React.forwardRef<
31 | React.ElementRef,
32 | React.ComponentPropsWithoutRef
33 | >(({ className, ...props }, ref) => (
34 |
35 |
36 |
44 |
45 | ))
46 | AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName
47 |
48 | const AlertDialogHeader = ({
49 | className,
50 | ...props
51 | }: React.HTMLAttributes) => (
52 |
59 | )
60 | AlertDialogHeader.displayName = "AlertDialogHeader"
61 |
62 | const AlertDialogFooter = ({
63 | className,
64 | ...props
65 | }: React.HTMLAttributes) => (
66 |
73 | )
74 | AlertDialogFooter.displayName = "AlertDialogFooter"
75 |
76 | const AlertDialogTitle = React.forwardRef<
77 | React.ElementRef,
78 | React.ComponentPropsWithoutRef
79 | >(({ className, ...props }, ref) => (
80 |
85 | ))
86 | AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName
87 |
88 | const AlertDialogDescription = React.forwardRef<
89 | React.ElementRef,
90 | React.ComponentPropsWithoutRef
91 | >(({ className, ...props }, ref) => (
92 |
97 | ))
98 | AlertDialogDescription.displayName =
99 | AlertDialogPrimitive.Description.displayName
100 |
101 | const AlertDialogAction = React.forwardRef<
102 | React.ElementRef,
103 | React.ComponentPropsWithoutRef
104 | >(({ className, ...props }, ref) => (
105 |
110 | ))
111 | AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName
112 |
113 | const AlertDialogCancel = React.forwardRef<
114 | React.ElementRef,
115 | React.ComponentPropsWithoutRef
116 | >(({ className, ...props }, ref) => (
117 |
126 | ))
127 | AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName
128 |
129 | export {
130 | AlertDialog,
131 | AlertDialogPortal,
132 | AlertDialogOverlay,
133 | AlertDialogTrigger,
134 | AlertDialogContent,
135 | AlertDialogHeader,
136 | AlertDialogFooter,
137 | AlertDialogTitle,
138 | AlertDialogDescription,
139 | AlertDialogAction,
140 | AlertDialogCancel,
141 | }
142 |
--------------------------------------------------------------------------------
/components/ui/avatar.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as AvatarPrimitive from "@radix-ui/react-avatar"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Avatar = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 | ))
21 | Avatar.displayName = AvatarPrimitive.Root.displayName
22 |
23 | const AvatarImage = React.forwardRef<
24 | React.ElementRef,
25 | React.ComponentPropsWithoutRef
26 | >(({ className, ...props }, ref) => (
27 |
32 | ))
33 | AvatarImage.displayName = AvatarPrimitive.Image.displayName
34 |
35 | const AvatarFallback = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
47 | ))
48 | AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName
49 |
50 | export { Avatar, AvatarImage, AvatarFallback }
51 |
--------------------------------------------------------------------------------
/components/ui/badge.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { cva, type VariantProps } from "class-variance-authority"
3 |
4 | import { cn } from "@/lib/utils"
5 |
6 | const badgeVariants = cva(
7 | "inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
8 | {
9 | variants: {
10 | variant: {
11 | default:
12 | "border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
13 | secondary:
14 | "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
15 | destructive:
16 | "border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
17 | outline: "text-foreground",
18 | },
19 | },
20 | defaultVariants: {
21 | variant: "default",
22 | },
23 | }
24 | )
25 |
26 | export interface BadgeProps
27 | extends React.HTMLAttributes,
28 | VariantProps {}
29 |
30 | function Badge({ className, variant, ...props }: BadgeProps) {
31 | return (
32 |
33 | )
34 | }
35 |
36 | export { Badge, badgeVariants }
37 |
--------------------------------------------------------------------------------
/components/ui/button.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 | import { Slot } from "@radix-ui/react-slot"
3 | import { cva, type VariantProps } from "class-variance-authority"
4 |
5 | import { cn } from "@/lib/utils"
6 |
7 | const buttonVariants = cva(
8 | "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
9 | {
10 | variants: {
11 | variant: {
12 | default: "bg-primary text-primary-foreground hover:bg-primary/90",
13 | destructive:
14 | "bg-destructive text-destructive-foreground hover:bg-destructive/90",
15 | outline:
16 | "border border-input bg-background hover:bg-accent hover:text-accent-foreground",
17 | secondary:
18 | "bg-secondary text-secondary-foreground hover:bg-secondary/80",
19 | ghost: "hover:bg-accent hover:text-accent-foreground",
20 | link: "text-primary underline-offset-4 hover:underline",
21 | },
22 | size: {
23 | default: "h-10 px-4 py-2",
24 | sm: "h-9 rounded-md px-3",
25 | lg: "h-11 rounded-md px-8",
26 | icon: "h-10 w-10",
27 | },
28 | },
29 | defaultVariants: {
30 | variant: "default",
31 | size: "default",
32 | },
33 | }
34 | )
35 |
36 | export interface ButtonProps
37 | extends React.ButtonHTMLAttributes,
38 | VariantProps {
39 | asChild?: boolean
40 | }
41 |
42 | const Button = React.forwardRef(
43 | ({ className, variant, size, asChild = false, ...props }, ref) => {
44 | const Comp = asChild ? Slot : "button"
45 | return (
46 |
51 | )
52 | }
53 | )
54 | Button.displayName = "Button"
55 |
56 | export { Button, buttonVariants }
57 |
--------------------------------------------------------------------------------
/components/ui/card.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Card = React.forwardRef<
6 | HTMLDivElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
17 | ))
18 | Card.displayName = "Card"
19 |
20 | const CardHeader = React.forwardRef<
21 | HTMLDivElement,
22 | React.HTMLAttributes
23 | >(({ className, ...props }, ref) => (
24 |
29 | ))
30 | CardHeader.displayName = "CardHeader"
31 |
32 | const CardTitle = React.forwardRef<
33 | HTMLParagraphElement,
34 | React.HTMLAttributes
35 | >(({ className, ...props }, ref) => (
36 |
44 | ))
45 | CardTitle.displayName = "CardTitle"
46 |
47 | const CardDescription = React.forwardRef<
48 | HTMLParagraphElement,
49 | React.HTMLAttributes
50 | >(({ className, ...props }, ref) => (
51 |
56 | ))
57 | CardDescription.displayName = "CardDescription"
58 |
59 | const CardContent = React.forwardRef<
60 | HTMLDivElement,
61 | React.HTMLAttributes
62 | >(({ className, ...props }, ref) => (
63 |
64 | ))
65 | CardContent.displayName = "CardContent"
66 |
67 | const CardFooter = React.forwardRef<
68 | HTMLDivElement,
69 | React.HTMLAttributes
70 | >(({ className, ...props }, ref) => (
71 |
76 | ))
77 | CardFooter.displayName = "CardFooter"
78 |
79 | export { Card, CardHeader, CardFooter, CardTitle, CardDescription, CardContent }
80 |
--------------------------------------------------------------------------------
/components/ui/checkbox.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox"
5 | import { Check } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Checkbox = React.forwardRef<
10 | React.ElementRef,
11 | React.ComponentPropsWithoutRef
12 | >(({ className, ...props }, ref) => (
13 |
21 |
24 |
25 |
26 |
27 | ))
28 | Checkbox.displayName = CheckboxPrimitive.Root.displayName
29 |
30 | export { Checkbox }
31 |
--------------------------------------------------------------------------------
/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as DialogPrimitive from '@radix-ui/react-dialog'
5 | // import { X } from 'lucide-react'
6 |
7 | import { cn } from '@/lib/utils'
8 |
9 | const Dialog = DialogPrimitive.Root
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger
12 |
13 | const DialogPortal = DialogPrimitive.Portal
14 |
15 | const DialogClose = DialogPrimitive.Close
16 |
17 | const DialogOverlay = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef
20 | >(({ className, ...props }, ref) => (
21 |
29 | ))
30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
31 |
32 | const DialogContent = React.forwardRef<
33 | React.ElementRef,
34 | React.ComponentPropsWithoutRef
35 | >(({ className, children, ...props }, ref) => (
36 |
37 |
38 |
46 | {children}
47 | {/*
48 |
49 | Close
50 | */}
51 |
52 |
53 | ))
54 | DialogContent.displayName = DialogPrimitive.Content.displayName
55 |
56 | const DialogHeader = ({
57 | className,
58 | ...props
59 | }: React.HTMLAttributes) => (
60 |
67 | )
68 | DialogHeader.displayName = 'DialogHeader'
69 |
70 | const DialogFooter = ({
71 | className,
72 | ...props
73 | }: React.HTMLAttributes) => (
74 |
81 | )
82 | DialogFooter.displayName = 'DialogFooter'
83 |
84 | const DialogTitle = React.forwardRef<
85 | React.ElementRef,
86 | React.ComponentPropsWithoutRef
87 | >(({ className, ...props }, ref) => (
88 |
96 | ))
97 | DialogTitle.displayName = DialogPrimitive.Title.displayName
98 |
99 | const DialogDescription = React.forwardRef<
100 | React.ElementRef,
101 | React.ComponentPropsWithoutRef
102 | >(({ className, ...props }, ref) => (
103 |
108 | ))
109 | DialogDescription.displayName = DialogPrimitive.Description.displayName
110 |
111 | export {
112 | Dialog,
113 | DialogPortal,
114 | DialogOverlay,
115 | DialogClose,
116 | DialogTrigger,
117 | DialogContent,
118 | DialogHeader,
119 | DialogFooter,
120 | DialogTitle,
121 | DialogDescription,
122 | }
123 |
--------------------------------------------------------------------------------
/components/ui/drawer.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import { Drawer as DrawerPrimitive } from "vaul"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Drawer = ({
9 | shouldScaleBackground = true,
10 | ...props
11 | }: React.ComponentProps) => (
12 |
16 | )
17 | Drawer.displayName = "Drawer"
18 |
19 | const DrawerTrigger = DrawerPrimitive.Trigger
20 |
21 | const DrawerPortal = DrawerPrimitive.Portal
22 |
23 | const DrawerClose = DrawerPrimitive.Close
24 |
25 | const DrawerOverlay = React.forwardRef<
26 | React.ElementRef,
27 | React.ComponentPropsWithoutRef
28 | >(({ className, ...props }, ref) => (
29 |
34 | ))
35 | DrawerOverlay.displayName = DrawerPrimitive.Overlay.displayName
36 |
37 | const DrawerContent = React.forwardRef<
38 | React.ElementRef,
39 | React.ComponentPropsWithoutRef
40 | >(({ className, children, ...props }, ref) => (
41 |
42 |
43 |
51 |
52 | {children}
53 |
54 |
55 | ))
56 | DrawerContent.displayName = "DrawerContent"
57 |
58 | const DrawerHeader = ({
59 | className,
60 | ...props
61 | }: React.HTMLAttributes) => (
62 |
66 | )
67 | DrawerHeader.displayName = "DrawerHeader"
68 |
69 | const DrawerFooter = ({
70 | className,
71 | ...props
72 | }: React.HTMLAttributes) => (
73 |
77 | )
78 | DrawerFooter.displayName = "DrawerFooter"
79 |
80 | const DrawerTitle = React.forwardRef<
81 | React.ElementRef,
82 | React.ComponentPropsWithoutRef
83 | >(({ className, ...props }, ref) => (
84 |
92 | ))
93 | DrawerTitle.displayName = DrawerPrimitive.Title.displayName
94 |
95 | const DrawerDescription = React.forwardRef<
96 | React.ElementRef,
97 | React.ComponentPropsWithoutRef
98 | >(({ className, ...props }, ref) => (
99 |
104 | ))
105 | DrawerDescription.displayName = DrawerPrimitive.Description.displayName
106 |
107 | export {
108 | Drawer,
109 | DrawerPortal,
110 | DrawerOverlay,
111 | DrawerTrigger,
112 | DrawerClose,
113 | DrawerContent,
114 | DrawerHeader,
115 | DrawerFooter,
116 | DrawerTitle,
117 | DrawerDescription,
118 | }
119 |
--------------------------------------------------------------------------------
/components/ui/form.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 | import * as LabelPrimitive from '@radix-ui/react-label'
3 | import { Slot } from '@radix-ui/react-slot'
4 | import {
5 | Controller,
6 | ControllerProps,
7 | FieldPath,
8 | FieldValues,
9 | FormProvider,
10 | useFormContext,
11 | } from 'react-hook-form'
12 |
13 | import { cn } from '@/lib/utils'
14 | import { Label } from '@/components/ui/label'
15 |
16 | const Form = FormProvider
17 |
18 | type FormFieldContextValue<
19 | TFieldValues extends FieldValues = FieldValues,
20 | TName extends FieldPath = FieldPath
21 | > = {
22 | name: TName
23 | }
24 |
25 | const FormFieldContext = React.createContext(
26 | {} as FormFieldContextValue
27 | )
28 |
29 | const FormField = <
30 | TFieldValues extends FieldValues = FieldValues,
31 | TName extends FieldPath = FieldPath
32 | >({
33 | ...props
34 | }: ControllerProps) => {
35 | return (
36 |
37 |
38 |
39 | )
40 | }
41 |
42 | const useFormField = () => {
43 | const fieldContext = React.useContext(FormFieldContext)
44 | const itemContext = React.useContext(FormItemContext)
45 | const { getFieldState, formState } = useFormContext()
46 |
47 | const fieldState = getFieldState(fieldContext.name, formState)
48 |
49 | if (!fieldContext) {
50 | throw new Error('useFormField should be used within ')
51 | }
52 |
53 | const { id } = itemContext
54 |
55 | return {
56 | id,
57 | name: fieldContext.name,
58 | formItemId: `${id}-form-item`,
59 | formDescriptionId: `${id}-form-item-description`,
60 | formMessageId: `${id}-form-item-message`,
61 | ...fieldState,
62 | }
63 | }
64 |
65 | type FormItemContextValue = {
66 | id: string
67 | }
68 |
69 | const FormItemContext = React.createContext(
70 | {} as FormItemContextValue
71 | )
72 |
73 | const FormItem = React.forwardRef<
74 | HTMLDivElement,
75 | React.HTMLAttributes
76 | >(({ className, ...props }, ref) => {
77 | const id = React.useId()
78 |
79 | return (
80 |
81 |
82 |
83 | )
84 | })
85 | FormItem.displayName = 'FormItem'
86 |
87 | const FormLabel = React.forwardRef<
88 | React.ElementRef,
89 | React.ComponentPropsWithoutRef
90 | >(({ className, ...props }, ref) => {
91 | const { error, formItemId } = useFormField()
92 |
93 | return (
94 |
100 | )
101 | })
102 | FormLabel.displayName = 'FormLabel'
103 |
104 | const FormControl = React.forwardRef<
105 | React.ElementRef,
106 | React.ComponentPropsWithoutRef
107 | >(({ ...props }, ref) => {
108 | const { error, formItemId, formDescriptionId, formMessageId } = useFormField()
109 |
110 | return (
111 |
122 | )
123 | })
124 | FormControl.displayName = 'FormControl'
125 |
126 | const FormDescription = React.forwardRef<
127 | HTMLParagraphElement,
128 | React.HTMLAttributes
129 | >(({ className, ...props }, ref) => {
130 | const { formDescriptionId } = useFormField()
131 |
132 | return (
133 |
139 | )
140 | })
141 | FormDescription.displayName = 'FormDescription'
142 |
143 | const FormMessage = React.forwardRef<
144 | HTMLParagraphElement,
145 | React.HTMLAttributes
146 | >(({ className, children, ...props }, ref) => {
147 | const { error, formMessageId } = useFormField()
148 | const body = error ? String(error?.message) : children
149 |
150 | if (!body) {
151 | return null
152 | }
153 |
154 | return (
155 |
161 | {body}
162 |
163 | )
164 | })
165 | FormMessage.displayName = 'FormMessage'
166 |
167 | export {
168 | useFormField,
169 | Form,
170 | FormItem,
171 | FormLabel,
172 | FormControl,
173 | FormDescription,
174 | FormMessage,
175 | FormField,
176 | }
177 |
--------------------------------------------------------------------------------
/components/ui/input.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface InputProps
6 | extends React.InputHTMLAttributes {}
7 |
8 | const Input = React.forwardRef(
9 | ({ className, type, ...props }, ref) => {
10 | return (
11 |
20 | )
21 | }
22 | )
23 | Input.displayName = "Input"
24 |
25 | export { Input }
26 |
--------------------------------------------------------------------------------
/components/ui/label.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as LabelPrimitive from "@radix-ui/react-label"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const labelVariants = cva(
10 | "text-sm font-medium leading-none peer-disabled:cursor-not-allowed peer-disabled:opacity-70"
11 | )
12 |
13 | const Label = React.forwardRef<
14 | React.ElementRef,
15 | React.ComponentPropsWithoutRef &
16 | VariantProps
17 | >(({ className, ...props }, ref) => (
18 |
23 | ))
24 | Label.displayName = LabelPrimitive.Root.displayName
25 |
26 | export { Label }
27 |
--------------------------------------------------------------------------------
/components/ui/popover.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import * as React from 'react'
4 | import * as PopoverPrimitive from '@radix-ui/react-popover'
5 |
6 | import { cn } from '@/lib/utils'
7 |
8 | const Popover = PopoverPrimitive.Root
9 |
10 | const PopoverTrigger = PopoverPrimitive.Trigger
11 |
12 | const PopoverContent = React.forwardRef<
13 | React.ElementRef,
14 | React.ComponentPropsWithoutRef
15 | >(({ className, align = 'center', sideOffset = 4, ...props }, ref) => (
16 |
17 |
27 |
28 | ))
29 | PopoverContent.displayName = PopoverPrimitive.Content.displayName
30 |
31 | export { Popover, PopoverTrigger, PopoverContent }
32 |
--------------------------------------------------------------------------------
/components/ui/sonner.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { useTheme } from "next-themes"
4 | import { Toaster as Sonner } from "sonner"
5 |
6 | type ToasterProps = React.ComponentProps
7 |
8 | const Toaster = ({ ...props }: ToasterProps) => {
9 | const { theme = "system" } = useTheme()
10 |
11 | return (
12 |
28 | )
29 | }
30 |
31 | export { Toaster }
32 |
--------------------------------------------------------------------------------
/components/ui/switch.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SwitchPrimitives from "@radix-ui/react-switch"
5 |
6 | import { cn } from "@/lib/utils"
7 |
8 | const Switch = React.forwardRef<
9 | React.ElementRef,
10 | React.ComponentPropsWithoutRef
11 | >(({ className, ...props }, ref) => (
12 |
20 |
25 |
26 | ))
27 | Switch.displayName = SwitchPrimitives.Root.displayName
28 |
29 | export { Switch }
30 |
--------------------------------------------------------------------------------
/components/ui/table.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | const Table = React.forwardRef<
6 | HTMLTableElement,
7 | React.HTMLAttributes
8 | >(({ className, ...props }, ref) => (
9 |
16 | ))
17 | Table.displayName = "Table"
18 |
19 | const TableHeader = React.forwardRef<
20 | HTMLTableSectionElement,
21 | React.HTMLAttributes
22 | >(({ className, ...props }, ref) => (
23 |
24 | ))
25 | TableHeader.displayName = "TableHeader"
26 |
27 | const TableBody = React.forwardRef<
28 | HTMLTableSectionElement,
29 | React.HTMLAttributes
30 | >(({ className, ...props }, ref) => (
31 |
36 | ))
37 | TableBody.displayName = "TableBody"
38 |
39 | const TableFooter = React.forwardRef<
40 | HTMLTableSectionElement,
41 | React.HTMLAttributes
42 | >(({ className, ...props }, ref) => (
43 | tr]:last:border-b-0",
47 | className
48 | )}
49 | {...props}
50 | />
51 | ))
52 | TableFooter.displayName = "TableFooter"
53 |
54 | const TableRow = React.forwardRef<
55 | HTMLTableRowElement,
56 | React.HTMLAttributes
57 | >(({ className, ...props }, ref) => (
58 |
66 | ))
67 | TableRow.displayName = "TableRow"
68 |
69 | const TableHead = React.forwardRef<
70 | HTMLTableCellElement,
71 | React.ThHTMLAttributes
72 | >(({ className, ...props }, ref) => (
73 | |
81 | ))
82 | TableHead.displayName = "TableHead"
83 |
84 | const TableCell = React.forwardRef<
85 | HTMLTableCellElement,
86 | React.TdHTMLAttributes
87 | >(({ className, ...props }, ref) => (
88 | |
93 | ))
94 | TableCell.displayName = "TableCell"
95 |
96 | const TableCaption = React.forwardRef<
97 | HTMLTableCaptionElement,
98 | React.HTMLAttributes
99 | >(({ className, ...props }, ref) => (
100 |
105 | ))
106 | TableCaption.displayName = "TableCaption"
107 |
108 | export {
109 | Table,
110 | TableHeader,
111 | TableBody,
112 | TableFooter,
113 | TableHead,
114 | TableRow,
115 | TableCell,
116 | TableCaption,
117 | }
118 |
--------------------------------------------------------------------------------
/components/ui/textarea.tsx:
--------------------------------------------------------------------------------
1 | import * as React from "react"
2 |
3 | import { cn } from "@/lib/utils"
4 |
5 | export interface TextareaProps
6 | extends React.TextareaHTMLAttributes {}
7 |
8 | const Textarea = React.forwardRef(
9 | ({ className, ...props }, ref) => {
10 | return (
11 |
19 | )
20 | }
21 | )
22 | Textarea.displayName = "Textarea"
23 |
24 | export { Textarea }
25 |
--------------------------------------------------------------------------------
/components/ui/toaster.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import {
4 | Toast,
5 | ToastClose,
6 | ToastDescription,
7 | ToastProvider,
8 | ToastTitle,
9 | ToastViewport,
10 | } from "@/components/ui/toast"
11 | import { useToast } from "@/components/ui/use-toast"
12 |
13 | export function Toaster() {
14 | const { toasts } = useToast()
15 |
16 | return (
17 |
18 | {toasts.map(function ({ id, title, description, action, ...props }) {
19 | return (
20 |
21 |
22 | {title && {title}}
23 | {description && (
24 | {description}
25 | )}
26 |
27 | {action}
28 |
29 |
30 | )
31 | })}
32 |
33 |
34 | )
35 | }
36 |
--------------------------------------------------------------------------------
/components/ui/use-toast.ts:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | // Inspired by react-hot-toast library
4 | import * as React from "react"
5 |
6 | import type {
7 | ToastActionElement,
8 | ToastProps,
9 | } from "@/components/ui/toast"
10 |
11 | const TOAST_LIMIT = 1
12 | const TOAST_REMOVE_DELAY = 1000000
13 |
14 | type ToasterToast = ToastProps & {
15 | id: string
16 | title?: React.ReactNode
17 | description?: React.ReactNode
18 | action?: ToastActionElement
19 | }
20 |
21 | const actionTypes = {
22 | ADD_TOAST: "ADD_TOAST",
23 | UPDATE_TOAST: "UPDATE_TOAST",
24 | DISMISS_TOAST: "DISMISS_TOAST",
25 | REMOVE_TOAST: "REMOVE_TOAST",
26 | } as const
27 |
28 | let count = 0
29 |
30 | function genId() {
31 | count = (count + 1) % Number.MAX_SAFE_INTEGER
32 | return count.toString()
33 | }
34 |
35 | type ActionType = typeof actionTypes
36 |
37 | type Action =
38 | | {
39 | type: ActionType["ADD_TOAST"]
40 | toast: ToasterToast
41 | }
42 | | {
43 | type: ActionType["UPDATE_TOAST"]
44 | toast: Partial
45 | }
46 | | {
47 | type: ActionType["DISMISS_TOAST"]
48 | toastId?: ToasterToast["id"]
49 | }
50 | | {
51 | type: ActionType["REMOVE_TOAST"]
52 | toastId?: ToasterToast["id"]
53 | }
54 |
55 | interface State {
56 | toasts: ToasterToast[]
57 | }
58 |
59 | const toastTimeouts = new Map>()
60 |
61 | const addToRemoveQueue = (toastId: string) => {
62 | if (toastTimeouts.has(toastId)) {
63 | return
64 | }
65 |
66 | const timeout = setTimeout(() => {
67 | toastTimeouts.delete(toastId)
68 | dispatch({
69 | type: "REMOVE_TOAST",
70 | toastId: toastId,
71 | })
72 | }, TOAST_REMOVE_DELAY)
73 |
74 | toastTimeouts.set(toastId, timeout)
75 | }
76 |
77 | export const reducer = (state: State, action: Action): State => {
78 | switch (action.type) {
79 | case "ADD_TOAST":
80 | return {
81 | ...state,
82 | toasts: [action.toast, ...state.toasts].slice(0, TOAST_LIMIT),
83 | }
84 |
85 | case "UPDATE_TOAST":
86 | return {
87 | ...state,
88 | toasts: state.toasts.map((t) =>
89 | t.id === action.toast.id ? { ...t, ...action.toast } : t
90 | ),
91 | }
92 |
93 | case "DISMISS_TOAST": {
94 | const { toastId } = action
95 |
96 | // ! Side effects ! - This could be extracted into a dismissToast() action,
97 | // but I'll keep it here for simplicity
98 | if (toastId) {
99 | addToRemoveQueue(toastId)
100 | } else {
101 | state.toasts.forEach((toast) => {
102 | addToRemoveQueue(toast.id)
103 | })
104 | }
105 |
106 | return {
107 | ...state,
108 | toasts: state.toasts.map((t) =>
109 | t.id === toastId || toastId === undefined
110 | ? {
111 | ...t,
112 | open: false,
113 | }
114 | : t
115 | ),
116 | }
117 | }
118 | case "REMOVE_TOAST":
119 | if (action.toastId === undefined) {
120 | return {
121 | ...state,
122 | toasts: [],
123 | }
124 | }
125 | return {
126 | ...state,
127 | toasts: state.toasts.filter((t) => t.id !== action.toastId),
128 | }
129 | }
130 | }
131 |
132 | const listeners: Array<(state: State) => void> = []
133 |
134 | let memoryState: State = { toasts: [] }
135 |
136 | function dispatch(action: Action) {
137 | memoryState = reducer(memoryState, action)
138 | listeners.forEach((listener) => {
139 | listener(memoryState)
140 | })
141 | }
142 |
143 | type Toast = Omit
144 |
145 | function toast({ ...props }: Toast) {
146 | const id = genId()
147 |
148 | const update = (props: ToasterToast) =>
149 | dispatch({
150 | type: "UPDATE_TOAST",
151 | toast: { ...props, id },
152 | })
153 | const dismiss = () => dispatch({ type: "DISMISS_TOAST", toastId: id })
154 |
155 | dispatch({
156 | type: "ADD_TOAST",
157 | toast: {
158 | ...props,
159 | id,
160 | open: true,
161 | onOpenChange: (open) => {
162 | if (!open) dismiss()
163 | },
164 | },
165 | })
166 |
167 | return {
168 | id: id,
169 | dismiss,
170 | update,
171 | }
172 | }
173 |
174 | function useToast() {
175 | const [state, setState] = React.useState(memoryState)
176 |
177 | React.useEffect(() => {
178 | listeners.push(setState)
179 | return () => {
180 | const index = listeners.indexOf(setState)
181 | if (index > -1) {
182 | listeners.splice(index, 1)
183 | }
184 | }
185 | }, [state])
186 |
187 | return {
188 | ...state,
189 | toast,
190 | dismiss: (toastId?: string) => dispatch({ type: "DISMISS_TOAST", toastId }),
191 | }
192 | }
193 |
194 | export { useToast, toast }
195 |
--------------------------------------------------------------------------------
/emails/ResetPassword.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Body,
3 | Container,
4 | Head,
5 | Heading,
6 | Html,
7 | Img,
8 | Preview,
9 | Text,
10 | Tailwind,
11 | } from '@react-email/components'
12 | import * as React from 'react'
13 |
14 | interface ResetPasswordProps {
15 | company: string
16 | token: string
17 | clientName: string
18 | osName: string
19 | ip: string
20 | baseUrl?: string
21 | }
22 |
23 | export const ResetPassword = ({
24 | company,
25 | token,
26 | clientName,
27 | osName,
28 | ip,
29 | baseUrl,
30 | }: ResetPasswordProps) => (
31 |
42 |
43 |
44 | Password Reset Request
45 |
46 |
47 |
48 | Password Reset Request
49 |
50 |
51 |
52 | You recently requested to reset your password for your {company}{' '}
53 | account. Use the button below to reset it.{' '}
54 |
55 | This password reset is only valid for the next 10 minutes.
56 |
57 |
58 |
59 |
64 | Reset your password
65 |
66 |
67 |
68 |
69 | Didn't request this?
70 | {' '}
71 |
72 | For security, this request was received from {ip} a {osName} device
73 | using {clientName}. If you did not request a password reset, please
74 | ignore this email.
75 |
76 |
77 |
83 |
84 |
85 | Thanks,
86 |
87 | {company}
88 |
89 |
90 |
91 |
92 |
93 | If you’re having trouble with the button above, copy and paste the
94 | URL below into your web browser.
95 |
96 | {baseUrl}/auth/reset-password/{token}
97 |
98 |
99 |
100 |
101 |
102 |
103 | )
104 |
105 | export default ResetPassword
106 |
--------------------------------------------------------------------------------
/emails/VerifyAccount.tsx:
--------------------------------------------------------------------------------
1 | import {
2 | Body,
3 | Container,
4 | Head,
5 | Heading,
6 | Html,
7 | Img,
8 | Preview,
9 | Text,
10 | Tailwind,
11 | } from '@react-email/components'
12 | import * as React from 'react'
13 |
14 | interface VerifyAccountProps {
15 | company: string
16 | token: string
17 | clientName: string
18 | osName: string
19 | ip: string
20 | baseUrl?: string
21 | }
22 |
23 | export const VerifyAccount = ({
24 | company,
25 | token,
26 | clientName,
27 | osName,
28 | ip,
29 | baseUrl,
30 | }: VerifyAccountProps) => (
31 |
42 |
43 |
44 | Verification code
45 |
46 |
47 |
48 | Verification code
49 |
50 |
51 |
52 | Please verify your account to activate your {company} account.
53 |
54 |
55 |
56 | Click the button below to verify your email and complete account
57 | setup. This link will expire in 72 minutes.
58 |
59 |
60 |
65 | Verify Your Email
66 |
67 |
68 |
69 | If you did not request to verify this email, you can ignore this
70 | message. Your account will not be activated until you click the
71 | verification link.
72 |
73 |
74 |
75 |
76 | Didn't request this?
77 | {' '}
78 |
79 | For security, this request was received from {ip} a {osName} device
80 | using {clientName}. If you did not request a password reset, please
81 | ignore this email.
82 |
83 |
84 |
90 |
91 |
92 | Thanks,
93 |
94 | {company}
95 |
96 |
97 |
98 |
99 |
100 | If you’re having trouble with the button above, copy and paste the
101 | URL below into your web browser.
102 |
103 | {baseUrl}/auth/verification/{token}
104 |
105 |
106 |
107 |
108 |
109 |
110 | )
111 |
112 | export default VerifyAccount
113 |
--------------------------------------------------------------------------------
/global.d.ts:
--------------------------------------------------------------------------------
1 | import { NextApiRequest, NextApiResponse } from 'next'
2 | import { IUser } from './models/User'
3 | import { NextRequest } from 'next/server'
4 | import { PrismaClient } from '@prisma/client'
5 |
6 | declare global {
7 | var mongoose: any
8 | var prisma: PrismaClient
9 | namespace NodeJS {
10 | interface ProcessEnv {
11 | NODE_ENV: string
12 | MONGO_URI: string
13 | JWT_SECRET: string
14 | SMTP_SERVER: string
15 | SMTP_PORT: number
16 | SMTP_USER: string
17 | SMTP_KEY: string
18 | }
19 | }
20 | interface NextApiRequestExtended extends Request {
21 | user: {
22 | id: string
23 | name: string
24 | email: string
25 | role: string
26 | }
27 | url: string
28 | method: 'GET' | 'POST' | 'DELETE' | 'PUT'
29 | query: {
30 | limit: string
31 | page: string
32 | q: string
33 | id: string
34 | secret: string
35 | type: string
36 | option: string
37 | }
38 | }
39 | interface NextApiResponseExtended extends NextRequest {
40 | Data: any
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/hooks/useAuthorization.ts:
--------------------------------------------------------------------------------
1 | 'use client'
2 | import useUserInfoStore from '@/zustand/userStore'
3 | import { usePathname, useParams } from 'next/navigation'
4 |
5 | const useAuthorization = () => {
6 | const pathname = usePathname()
7 | const params = useParams()
8 |
9 | const { userInfo } = useUserInfoStore((state) => state)
10 |
11 | const param = () => {
12 | const keys = Object.keys(params)
13 | const values = Object.values(params)
14 | let param = pathname
15 | keys.forEach((k, i) => {
16 | // @ts-ignore
17 | param = param.replace(values[i], `[${k}]`)
18 | })
19 | return param
20 | }
21 |
22 | if (
23 | userInfo.id &&
24 | !userInfo?.routes?.map((g: { path: string }) => g.path).includes(param())
25 | ) {
26 | return '/'
27 | }
28 |
29 | if (!userInfo.token) {
30 | return `/auth/login?next=${pathname}`
31 | }
32 | }
33 |
34 | export default useAuthorization
35 |
--------------------------------------------------------------------------------
/hooks/useInterval.ts:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import useUserInfoStore from '@/zustand/userStore'
4 | import { useEffect } from 'react'
5 | import axios from 'axios'
6 | import { baseUrl } from '@/lib/setting'
7 |
8 | export default function useInterval() {
9 | const { userInfo, updateUserInfo } = useUserInfoStore((state) => state)
10 |
11 | const fetchData = async () => {
12 | const { data } = await axios.get(`${baseUrl}/api/users/${userInfo?.id}`)
13 |
14 | if (data?.routes?.length > 0 && data?.menu?.length > 0) {
15 | updateUserInfo({
16 | ...userInfo,
17 | ...data,
18 | })
19 |
20 | return await data
21 | }
22 | }
23 |
24 | useEffect(() => {
25 | const intervalId = setInterval(async () => {
26 | if (userInfo?.token) await fetchData()
27 | }, 18000) // check every 60 seconds
28 | return () => clearInterval(intervalId)
29 | // eslint-disable-next-line react-hooks/exhaustive-deps
30 | }, [])
31 |
32 | return { token: userInfo?.token }
33 | }
34 |
--------------------------------------------------------------------------------
/lib/auth.ts:
--------------------------------------------------------------------------------
1 | import jwt from 'jsonwebtoken'
2 | import { getEnvVariable } from './helpers'
3 | import { NextResponse } from 'next/server'
4 | import { prisma } from './prisma.db'
5 |
6 | interface JwtPayload {
7 | id: string
8 | iat: number
9 | exp: number
10 | }
11 |
12 | export const isAuth = async (req: any, params?: { id: string }) => {
13 | const { searchParams } = new URL(req.url)
14 | const pageSize = parseInt(searchParams.get('limit') as string) || 25
15 | if (pageSize > 300)
16 | throw {
17 | message: 'Page limit should be less than or equal to 300',
18 | status: 400,
19 | }
20 |
21 | let token: string = ''
22 |
23 | if (req.headers.get('Authorization')?.startsWith('Bearer')) {
24 | try {
25 | token = req.headers.get('Authorization')?.substring(7) as string
26 |
27 | const JWT_SECRET = getEnvVariable('JWT_SECRET')
28 |
29 | const decoded = jwt.verify(token, JWT_SECRET) as JwtPayload
30 |
31 | const userRole = await prisma.user.findFirst({
32 | where: {
33 | id: decoded.id,
34 | },
35 | include: {
36 | role: {
37 | select: {
38 | type: true,
39 | permissions: {
40 | select: {
41 | method: true,
42 | route: true,
43 | },
44 | },
45 | },
46 | },
47 | },
48 | })
49 |
50 | const accessToken = userRole?.accessToken
51 | if (!accessToken || accessToken !== token) {
52 | throw {
53 | message:
54 | 'Your session token is either invalid or has been revoked. Please log in again to continue',
55 | status: 401,
56 | }
57 | }
58 |
59 | req.user = {
60 | id: userRole?.id,
61 | name: userRole?.name,
62 | email: userRole?.email,
63 | mobile: userRole?.mobile,
64 | role: userRole?.role.type,
65 | }
66 |
67 | const permissions = userRole?.role?.permissions
68 |
69 | let { url, method } = req
70 | url = `/api/${url.split('/api/')[1]}`
71 |
72 | if (params?.id) {
73 | // api/path/:id
74 | const removedIDFromURL = url.replace(params?.id, ':id')
75 | url = removedIDFromURL.split('?')?.[0]
76 | }
77 |
78 | if (url.includes('?')) {
79 | // api/path
80 | url = url.split('?')?.[0]
81 | }
82 |
83 | if (
84 | permissions?.find(
85 | (permission) =>
86 | permission.route === url && permission.method === method
87 | )
88 | ) {
89 | url = req.url
90 | return NextResponse.next()
91 | }
92 |
93 | throw {
94 | message: 'You do not have permission to access this route',
95 | status: 403,
96 | }
97 | } catch ({ message }: any) {
98 | throw {
99 | message: message || 'Not authorized, token failed',
100 | status: 401,
101 | }
102 | }
103 | }
104 | if (!token) {
105 | throw {
106 | message: 'Not authorized, no token',
107 | status: 401,
108 | }
109 | }
110 | }
111 |
--------------------------------------------------------------------------------
/lib/capitalize.ts:
--------------------------------------------------------------------------------
1 | export const Capitalize = (str: string) => {
2 | str = str?.replace(/([A-Z])/g, ' $1')
3 | return str?.charAt(0)?.toUpperCase() + str?.slice(1)?.toLowerCase()
4 | }
5 |
--------------------------------------------------------------------------------
/lib/currency.ts:
--------------------------------------------------------------------------------
1 | export const currency = (amount: number, decimal = 3) => {
2 | if (!amount) return '$0.00'
3 | let amountWithDecimal = amount
4 | .toString()
5 | .substring(0, amount.toString().indexOf('.') + decimal)
6 |
7 | const formatter = new Intl.NumberFormat('en-US', {
8 | style: 'currency',
9 | currency: 'USD',
10 | maximumFractionDigits: 20,
11 | })
12 |
13 | let truncatedValue = formatter.format(amount)
14 |
15 | amountWithDecimal = amountWithDecimal.split('.')[1]
16 | amountWithDecimal = amountWithDecimal ? amountWithDecimal : '00'
17 | truncatedValue = truncatedValue.split('.')[0]
18 |
19 | return `${truncatedValue}.${amountWithDecimal}`
20 | }
21 |
--------------------------------------------------------------------------------
/lib/dateTime.ts:
--------------------------------------------------------------------------------
1 | import dayjs from 'dayjs'
2 |
3 | import utc from 'dayjs/plugin/utc'
4 | import timezone from 'dayjs/plugin/timezone'
5 | import 'dayjs/locale/es' // Replace 'en' with your desired locale
6 |
7 | // Load the Day.js plugins
8 | dayjs.extend(utc)
9 | dayjs.extend(timezone)
10 |
11 | const DateTime = dayjs
12 |
13 | export default DateTime
14 |
--------------------------------------------------------------------------------
/lib/email-helper.ts:
--------------------------------------------------------------------------------
1 | import nodemailer from 'nodemailer'
2 |
3 | type Payload = {
4 | to: string
5 | subject: string
6 | html: any
7 | }
8 |
9 | const smtpSettings = {
10 | host: process.env.SMTP_SERVER,
11 | port: process.env.SMTP_PORT,
12 | secure: true,
13 | auth: {
14 | user: process.env.SMTP_USER,
15 | pass: process.env.SMTP_KEY,
16 | },
17 | }
18 |
19 | export const handleEmailFire = async (data: Payload) => {
20 | const transporter = nodemailer.createTransport(smtpSettings)
21 |
22 | return await transporter.sendMail({
23 | from: process.env.SMTP_USER,
24 | ...data,
25 | })
26 | }
27 |
--------------------------------------------------------------------------------
/lib/getDevice.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import DeviceDetector from 'device-detector-js'
3 |
4 | export const getDevice = async ({
5 | req,
6 | hasIp = false,
7 | }: {
8 | req: Request
9 | hasIp?: boolean
10 | }) => {
11 | try {
12 | const deviceDetector = new DeviceDetector()
13 | const device = deviceDetector.parse(
14 | req.headers.get('user-agent') || ''
15 | ) as any
16 |
17 | const {
18 | client: { name: clientName },
19 | os: { name: osName },
20 | } = device
21 |
22 | const host = req.headers.get('host') // localhost:3000
23 | const protocol = req.headers.get('x-forwarded-proto') // http
24 | const url = `${protocol}://${host}`
25 |
26 | let ip = ''
27 | if (hasIp) {
28 | const { data } = await axios.get('https://api.ipify.org/?format=json')
29 |
30 | ip = data?.ip
31 | }
32 |
33 | return {
34 | clientName,
35 | osName,
36 | ip,
37 | url,
38 | }
39 | } catch (error: any) {
40 | throw {
41 | message: error?.message,
42 | status: 500,
43 | }
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/lib/helpers.ts:
--------------------------------------------------------------------------------
1 | import { NextResponse } from 'next/server'
2 | import crypto from 'crypto'
3 | import bcrypt from 'bcryptjs'
4 | import jwt from 'jsonwebtoken'
5 | import { Prisma } from '@prisma/client'
6 | import { join } from 'path'
7 |
8 | export function getEnvVariable(key: string): string {
9 | const value = process.env[key]
10 |
11 | if (!value || value.length === 0) {
12 | console.log(`The environment variable ${key} is not set.`)
13 | throw new Error(`The environment variable ${key} is not set.`)
14 | }
15 |
16 | return value
17 | }
18 |
19 | export function getErrorResponse(
20 | error: string | null = null,
21 | status: number = 500,
22 | prismaError?: Prisma.PrismaClientKnownRequestError,
23 | req?: Request & { temporaryBody?: any }
24 | ) {
25 | if (prismaError) {
26 | switch (prismaError.code) {
27 | case 'P1001':
28 | error = 'Target database connection error'
29 | status = 500
30 | break
31 | case 'P1002':
32 | error = 'Query execution error'
33 | status = 500
34 | break
35 | case 'P1003':
36 | error = 'Prisma Client generation error'
37 | status = 500
38 | break
39 | case 'P1009':
40 | error = 'Validation error'
41 | status = 400
42 | break
43 | case 'P2000':
44 | error = 'Database schema has changed'
45 | status = 500
46 | break
47 | case 'P2002':
48 | error = 'A unique constraint violation occurred.'
49 | status = 400
50 | break
51 | case 'P2003':
52 | error = `Foreign key constraint failed on the field: ${prismaError.meta?.field_name}`
53 | status = 400
54 | break
55 | case 'P2025':
56 | error =
57 | 'An operation failed because it depends on one or more records that were required but not found.'
58 | status = 404
59 | break
60 | default:
61 | error = error || 'An unexpected database error occurred.'
62 | status = status || 500
63 | break
64 | }
65 | }
66 |
67 | // ===== Start only for bank =====
68 | const url = req?.url
69 |
70 | if (url && url.includes('/api/bank')) {
71 | // help full log
72 | console.log({
73 | req,
74 | error,
75 | body: req?.temporaryBody,
76 | date: new Date().toISOString(),
77 | })
78 |
79 | return new NextResponse(
80 | JSON.stringify({
81 | ...(url?.includes('login')
82 | ? {
83 | token: 'Un authorized',
84 | status: false,
85 | }
86 | : {
87 | message: 'Failed',
88 | status: false,
89 | }),
90 | }),
91 | {
92 | status,
93 | headers: { 'Content-Type': 'application/json' },
94 | }
95 | )
96 | }
97 | // ===== End only for bank =====
98 |
99 | return new NextResponse(
100 | JSON.stringify({
101 | status: status < 500 ? 'fail' : 'error',
102 | error: error ? error : null,
103 | }),
104 | {
105 | status,
106 | headers: { 'Content-Type': 'application/json' },
107 | }
108 | )
109 | }
110 | export async function matchPassword({
111 | enteredPassword,
112 | password,
113 | }: {
114 | enteredPassword: string
115 | password: string
116 | }) {
117 | return await bcrypt.compare(enteredPassword, password)
118 | }
119 |
120 | export async function encryptPassword({ password }: { password: string }) {
121 | const salt = bcrypt.genSaltSync(10)
122 | return bcrypt.hashSync(password, salt)
123 | }
124 |
125 | export async function getResetPasswordToken(minutes = 10) {
126 | const resetToken = crypto.randomBytes(20).toString('hex')
127 |
128 | return {
129 | resetToken,
130 | resetPasswordToken: crypto
131 | .createHash('sha256')
132 | .update(resetToken)
133 | .digest('hex'),
134 | resetPasswordExpire: Date.now() + minutes * (60 * 1000), // Ten Minutes
135 | }
136 | }
137 |
138 | export async function generateToken(id: string) {
139 | const JWT_SECRET = getEnvVariable('JWT_SECRET')
140 | return jwt.sign({ id }, JWT_SECRET, {
141 | expiresIn: '1d',
142 | })
143 | }
144 |
145 | export function getBackupDirectory(): string {
146 | const backupPath = process.env.BACKUP_PATH
147 | if (!backupPath) {
148 | console.warn(
149 | 'BACKUP_PATH environment variable not set. Using default ./db_backups'
150 | )
151 | return join(process.cwd(), 'db_backups') // Fallback, less ideal in Docker
152 | }
153 | return backupPath // e.g., /app/db_backups (inside container)
154 | }
155 |
--------------------------------------------------------------------------------
/lib/meta.ts:
--------------------------------------------------------------------------------
1 | import type { Metadata } from 'next'
2 |
3 | interface Props {
4 | title: string
5 | description: string
6 | openGraphImage: string
7 | author?: string
8 | canonical?: string
9 | }
10 |
11 | const domain = 'https://ahmedibra.com'
12 |
13 | export default function meta({
14 | title,
15 | description,
16 | openGraphImage,
17 | author = 'Ahmed Ibrahim',
18 | ...props
19 | }: Props & Metadata) {
20 | return {
21 | title,
22 | description,
23 | authors: {
24 | name: author,
25 | },
26 | author: author,
27 | creator: author,
28 | publisher: author,
29 | metadataBase: new URL(domain),
30 | generator: 'Next.js',
31 | applicationName: 'Ahmed Ibrahim',
32 | referrer: 'origin-when-cross-origin' as any,
33 | robots: {
34 | index: true,
35 | follow: true,
36 | nocache: false,
37 | noimageindex: false,
38 | 'max-video-preview': -1,
39 | 'max-snippet': -1,
40 | },
41 | alternates: {
42 | canonical: props?.canonical || '/',
43 | languages: {
44 | 'en-US': '/en-US',
45 | },
46 | },
47 | openGraph: {
48 | title,
49 | description,
50 | url: domain,
51 | siteName: 'TopTayo',
52 | images: [
53 | {
54 | url: openGraphImage,
55 | width: 1200,
56 | height: 630,
57 | },
58 | ],
59 | locale: 'en_US',
60 | type: 'website',
61 | },
62 | icons: {
63 | icon: '/logo.svg',
64 | shortcut: '/logo.svg',
65 | apple: '/logo.svg',
66 | other: {
67 | rel: 'apple-touch-icon-precomposed',
68 | url: '/logo.svg',
69 | },
70 | },
71 | twitter: {
72 | card: 'summary_large_image',
73 | title,
74 | description,
75 | siteId: '1467726470533754880',
76 | creator: author,
77 | creatorId: '1467726470533754880',
78 | images: {
79 | url: openGraphImage,
80 | alt: title,
81 | },
82 | app: {
83 | name: 'twitter_app',
84 | id: {
85 | iphone: 'twitter_app://iphone',
86 | ipad: 'twitter_app://ipad',
87 | googleplay: 'twitter_app://googleplay',
88 | },
89 | url: {
90 | iphone: 'https://iphone_url',
91 | ipad: 'https://ipad_url',
92 | },
93 | },
94 | },
95 | verification: {
96 | google: 'google',
97 | yandex: 'yandex',
98 | yahoo: 'yahoo',
99 | other: {
100 | me: ['info@ahmedibra.com', 'ahmaat19@gmail.com'],
101 | },
102 | },
103 | appleWebApp: {
104 | title,
105 | startupImage: [
106 | '/logo.svg',
107 | {
108 | url: '/logo.svg',
109 | media: '(device-width: 768px) and (device-height: 1024px)',
110 | },
111 | ],
112 | },
113 | web: {
114 | url: domain,
115 | should_fallback: true,
116 | },
117 | keywords:
118 | props?.keywords ||
119 | `Ahmed Ibrahim, Ahmed Ibrahim Samow, Next.js, Web & Mobile Development, App Development, Design Agency, Web Design, eCommerce, Websites, Web Solutions, Business Growth, Software Development, Custom Software Development, Custom Web Design, Somalia, Somali Web Design, Somali Web Development, SEO Optimization, Marketing, Branding, USSD, EVC Plus, Web Development, scalable web applications, responsive web applications, Mobile App Development, cross-platform mobile applications, Full Stack Development, end-to-end web solutions, front-end and back-end development, API Development, robust APIs, different technologies, Database Management, SQL, NoSQL, PostgreSQL, MongoDB, optimize databases, Server Configuration and Deployment, server setup, app deployment, Docker, AWS, DigitalOcean, UI/UX Design and Development, visually appealing UI/UX, interactive experiences, E-commerce Solutions, secure e-commerce platforms, scalable e-commerce, Custom Software Development, tailor-made software solutions, specific business requirements, Code Review and Refactoring, code review, code refactoring, quality improvement, dropshipping, toptayo, top-tayo, toptayo.com, https://toptayo.com`,
120 | ...props,
121 | }
122 | }
123 |
--------------------------------------------------------------------------------
/lib/numberFormat.ts:
--------------------------------------------------------------------------------
1 | export const numberFormat = (amount: number) => {
2 | const formatter = new Intl.NumberFormat('en-US', {
3 | style: 'decimal',
4 | minimumFractionDigits: 0,
5 | })
6 | return formatter.format(amount)
7 | }
8 |
--------------------------------------------------------------------------------
/lib/prisma.db.ts:
--------------------------------------------------------------------------------
1 | // @ts-ignore
2 | import { PrismaClient, QueryMode } from '@prisma/client'
3 |
4 | let prisma: PrismaClient
5 |
6 | if (process.env.NODE_ENV === 'production') {
7 | prisma = new PrismaClient()
8 | } else {
9 | if (!global.prisma) {
10 | global.prisma = new PrismaClient()
11 | }
12 |
13 | prisma = global.prisma
14 | }
15 |
16 | export { prisma, QueryMode }
17 |
--------------------------------------------------------------------------------
/lib/provider.tsx:
--------------------------------------------------------------------------------
1 | 'use client'
2 |
3 | import React from 'react'
4 | import { QueryClientProvider, QueryClient } from '@tanstack/react-query'
5 | import { ReactQueryDevtools } from '@tanstack/react-query-devtools'
6 |
7 | function Providers({ children }: React.PropsWithChildren) {
8 | const [client] = React.useState(
9 | new QueryClient({ defaultOptions: { queries: { staleTime: 5000 } } })
10 | )
11 |
12 | return (
13 |
14 | {children}
15 |
16 |
17 | )
18 | }
19 |
20 | export default Providers
21 |
--------------------------------------------------------------------------------
/lib/setting.ts:
--------------------------------------------------------------------------------
1 | const BASE_URL = process.env.NEXT_PUBLIC_API_URL
2 |
3 | export const baseUrl =
4 | process.env.NODE_ENV === 'production'
5 | ? `${BASE_URL}/`
6 | : 'http://localhost:3000/'
7 |
8 | export const siteName = 'Boilerplate'
9 |
10 | export const logo = '/logo.svg'
11 |
12 | export const siteDescription =
13 | 'Boilerplate is a starter template for building full-stack applications with Next.js, Prisma, and Tailwind CSS.'
14 |
--------------------------------------------------------------------------------
/lib/utils.ts:
--------------------------------------------------------------------------------
1 | import { type ClassValue, clsx } from "clsx"
2 | import { twMerge } from "tailwind-merge"
3 |
4 | export function cn(...inputs: ClassValue[]) {
5 | return twMerge(clsx(inputs))
6 | }
7 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | const path = require('path');
2 |
3 | /** @type {import('next').NextConfig} */
4 | const nextConfig = {
5 | reactStrictMode: false,
6 | images: {
7 | remotePatterns: [
8 | {
9 | protocol: 'https',
10 | hostname: 'ahmedibra.com'
11 | },
12 | {
13 | protocol: 'https',
14 | hostname: 'github.com'
15 | },
16 | {
17 | protocol: 'https',
18 | hostname: 'ui-avatars.com'
19 | }
20 | ]
21 | }
22 | }
23 |
24 | module.exports = nextConfig
25 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "next-ts-boilerplate",
3 | "version": "0.1.0",
4 | "private": true,
5 | "scripts": {
6 | "dev": "next dev --turbopack",
7 | "postinstall": "npx prisma generate",
8 | "build": "npx prisma generate && next build",
9 | "start": "next start",
10 | "server:dev": "nodemon server/index.js",
11 | "server:start": "next build && node server/index.js",
12 | "lint": "next lint",
13 | "migrate": "npx prisma migrate dev",
14 | "generate": "npx prisma generate",
15 | "studio": "npx prisma studio",
16 | "email": "email dev"
17 | },
18 | "dependencies": {
19 | "@aws-sdk/client-s3": "^3.815.0",
20 | "@hookform/resolvers": "^5.0.1",
21 | "@prisma/client": "^6.8.2",
22 | "@radix-ui/react-alert-dialog": "^1.1.14",
23 | "@radix-ui/react-avatar": "^1.1.10",
24 | "@radix-ui/react-checkbox": "^1.3.2",
25 | "@radix-ui/react-dialog": "^1.1.14",
26 | "@radix-ui/react-dropdown-menu": "^2.1.15",
27 | "@radix-ui/react-label": "^2.1.7",
28 | "@radix-ui/react-menubar": "^1.1.15",
29 | "@radix-ui/react-popover": "^1.1.14",
30 | "@radix-ui/react-select": "^2.2.5",
31 | "@radix-ui/react-slot": "^1.2.3",
32 | "@radix-ui/react-switch": "^1.2.5",
33 | "@radix-ui/react-toast": "^1.2.14",
34 | "@react-email/components": "0.0.41",
35 | "@react-email/render": "^1.1.2",
36 | "@tanstack/react-query": "^5.76.1",
37 | "@tanstack/react-query-devtools": "^5.76.1",
38 | "@tanstack/react-table": "^8.21.3",
39 | "@types/node": "22.13.14",
40 | "@types/react": "19.1.5",
41 | "@types/react-dom": "19.1.5",
42 | "@uidotdev/usehooks": "^2.4.1",
43 | "autoprefixer": "^10.4.21",
44 | "axios": "^1.9.0",
45 | "bcrypt": "^6.0.0",
46 | "bcryptjs": "^3.0.2",
47 | "class-variance-authority": "^0.7.1",
48 | "clsx": "^2.1.1",
49 | "cmdk": "^1.1.1",
50 | "cors": "^2.8.5",
51 | "dayjs": "^1.11.13",
52 | "device-detector-js": "^3.0.3",
53 | "eslint": "^9.27.0",
54 | "eslint-config-next": "15.3.2",
55 | "express": "^5.1.0",
56 | "jsonwebtoken": "^9.0.2",
57 | "jszip": "^3.10.1",
58 | "lucide-react": "^0.511.0",
59 | "moment": "^2.30.1",
60 | "next": "15.3.2",
61 | "next-themes": "^0.4.6",
62 | "nodemailer": "^7.0.3",
63 | "react": "19.1.0",
64 | "react-confirm-alert": "^3.0.6",
65 | "react-dom": "19.1.0",
66 | "react-email": "^4.0.15",
67 | "react-hook-form": "^7.56.4",
68 | "react-icons": "^5.5.0",
69 | "react-loading-icons": "^1.1.0",
70 | "react-top-loading-bar": "^3.0.2",
71 | "sharp": "^0.34.2",
72 | "socket.io": "^4.8.1",
73 | "sonner": "^2.0.3",
74 | "tailwind-merge": "^3.3.0",
75 | "tailwindcss-animate": "^1.0.7",
76 | "tw-animate-css": "^1.3.0",
77 | "typescript": "5.8.3",
78 | "use-debounce": "^10.0.4",
79 | "vaul": "^1.1.2",
80 | "zip-a-folder": "^3.1.9",
81 | "zod": "^3.25.20",
82 | "zustand": "^5.0.5"
83 | },
84 | "devDependencies": {
85 | "@types/node": "^22",
86 | "@tailwindcss/postcss": "^4",
87 | "@types/jsonwebtoken": "^9.0.9",
88 | "@types/nodemailer": "^6.4.17",
89 | "prisma": "^6.8.2",
90 | "tailwindcss": "^4"
91 | },
92 | "pnpm": {
93 | "overrides": {
94 | "@types/react": "19.1.5",
95 | "@types/react-dom": "19.1.5"
96 | }
97 | }
98 | }
99 |
--------------------------------------------------------------------------------
/postcss.config.mjs:
--------------------------------------------------------------------------------
1 | const config = {
2 | plugins: ['@tailwindcss/postcss'],
3 | }
4 |
5 | export default config
6 |
--------------------------------------------------------------------------------
/prisma/migrations/20250331142435_nanoid/migration.sql:
--------------------------------------------------------------------------------
1 | CREATE EXTENSION IF NOT EXISTS pgcrypto;
2 |
3 | CREATE OR REPLACE FUNCTION nanoid(size int DEFAULT 21)
4 | RETURNS text AS $$
5 | DECLARE
6 | id text := '';
7 | i int := 0;
8 | urlAlphabet char(64) := 'ModuleSymbhasOwnPr-0123456789ABCDEFGHNRVfgctiUvz_KqYTJkLxpZXIjQW';
9 | bytes bytea := gen_random_bytes(size);
10 | byte int;
11 | pos int;
12 | BEGIN
13 | WHILE i < size LOOP
14 | byte := get_byte(bytes, i);
15 | pos := (byte & 63) + 1; -- + 1 because substr starts at 1 for some reason
16 | id := id || substr(urlAlphabet, pos, 1);
17 | i = i + 1;
18 | END LOOP;
19 | RETURN id;
20 | END
21 | $$ LANGUAGE PLPGSQL STABLE;
22 |
23 |
24 | -- CreateEnum
25 | CREATE TYPE "Method" AS ENUM ('GET', 'POST', 'PUT', 'DELETE');
26 |
27 | -- CreateEnum
28 | CREATE TYPE "Status" AS ENUM ('ACTIVE', 'INACTIVE', 'PENDING_VERIFICATION');
29 |
30 | -- CreateTable
31 | CREATE TABLE "users" (
32 | "id" VARCHAR(21) NOT NULL DEFAULT nanoid(),
33 | "email" TEXT NOT NULL,
34 | "name" TEXT NOT NULL,
35 | "image" TEXT,
36 | "mobile" INTEGER,
37 | "address" TEXT,
38 | "bio" TEXT,
39 | "password" TEXT NOT NULL,
40 | "status" "Status" NOT NULL DEFAULT 'PENDING_VERIFICATION',
41 | "resetPasswordToken" TEXT,
42 | "resetPasswordExpire" BIGINT,
43 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
44 | "updatedAt" TIMESTAMP(3) NOT NULL,
45 | "roleId" TEXT NOT NULL,
46 |
47 | CONSTRAINT "users_pkey" PRIMARY KEY ("id")
48 | );
49 |
50 | -- CreateTable
51 | CREATE TABLE "roles" (
52 | "id" VARCHAR(21) NOT NULL DEFAULT nanoid(),
53 | "name" TEXT NOT NULL,
54 | "type" TEXT NOT NULL,
55 | "description" TEXT,
56 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
57 | "updatedAt" TIMESTAMP(3) NOT NULL,
58 |
59 | CONSTRAINT "roles_pkey" PRIMARY KEY ("id")
60 | );
61 |
62 | -- CreateTable
63 | CREATE TABLE "permissions" (
64 | "id" VARCHAR(21) NOT NULL DEFAULT nanoid(),
65 | "name" TEXT NOT NULL,
66 | "method" "Method" NOT NULL,
67 | "route" TEXT NOT NULL,
68 | "description" TEXT,
69 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
70 | "updatedAt" TIMESTAMP(3) NOT NULL,
71 |
72 | CONSTRAINT "permissions_pkey" PRIMARY KEY ("id")
73 | );
74 |
75 | -- CreateTable
76 | CREATE TABLE "client_permissions" (
77 | "id" VARCHAR(21) NOT NULL DEFAULT nanoid(),
78 | "name" TEXT NOT NULL,
79 | "sort" INTEGER NOT NULL,
80 | "menu" TEXT NOT NULL,
81 | "path" TEXT NOT NULL,
82 | "description" TEXT,
83 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
84 | "updatedAt" TIMESTAMP(3) NOT NULL,
85 |
86 | CONSTRAINT "client_permissions_pkey" PRIMARY KEY ("id")
87 | );
88 |
89 | -- CreateTable
90 | CREATE TABLE "_PermissionToRole" (
91 | "A" VARCHAR(21) NOT NULL,
92 | "B" VARCHAR(21) NOT NULL,
93 |
94 | CONSTRAINT "_PermissionToRole_AB_pkey" PRIMARY KEY ("A","B")
95 | );
96 |
97 | -- CreateTable
98 | CREATE TABLE "_ClientPermissionToRole" (
99 | "A" VARCHAR(21) NOT NULL,
100 | "B" VARCHAR(21) NOT NULL,
101 |
102 | CONSTRAINT "_ClientPermissionToRole_AB_pkey" PRIMARY KEY ("A","B")
103 | );
104 |
105 | -- CreateIndex
106 | CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
107 |
108 | -- CreateIndex
109 | CREATE UNIQUE INDEX "roles_name_key" ON "roles"("name");
110 |
111 | -- CreateIndex
112 | CREATE UNIQUE INDEX "roles_type_key" ON "roles"("type");
113 |
114 | -- CreateIndex
115 | CREATE UNIQUE INDEX "permissions_method_route_key" ON "permissions"("method", "route");
116 |
117 | -- CreateIndex
118 | CREATE UNIQUE INDEX "client_permissions_name_key" ON "client_permissions"("name");
119 |
120 | -- CreateIndex
121 | CREATE UNIQUE INDEX "client_permissions_path_key" ON "client_permissions"("path");
122 |
123 | -- CreateIndex
124 | CREATE INDEX "_PermissionToRole_B_index" ON "_PermissionToRole"("B");
125 |
126 | -- CreateIndex
127 | CREATE INDEX "_ClientPermissionToRole_B_index" ON "_ClientPermissionToRole"("B");
128 |
129 | -- AddForeignKey
130 | ALTER TABLE "users" ADD CONSTRAINT "users_roleId_fkey" FOREIGN KEY ("roleId") REFERENCES "roles"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
131 |
132 | -- AddForeignKey
133 | ALTER TABLE "_PermissionToRole" ADD CONSTRAINT "_PermissionToRole_A_fkey" FOREIGN KEY ("A") REFERENCES "permissions"("id") ON DELETE CASCADE ON UPDATE CASCADE;
134 |
135 | -- AddForeignKey
136 | ALTER TABLE "_PermissionToRole" ADD CONSTRAINT "_PermissionToRole_B_fkey" FOREIGN KEY ("B") REFERENCES "roles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
137 |
138 | -- AddForeignKey
139 | ALTER TABLE "_ClientPermissionToRole" ADD CONSTRAINT "_ClientPermissionToRole_A_fkey" FOREIGN KEY ("A") REFERENCES "client_permissions"("id") ON DELETE CASCADE ON UPDATE CASCADE;
140 |
141 | -- AddForeignKey
142 | ALTER TABLE "_ClientPermissionToRole" ADD CONSTRAINT "_ClientPermissionToRole_B_fkey" FOREIGN KEY ("B") REFERENCES "roles"("id") ON DELETE CASCADE ON UPDATE CASCADE;
143 |
--------------------------------------------------------------------------------
/prisma/migrations/20250522135703_add_accesstoken_to_user/migration.sql:
--------------------------------------------------------------------------------
1 | -- AlterTable
2 | ALTER TABLE "users" ADD COLUMN "accessToken" TEXT;
3 |
--------------------------------------------------------------------------------
/prisma/migrations/migration_lock.toml:
--------------------------------------------------------------------------------
1 | # Please do not edit this file manually
2 | # It should be added in your version-control system (e.g., Git)
3 | provider = "postgresql"
4 |
--------------------------------------------------------------------------------
/prisma/schema.prisma:
--------------------------------------------------------------------------------
1 | generator client {
2 | provider = "prisma-client-js"
3 | }
4 |
5 | datasource db {
6 | provider = "postgresql"
7 | url = env("DATABASE_URL")
8 | }
9 |
10 | model User {
11 | id String @id @default(dbgenerated("nanoid()")) @db.VarChar(21)
12 | email String @unique
13 | name String
14 | image String?
15 | mobile Int?
16 | address String?
17 | bio String?
18 | password String
19 | status Status @default(PENDING_VERIFICATION)
20 | resetPasswordToken String?
21 | resetPasswordExpire BigInt?
22 | accessToken String?
23 | createdAt DateTime @default(now())
24 | updatedAt DateTime @updatedAt
25 |
26 | role Role @relation(fields: [roleId], references: [id], onDelete: Restrict)
27 | roleId String
28 |
29 | @@map("users")
30 | }
31 |
32 | model Role {
33 | id String @id @default(dbgenerated("nanoid()")) @db.VarChar(21)
34 | name String @unique
35 | type String @unique
36 | description String?
37 | createdAt DateTime @default(now())
38 | updatedAt DateTime @updatedAt
39 |
40 | users User[]
41 | permissions Permission[]
42 | clientPermissions ClientPermission[]
43 |
44 | @@map("roles")
45 | }
46 |
47 | model Permission {
48 | id String @id @default(dbgenerated("nanoid()")) @db.VarChar(21)
49 | name String
50 | method Method
51 | route String
52 | description String?
53 | createdAt DateTime @default(now())
54 | updatedAt DateTime @updatedAt
55 |
56 | role Role[]
57 |
58 | @@unique([method, route])
59 | @@map("permissions")
60 | }
61 |
62 | model ClientPermission {
63 | id String @id @default(dbgenerated("nanoid()")) @db.VarChar(21)
64 | name String @unique
65 | sort Int
66 | menu String
67 | path String @unique
68 | description String?
69 | createdAt DateTime @default(now())
70 | updatedAt DateTime @updatedAt
71 |
72 | role Role[]
73 |
74 | @@map("client_permissions")
75 | }
76 |
77 | enum Method {
78 | GET
79 | POST
80 | PUT
81 | DELETE
82 | }
83 |
84 | enum Status {
85 | ACTIVE
86 | INACTIVE
87 | PENDING_VERIFICATION
88 | }
89 |
--------------------------------------------------------------------------------
/public/next.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/public/vercel.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/server/index.js:
--------------------------------------------------------------------------------
1 | const express = require('express')
2 | const path = require('path')
3 | const http = require('http')
4 | const next = require('next')
5 | const socketio = require('socket.io')
6 | const cors = require('cors')
7 | const socketIO = require('./socket')
8 |
9 | const port = parseInt(process.env.PORT || '3000', 10)
10 | const dev = process.env.NODE_ENV !== 'production'
11 | const nextApp = next({ dev })
12 | const nextHandler = nextApp.getRequestHandler()
13 |
14 | nextApp.prepare().then(async () => {
15 | let app = express()
16 |
17 | app.use(cors())
18 |
19 | const server = http.createServer(app)
20 | const io = new socketio.Server({
21 | cors: {
22 | origin: '*',
23 | methods: 'GET,POST',
24 | },
25 | })
26 | io.attach(server)
27 |
28 | app.use(express.static(path.join(__dirname, './public')))
29 | app.use('/_next', express.static(path.join(__dirname, './.next')))
30 |
31 | io.on('connection', (socket) => {
32 | console.log('A user connected')
33 |
34 | socketIO(socket)
35 |
36 | })
37 |
38 | app.all('*', (req, res) => nextHandler(req, res))
39 |
40 | server.listen(port, () => {
41 | console.log(`> Ready on http://localhost:${port}`)
42 | })
43 | })
44 |
45 |
--------------------------------------------------------------------------------
/server/socket.js:
--------------------------------------------------------------------------------
1 | /**
2 | * @typedef {import("socket.io").Socket} Socket
3 | */
4 |
5 | /**
6 | * Socket IO handler
7 | * @param {Socket} socket - The socket instance
8 | * @returns {void}
9 | */
10 | const socketIO = (socket) => {
11 | // socket.emit('requestHormuudUSSDCode', {
12 | // id: '1',
13 | // code: '*123#',
14 | // // code: '*727*1845822*20*3007#',
15 | // model: 'SM-A556E',
16 | // });
17 |
18 | socket.on('responseHormuudUSSDCode', (data) => {
19 | console.log('🚀 ', data);
20 | });
21 |
22 |
23 |
24 |
25 | };
26 |
27 | module.exports = socketIO;
--------------------------------------------------------------------------------
/services/api.tsx:
--------------------------------------------------------------------------------
1 | import axios from 'axios'
2 | import {
3 | QueryClient,
4 | useMutation,
5 | useQuery,
6 | UseQueryResult,
7 | UseMutationResult,
8 | UseInfiniteQueryResult,
9 | useInfiniteQuery,
10 | } from '@tanstack/react-query'
11 | import useUserInfoStore from '@/zustand/userStore'
12 | import { baseUrl } from '@/lib/setting'
13 |
14 | export const api = async (method: Method, url: string, obj: any = {}) => {
15 | const logout = useUserInfoStore.getState().logout
16 |
17 | const token = useUserInfoStore.getState().userInfo.token
18 |
19 | const config = {
20 | headers: {
21 | Authorization: `Bearer ${token}`,
22 | },
23 | }
24 |
25 | try {
26 | let response
27 | let fullUrl = `${baseUrl}/api/${url}`
28 |
29 | switch (method) {
30 | case 'GET':
31 | response = await axios.get(fullUrl, config)
32 | break
33 | case 'POST':
34 | response = await axios.post(fullUrl, obj, config)
35 | break
36 | case 'PUT':
37 | response = await axios.put(fullUrl, obj, config)
38 | break
39 | case 'DELETE':
40 | response = await axios.delete(fullUrl, config)
41 | break
42 | default:
43 | return `Unsupported method: ${method}`
44 | }
45 | return response.data
46 | } catch (error: any) {
47 | const err =
48 | error?.response?.data?.error || error?.message || 'Something went wrong'
49 | const expectedErrors = [
50 | 'invalid signature',
51 | 'jwt expired',
52 | 'Unauthorized',
53 | 'jwt malformed',
54 | 'revoked',
55 | ]
56 |
57 | if (expectedErrors.includes(err) || err.includes('has been revoked')) {
58 | logout()
59 | }
60 | throw err
61 | }
62 | }
63 |
64 | type Method = 'GET' | 'POST' | 'PUT' | 'DELETE'
65 |
66 | interface ApiHookParams {
67 | key: string[]
68 | method: Method
69 | url: string
70 | }
71 |
72 | export default function ApiCall({ key, method, url }: ApiHookParams) {
73 | const queryClient = new QueryClient()
74 |
75 | // Define hooks unconditionally
76 | const getQuery: UseQueryResult = useQuery({
77 | queryKey: key,
78 | queryFn: () => api('GET', url),
79 | retry: 0,
80 | enabled: method === 'GET',
81 | })
82 |
83 | const postMutation: UseMutationResult = useMutation({
84 | mutationFn: (obj: any) => api('POST', url, obj),
85 | retry: 0,
86 | onSuccess: () => queryClient.invalidateQueries({ queryKey: key }),
87 | })
88 |
89 | const putMutation: UseMutationResult = useMutation({
90 | mutationFn: (obj: any) => api('PUT', `${url}/${obj?.id}`, obj),
91 | retry: 0,
92 | onSuccess: () => queryClient.invalidateQueries({ queryKey: key }),
93 | })
94 |
95 | const deleteMutation: UseMutationResult = useMutation({
96 | mutationFn: (id: string) => api('DELETE', `${url}/${id}`),
97 | retry: 0,
98 | onSuccess: () => queryClient.invalidateQueries({ queryKey: key }),
99 | })
100 |
101 | // Return the appropriate hook result based on the method
102 | switch (method) {
103 | case 'GET':
104 | return { get: getQuery }
105 | case 'POST':
106 | return { post: postMutation }
107 | case 'PUT':
108 | return { put: putMutation }
109 | case 'DELETE':
110 | return { delete: deleteMutation }
111 | default:
112 | throw new Error(`Invalid method ${method}`)
113 | }
114 | }
115 |
116 | export const ApiInfiniteCall = ({ key, url }: ApiHookParams) => {
117 | const infinite: UseInfiniteQueryResult = useInfiniteQuery({
118 | queryKey: key,
119 | queryFn: ({ pageParam = 1 }) => api('GET', `${url}&page=${pageParam}`),
120 | initialPageParam: 1,
121 | getNextPageParam: (lastPage, pages) => {
122 | if (lastPage.endIndex < lastPage.total) {
123 | return pages.length + 1 // This assumes that pages are 1-indexed
124 | }
125 | return undefined
126 | },
127 | retry: 0,
128 | })
129 |
130 | return { infinite: infinite }
131 | }
132 |
--------------------------------------------------------------------------------
/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 | "downlevelIteration": true,
18 | "plugins": [
19 | {
20 | "name": "next"
21 | }
22 | ],
23 | "baseUrl": ".",
24 | "paths": {
25 | "@/*": ["./*"]
26 | }
27 | },
28 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
29 | "exclude": ["node_modules"]
30 | }
31 |
--------------------------------------------------------------------------------
/types/index.ts:
--------------------------------------------------------------------------------
1 | // types
2 |
--------------------------------------------------------------------------------
/zustand/dataStore.ts:
--------------------------------------------------------------------------------
1 | import { create } from 'zustand'
2 |
3 | type DataStore = {
4 | data: { id: string; data: any[] }[]
5 | setData: (data: { id: string; data: any[] }) => void
6 |
7 | dialogOpen: boolean
8 | setDialogOpen: (dialogOpen: boolean) => void
9 | }
10 |
11 | const useDataStore = create((set) => ({
12 | dialogOpen: false,
13 | setDialogOpen: (dialogOpen: boolean) => set({ dialogOpen }),
14 |
15 | data: [{ id: '', data: [] }],
16 | setData: (data: { id: string; data: any[] }) => {
17 | return set((state) => {
18 | const newData = state.data.filter((x) => x.id !== data.id)
19 | return {
20 | data: [...newData, data],
21 | }
22 | })
23 | },
24 | }))
25 |
26 | export default useDataStore
27 |
--------------------------------------------------------------------------------
/zustand/resetStore.ts:
--------------------------------------------------------------------------------
1 | import { create } from 'zustand'
2 |
3 | type ResetStore = {
4 | reset: boolean
5 | setReset: (x: boolean) => void
6 | }
7 |
8 | const useResetStore = create((set) => ({
9 | reset: false,
10 | setReset: (reset: boolean) => {
11 | return set(() => ({
12 | reset: reset,
13 | }))
14 | },
15 | }))
16 |
17 | export default useResetStore
18 |
--------------------------------------------------------------------------------
/zustand/useTempStore.ts:
--------------------------------------------------------------------------------
1 | import { create } from 'zustand'
2 | import { persist, createJSONStorage } from 'zustand/middleware'
3 |
4 | export type UserInfo = {
5 | readonly id?: string
6 | name: string
7 | email: string
8 | token: null
9 | role: string
10 | mobile: number
11 | routes: any[]
12 | menu: any[]
13 | image?: string
14 | }
15 |
16 | type UserInfoStore = {
17 | userInfo: UserInfo
18 | updateUserInfo: (userInfo: UserInfo) => void
19 | logout: () => void
20 | }
21 |
22 | const useUserInfoStore = create(
23 | persist(
24 | (set) => ({
25 | userInfo: {
26 | id: '',
27 | name: '',
28 | email: '',
29 | token: null,
30 | role: '',
31 | mobile: 0,
32 | routes: [],
33 | menu: [],
34 | image: '',
35 | },
36 | updateUserInfo: (userInfo) => {
37 | return set((state) => ({
38 | userInfo: {
39 | ...state.userInfo,
40 | ...userInfo,
41 | },
42 | }))
43 | },
44 | logout: () => {
45 | return set((state) => ({
46 | userInfo: {
47 | ...state.userInfo,
48 | id: '',
49 | name: '',
50 | email: '',
51 | token: null,
52 | role: '',
53 | mobile: 0,
54 | routes: [],
55 | menu: [],
56 | image: '',
57 | },
58 | }))
59 | },
60 | }),
61 | {
62 | name: 'userInfo',
63 | storage: createJSONStorage(() => localStorage),
64 | }
65 | )
66 | )
67 |
68 | export default useUserInfoStore
69 |
--------------------------------------------------------------------------------
/zustand/userStore.ts:
--------------------------------------------------------------------------------
1 | import { create } from 'zustand'
2 | import { persist, createJSONStorage } from 'zustand/middleware'
3 |
4 | export type UserInfo = {
5 | readonly id?: string
6 | name: string
7 | email: string
8 | token: null
9 | role: string
10 | mobile: number
11 | routes: any[]
12 | menu: any[]
13 | image?: string
14 | }
15 |
16 | type UserInfoStore = {
17 | userInfo: UserInfo
18 | updateUserInfo: (userInfo: UserInfo) => void
19 | logout: () => void
20 | }
21 |
22 | const useUserInfoStore = create(
23 | persist(
24 | (set) => ({
25 | userInfo: {
26 | id: '',
27 | name: '',
28 | email: '',
29 | token: null,
30 | role: '',
31 | mobile: 0,
32 | routes: [],
33 | menu: [],
34 | image: '',
35 | },
36 | updateUserInfo: (userInfo) => {
37 | return set((state) => ({
38 | userInfo: {
39 | ...state.userInfo,
40 | ...userInfo,
41 | },
42 | }))
43 | },
44 | logout: () => {
45 | return set((state) => ({
46 | userInfo: {
47 | ...state.userInfo,
48 | id: '',
49 | name: '',
50 | email: '',
51 | token: null,
52 | role: '',
53 | mobile: 0,
54 | routes: [],
55 | menu: [],
56 | image: '',
57 | },
58 | }))
59 | },
60 | }),
61 | {
62 | name: 'userInfo',
63 | storage: createJSONStorage(() => localStorage),
64 | }
65 | )
66 | )
67 |
68 | export default useUserInfoStore
69 |
--------------------------------------------------------------------------------