├── .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 |
58 | 59 | 65 | 66 | 71 | 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 |
74 | 75 | 81 | 88 | 89 | 94 | router.push('/auth/register')} 100 | /> 101 | 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 |
76 | 77 | 84 | 90 | 97 | 104 | 105 | 110 | router.push('/auth/login')} 116 | /> 117 | 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 |
80 | 81 | 88 | 89 | 96 | 97 | 102 | 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 | logo 45 | 46 |
47 | 48 |
49 |
50 |
{children}
51 | 52 |
53 |