├── .editorconfig ├── .env.example ├── .gitignore ├── .husky └── pre-commit ├── .node-version ├── .nvmrc ├── .prettierignore ├── .prettierrc ├── .vscode ├── extensions.json └── settings.json ├── README.md ├── components.json ├── compose.yaml ├── eslint.config.mjs ├── next.config.ts ├── package-lock.json ├── package.json ├── postcss.config.mjs ├── prisma ├── migrations │ └── migration_lock.toml ├── models │ ├── activity.prisma │ ├── feedback.prisma │ ├── permission.prisma │ ├── role.prisma │ ├── token.prisma │ ├── tool.prisma │ └── user.prisma ├── schema.prisma └── seed.ts ├── public ├── error │ ├── calling-help-gray.svg │ ├── calling-help-white.svg │ ├── crashed-error-gray.svg │ ├── crashed-error-white.svg │ ├── timed-out-error-gray.svg │ └── timed-out-error-white.svg ├── favicon.ico ├── illustrations │ ├── home-office-dark.svg │ ├── home-office-white.svg │ ├── no-results-dark.svg │ ├── no-results-light.svg │ ├── page-under-construction-dark.svg │ └── page-under-construction-light.svg ├── logo │ ├── logo-dark.svg │ └── logo-light.svg └── not-found │ ├── falling-dark.svg │ └── falling-gray.svg ├── src ├── actions │ ├── auth │ │ ├── change-email.ts │ │ └── confirm-email.ts │ ├── feedback.ts │ ├── permissions │ │ ├── create.ts │ │ ├── delete.ts │ │ ├── index.ts │ │ └── update.ts │ ├── roles │ │ ├── add-permissions.ts │ │ ├── add-tools.ts │ │ ├── add-users.ts │ │ ├── create.ts │ │ ├── delete.ts │ │ ├── index.ts │ │ ├── remove-permission.ts │ │ ├── remove-user.ts │ │ └── update.ts │ ├── tokens │ │ ├── create.ts │ │ ├── delete.ts │ │ ├── index.ts │ │ └── update.ts │ ├── tools │ │ ├── create.ts │ │ ├── delete.ts │ │ ├── index.ts │ │ └── update.ts │ └── users │ │ ├── add-roles.ts │ │ ├── add-user.ts │ │ ├── delete-account.ts │ │ ├── delete-user.ts │ │ ├── get-current-user.ts │ │ ├── password-reset.ts │ │ ├── remove-role.ts │ │ ├── update-email.ts │ │ ├── update-tool-favorite.ts │ │ ├── update-user.ts │ │ └── update-username.ts ├── app │ ├── (admin) │ │ ├── admin │ │ │ ├── _components │ │ │ │ ├── card-kpi-loading.tsx │ │ │ │ └── card-kpi.tsx │ │ │ ├── page.tsx │ │ │ ├── permissions │ │ │ │ ├── [permissionId] │ │ │ │ │ ├── _components │ │ │ │ │ │ ├── delete-permission-button.tsx │ │ │ │ │ │ └── edit-permission-form.tsx │ │ │ │ │ ├── loading.tsx │ │ │ │ │ ├── not-found.tsx │ │ │ │ │ └── page.tsx │ │ │ │ ├── _components │ │ │ │ │ ├── cell-actions.tsx │ │ │ │ │ ├── columns.tsx │ │ │ │ │ ├── create-permission-button.tsx │ │ │ │ │ ├── delete-permission-dialog.tsx │ │ │ │ │ ├── permissions-empty-state-table.tsx │ │ │ │ │ └── permissions-table.tsx │ │ │ │ ├── loading.tsx │ │ │ │ ├── new │ │ │ │ │ ├── _components │ │ │ │ │ │ └── create-permission-form.tsx │ │ │ │ │ ├── loading.tsx │ │ │ │ │ └── page.tsx │ │ │ │ └── page.tsx │ │ │ ├── roles │ │ │ │ ├── [roleId] │ │ │ │ │ ├── (routes) │ │ │ │ │ │ ├── permissions │ │ │ │ │ │ │ ├── _components │ │ │ │ │ │ │ │ ├── add-permission-button.tsx │ │ │ │ │ │ │ │ ├── add-permission-form.tsx │ │ │ │ │ │ │ │ ├── cell-actions.tsx │ │ │ │ │ │ │ │ ├── columns.tsx │ │ │ │ │ │ │ │ └── permissions-empty-state-table.tsx │ │ │ │ │ │ │ ├── add │ │ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ │ ├── tools │ │ │ │ │ │ │ ├── _components │ │ │ │ │ │ │ │ └── tools-select.tsx │ │ │ │ │ │ │ ├── loading.tsx │ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ │ └── users │ │ │ │ │ │ │ ├── _components │ │ │ │ │ │ │ ├── add-user-button.tsx │ │ │ │ │ │ │ ├── add-user-form.tsx │ │ │ │ │ │ │ ├── cell-actions.tsx │ │ │ │ │ │ │ ├── columns.tsx │ │ │ │ │ │ │ └── users-empty-state-table.tsx │ │ │ │ │ │ │ ├── add │ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── _components │ │ │ │ │ │ ├── delete-role-button.tsx │ │ │ │ │ │ └── edit-role-form.tsx │ │ │ │ │ ├── layout.tsx │ │ │ │ │ ├── loading.tsx │ │ │ │ │ └── page.tsx │ │ │ │ ├── _components │ │ │ │ │ ├── cell-actions.tsx │ │ │ │ │ ├── columns.tsx │ │ │ │ │ ├── create-role-button.tsx │ │ │ │ │ ├── delete-role-dialog.tsx │ │ │ │ │ ├── roles-empty-state-table.tsx │ │ │ │ │ └── roles-table.tsx │ │ │ │ ├── loading.tsx │ │ │ │ ├── new │ │ │ │ │ ├── _components │ │ │ │ │ │ └── create-role-form.tsx │ │ │ │ │ ├── loading.tsx │ │ │ │ │ └── page.tsx │ │ │ │ ├── not-found.tsx │ │ │ │ └── page.tsx │ │ │ ├── tokens │ │ │ │ ├── _components │ │ │ │ │ ├── cell-actions.tsx │ │ │ │ │ ├── columns.tsx │ │ │ │ │ ├── create-token-button.tsx │ │ │ │ │ ├── delete-token-dialog.tsx │ │ │ │ │ ├── edit-token-dialog.tsx │ │ │ │ │ ├── tokens-empty-state-table.tsx │ │ │ │ │ └── tokens-table.tsx │ │ │ │ └── page.tsx │ │ │ ├── tools │ │ │ │ ├── [toolId] │ │ │ │ │ ├── loading.tsx │ │ │ │ │ ├── not-found.tsx │ │ │ │ │ └── page.tsx │ │ │ │ ├── _components │ │ │ │ │ ├── cell-actions.tsx │ │ │ │ │ ├── columns.tsx │ │ │ │ │ ├── create-tool-button.tsx │ │ │ │ │ ├── delete-tool-button.tsx │ │ │ │ │ ├── delete-tool-dialog.tsx │ │ │ │ │ ├── tool-dialog.tsx │ │ │ │ │ ├── tool-form.tsx │ │ │ │ │ ├── tools-empty-state-table.tsx │ │ │ │ │ └── tools-table.tsx │ │ │ │ ├── new │ │ │ │ │ ├── loading.tsx │ │ │ │ │ └── page.tsx │ │ │ │ └── page.tsx │ │ │ └── users │ │ │ │ ├── [userId] │ │ │ │ ├── (routes) │ │ │ │ │ ├── actions │ │ │ │ │ │ ├── _components │ │ │ │ │ │ │ ├── reset-password-card.tsx │ │ │ │ │ │ │ └── set-temporary-password-card.tsx │ │ │ │ │ │ └── page.tsx │ │ │ │ │ ├── logs │ │ │ │ │ │ ├── page.tsx │ │ │ │ │ │ └── user-logs-table.tsx │ │ │ │ │ ├── metadata │ │ │ │ │ │ ├── _components │ │ │ │ │ │ │ └── metadata-editor.tsx │ │ │ │ │ │ └── page.tsx │ │ │ │ │ └── roles │ │ │ │ │ │ ├── _components │ │ │ │ │ │ ├── add-role-button.tsx │ │ │ │ │ │ ├── add-role-form.tsx │ │ │ │ │ │ ├── cell-actions.tsx │ │ │ │ │ │ ├── columns.tsx │ │ │ │ │ │ └── roles-empty-state-table.tsx │ │ │ │ │ │ ├── add │ │ │ │ │ │ └── page.tsx │ │ │ │ │ │ └── page.tsx │ │ │ │ ├── _components │ │ │ │ │ ├── delete-user-button.tsx │ │ │ │ │ └── edit-user-form.tsx │ │ │ │ ├── layout.tsx │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ │ ├── _components │ │ │ │ ├── cell-actions.tsx │ │ │ │ ├── columns.tsx │ │ │ │ ├── create-user-button.tsx │ │ │ │ ├── delete-user-dialog.tsx │ │ │ │ ├── users-empty-state-table.tsx │ │ │ │ └── users-table.tsx │ │ │ │ ├── loading.tsx │ │ │ │ ├── new │ │ │ │ ├── _components │ │ │ │ │ └── create-user-form.tsx │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ │ ├── not-found.tsx │ │ │ │ └── page.tsx │ │ └── layout.tsx │ ├── (auth) │ │ └── auth │ │ │ ├── change-email │ │ │ └── [token] │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ ├── confirm │ │ │ ├── [token] │ │ │ │ ├── loading.tsx │ │ │ │ └── page.tsx │ │ │ └── page.tsx │ │ │ ├── error │ │ │ └── page.tsx │ │ │ ├── login │ │ │ └── page.tsx │ │ │ ├── logout │ │ │ ├── logout.tsx │ │ │ └── page.tsx │ │ │ ├── register │ │ │ └── page.tsx │ │ │ └── reset-password │ │ │ ├── [token] │ │ │ └── page.tsx │ │ │ └── page.tsx │ ├── (healthcheck) │ │ └── healthcheck │ │ │ └── page.tsx │ ├── (home) │ │ ├── layout.tsx │ │ └── page.tsx │ ├── (root) │ │ ├── about │ │ │ └── page.tsx │ │ ├── layout.tsx │ │ ├── profile │ │ │ └── page.tsx │ │ └── settings │ │ │ ├── _components │ │ │ ├── delete-account-form.tsx │ │ │ ├── email-form.tsx │ │ │ └── username-form.tsx │ │ │ ├── layout.tsx │ │ │ ├── loading.tsx │ │ │ └── page.tsx │ ├── (tools) │ │ ├── layout.tsx │ │ └── tools │ │ │ ├── _components │ │ │ ├── add-tool-button.tsx │ │ │ ├── card-skeleton.tsx │ │ │ ├── favorites-section.tsx │ │ │ ├── grid-card.tsx │ │ │ ├── list-card.tsx │ │ │ ├── tool-empty-state.tsx │ │ │ ├── tool-search.tsx │ │ │ ├── tools-cards.tsx │ │ │ ├── tools-section.tsx │ │ │ └── view-switch.tsx │ │ │ ├── blog │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ │ ├── feedbacks │ │ │ ├── _components │ │ │ │ ├── columns.tsx │ │ │ │ ├── data-filters-ssr.tsx │ │ │ │ ├── data-pagination-ssr.tsx │ │ │ │ ├── data-table-column-header.tsx │ │ │ │ ├── data-table-ssr.tsx │ │ │ │ ├── data-table-utils-ssr.tsx │ │ │ │ ├── empty-state.tsx │ │ │ │ └── feedbacks-table.tsx │ │ │ ├── layout.tsx │ │ │ └── page.tsx │ │ │ └── page.tsx │ ├── api │ │ ├── auth │ │ │ ├── [...nextauth] │ │ │ │ └── route.ts │ │ │ ├── confirm │ │ │ │ └── route.ts │ │ │ ├── register │ │ │ │ └── route.ts │ │ │ └── reset-password │ │ │ │ ├── [token] │ │ │ │ └── route.ts │ │ │ │ └── route.ts │ │ ├── docs │ │ │ └── page.tsx │ │ ├── openapi │ │ │ └── route.ts │ │ ├── permissions │ │ │ └── options │ │ │ │ └── route.ts │ │ ├── tools │ │ │ └── route.ts │ │ └── v1 │ │ │ ├── permissions │ │ │ ├── [permissionId] │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ │ ├── roles │ │ │ ├── [roleId] │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ │ └── users │ │ │ ├── [userId] │ │ │ ├── metadata │ │ │ │ └── route.ts │ │ │ └── route.ts │ │ │ └── route.ts │ ├── layout.tsx │ └── not-found.tsx ├── auth.config.ts ├── auth.ts ├── components │ ├── 404.tsx │ ├── admin │ │ ├── section-loading.tsx │ │ ├── sidebar-nav.tsx │ │ └── table-loading.tsx │ ├── auth │ │ └── auth-template.tsx │ ├── back-link-button.tsx │ ├── combobox-multi.tsx │ ├── command-menu.tsx │ ├── copy-clipboard-button.tsx │ ├── copy-clipboard-dropdown-menu-item.tsx │ ├── error-boundary.tsx │ ├── feedback-button.tsx │ ├── health-status-button.tsx │ ├── logo.tsx │ ├── max-width-wrapper.tsx │ ├── navbar │ │ ├── desktop-nav.tsx │ │ ├── login-button.tsx │ │ ├── mobile-nav.tsx │ │ ├── nav-tabs.tsx │ │ ├── navbar.tsx │ │ ├── tool-switcher.tsx │ │ ├── user-avatar.tsx │ │ └── user-nav.tsx │ ├── page-header.tsx │ ├── providers.tsx │ ├── site-footer.tsx │ ├── theme-switch.tsx │ └── ui │ │ ├── avatar.tsx │ │ ├── badge.tsx │ │ ├── button.tsx │ │ ├── card.tsx │ │ ├── checkbox.tsx │ │ ├── command.tsx │ │ ├── context-menu.tsx │ │ ├── data-tables │ │ ├── data-table-column-header.tsx │ │ ├── data-table-faceted-filter.tsx │ │ ├── data-table-filters.tsx │ │ ├── data-table-pagination.tsx │ │ ├── data-table-toolbar.tsx │ │ ├── data-table-view-options.tsx │ │ ├── data-table.tsx │ │ ├── date-cell.tsx │ │ └── empty-state.tsx │ │ ├── dialog.tsx │ │ ├── drawer.tsx │ │ ├── dropdown-menu.tsx │ │ ├── form.tsx │ │ ├── hover-card.tsx │ │ ├── input.tsx │ │ ├── label.tsx │ │ ├── multiple-selector.tsx │ │ ├── popover.tsx │ │ ├── responsive-dialog.tsx │ │ ├── select.tsx │ │ ├── separator.tsx │ │ ├── sheet.tsx │ │ ├── skeleton.tsx │ │ ├── sonner.tsx │ │ ├── switch.tsx │ │ ├── table.tsx │ │ ├── textarea.tsx │ │ ├── toggle-group.tsx │ │ ├── toggle.tsx │ │ └── tooltip.tsx ├── data │ ├── feedback.ts │ ├── permission.ts │ ├── tools.ts │ └── user.ts ├── emails │ ├── confirm-email.tsx │ ├── reset-email.tsx │ └── update-email.tsx ├── env.ts ├── lib │ ├── activity.ts │ ├── api │ │ ├── api-error.ts │ │ ├── get-id-input-or-throw.ts │ │ ├── get-pagination.ts │ │ ├── index.ts │ │ └── parse-request-body.ts │ ├── auth │ │ ├── admin.ts │ │ ├── create-token-api.ts │ │ ├── hash-token.ts │ │ └── index.ts │ ├── hooks │ │ ├── use-copy-to-clipboard.tsx │ │ ├── use-media-query.tsx │ │ └── use-operating-system.tsx │ ├── icons.tsx │ ├── jwt.ts │ ├── mail.ts │ ├── openapi │ │ ├── index.ts │ │ ├── permissions │ │ │ ├── create-permission.ts │ │ │ ├── delete-permission.ts │ │ │ ├── index.ts │ │ │ ├── list-permissions.ts │ │ │ ├── read-permission.ts │ │ │ └── update-permission.ts │ │ ├── responses.ts │ │ ├── roles │ │ │ ├── create-role.ts │ │ │ ├── delete-role.ts │ │ │ ├── index.ts │ │ │ ├── list-roles.ts │ │ │ ├── read-role.ts │ │ │ └── update-role.ts │ │ └── users │ │ │ ├── create-user.ts │ │ │ ├── delete-user.ts │ │ │ ├── index.ts │ │ │ ├── list-users.ts │ │ │ ├── read-user.ts │ │ │ └── update-user.ts │ ├── prismadb.ts │ ├── rbac.tsx │ ├── swr │ │ ├── fetcher.ts │ │ └── use-tools.ts │ ├── utils.ts │ ├── validate-schema-action.ts │ └── zod │ │ ├── index.ts │ │ └── utils.ts ├── middleware.ts ├── schemas │ ├── activity-logs.ts │ ├── api.ts │ ├── auth.ts │ ├── feedbacks.ts │ ├── permissions.ts │ ├── roles.ts │ ├── tokens.ts │ ├── tools.ts │ └── users.ts ├── styles │ └── globals.css └── types │ ├── next-auth.d.ts │ └── types.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Prisma 2 | DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb" 3 | 4 | # Postgres Database 5 | DATABASE_HOSTNAME=postgres_db 6 | DATABASE_PORT=5432 7 | DATABASE_PASSWORD=postgres 8 | DATABASE_USER=postgres 9 | DATABASE_DB=postgres 10 | 11 | # Next Auth 12 | AUTH_URL=http://localhost:3000 13 | AUTH_URL_INTERNAL=http://localhost:3000 14 | AUTH_SECRET=random_string 15 | 16 | # Auth Google 17 | AUTH_GOOGLE_ID= 18 | AUTH_GOOGLE_SECRET= 19 | 20 | # Auth GitHub 21 | AUTH_GITHUB_ID= 22 | AUTH_GITHUB_SECRET= 23 | 24 | # Email 25 | MAIL_SERVER=smtp.gmail.com 26 | MAIL_PORT=587 27 | MAIL_USE_TLS=true 28 | MAIL_USERNAME= 29 | MAIL_PASSWORD= 30 | 31 | # Jwt 32 | JWT_SECRET_KEY=random_string 33 | JWT_EXPIRED_IN=300 -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/usr/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx lint-staged -------------------------------------------------------------------------------- /.node-version: -------------------------------------------------------------------------------- 1 | 22.12.0 -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | 22.12.0 -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | .next 4 | build 5 | src/components/ui/* 6 | !src/components/ui/data-tables/ -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "semi": false, 3 | "trailingComma": "all", 4 | "singleQuote": false, 5 | "printWidth": 88, 6 | "tabWidth": 2, 7 | "useTabs": false, 8 | "quoteProps": "as-needed", 9 | "proseWrap": "always", 10 | "plugins": ["@trivago/prettier-plugin-sort-imports", "prettier-plugin-tailwindcss"], 11 | "importOrder": [ 12 | "^(next/(.*)$)|^(next$)", 13 | "^(react/(.*)$)|^(react$)", 14 | "", 15 | "^@/types/(.*)$", 16 | "^@/lib/(.*)$", 17 | "^@/schemas/(.*)$", 18 | "^@/actions/(.*)$", 19 | "^@/data/(.*)$", 20 | "^@/components/ui/(.*)$", 21 | "^@/components/(.*)$|^components/(.*)$", 22 | "^@/styles/(.*)$", 23 | "^[./]" 24 | ], 25 | "importOrderSeparation": true, 26 | "importOrderSortSpecifiers": true, 27 | "endOfLine": "lf" 28 | } 29 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | // ESLint 4 | "dbaeumer.vscode-eslint", 5 | 6 | // Prettier 7 | "esbenp.prettier-vscode", 8 | 9 | // Tailwind CSS IntelliSense 10 | "bradlc.vscode-tailwindcss", 11 | 12 | // Prisma 13 | "Prisma.prisma", 14 | 15 | // Pretty TypeScript Errors 16 | "YoavBls.pretty-ts-errors", 17 | 18 | // ES7+ React/Redux/React-Native snippets 19 | "dsznajder.es7-react-js-snippets", 20 | 21 | // Error Lens 22 | "usernamehw.errorlens", 23 | 24 | // Console Ninja 25 | "WallabyJs.console-ninja", 26 | 27 | // Code Spell Checker 28 | "streetsidesoftware.code-spell-checker" 29 | ] 30 | } 31 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "node_modules\\typescript\\lib", 3 | "typescript.enablePromptUseWorkspaceTsdk": true, 4 | "editor.formatOnSave": true, 5 | "editor.defaultFormatter": "esbenp.prettier-vscode", 6 | "editor.tabSize": 2, 7 | "files.eol": "\n", 8 | "[prisma]": { 9 | "editor.defaultFormatter": "Prisma.prisma" 10 | }, 11 | "workbench.editor.customLabels.patterns": { 12 | "**/app/**/page.tsx": "${dirname} - page.tsx", 13 | "**/app/**/layout.tsx": "${dirname} - layout.tsx" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # :milky_way: Quark 2 | 3 | Quark is a template ready for tools using @nextjs 15 🚀 4 | 5 | ## :star: Features 6 | 7 | - Log in/Sign up with credentials or Google/GitHub account 8 | - Reset password 9 | - Confirm account via email 10 | - Light and Dark mode 11 | - Account settings 12 | - Admin mode 13 | - Role-based access control 14 | - Send feedback 15 | - Admin API 16 | - More features are coming... 17 | 18 | ## :construction_worker: Built using 19 | 20 | - **NextJS 15** @nextjs 21 | - **Hosted at** @vercel 22 | - **Auth with credentials and Google/GitHub** @nextauthjs v5 23 | - **UI** @shadcn and @tailwindcss v4 24 | - **DB** @PostgreSQL 25 | - **ORM** @prisma 26 | - **RBAC** 27 | - **React server actions** 28 | - **Admin mode** 29 | - **Email** react-email 30 | - **Validation** @zodtypes 31 | - **Tables** tanstack/react-table 32 | - **Forms** @HookForm 33 | - **API Documentation** Swagger UI 34 | - **OpenAPI** zod-openapi 35 | 36 | ## :rocket: Getting Started 37 | 38 | To get started with Quark, follow these steps: 39 | 40 | 1. Clone the repository: 41 | ```bash 42 | git clone https://github.com/ezeparziale/quark.git 43 | ``` 44 | 2. Install dependencies: 45 | ```bash 46 | cd quark 47 | npm install 48 | ``` 49 | 3. Set up environment variables: 50 | ```bash 51 | cp .env.example .env 52 | ``` 53 | 4. Run the development server: 54 | ```bash 55 | npm run dev 56 | ``` 57 | 58 | ## :whale: Using Docker Compose 59 | 60 | To set up PostgreSQL using Docker Compose, follow these steps: 61 | 62 | 1. Ensure Docker and Docker Compose are installed on your machine. 63 | 2. Start the PostgreSQL container: 64 | ```bash 65 | docker-compose up -d 66 | ``` 67 | 3. The PostgreSQL database will be available at `localhost:5432`. 68 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "new-york", 4 | "rsc": true, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "", 8 | "css": "src/styles/globals.css", 9 | "baseColor": "neutral", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils", 16 | "ui": "@/components/ui", 17 | "lib": "@/lib", 18 | "hooks": "@/hooks" 19 | }, 20 | "iconLibrary": "lucide" 21 | } 22 | -------------------------------------------------------------------------------- /compose.yaml: -------------------------------------------------------------------------------- 1 | services: 2 | db: 3 | image: postgres:17.5 4 | hostname: postgres_db 5 | container_name: postgres_db 6 | restart: always 7 | user: ${DATABASE_USER} 8 | ports: 9 | - 5432:5432 10 | environment: 11 | - POSTGRES_USER=${DATABASE_USER} 12 | - POSTGRES_PASSWORD=${DATABASE_PASSWORD} 13 | - POSTGRES_DB=${DATABASE_DB} 14 | volumes: 15 | - postgres-db:/var/lib/postgresql/data 16 | networks: 17 | - quark_net 18 | healthcheck: 19 | test: ["CMD", "pg_isready"] 20 | interval: 10s 21 | timeout: 5s 22 | retries: 5 23 | 24 | adminer: 25 | image: adminer:5.2.1 26 | hostname: adminer 27 | container_name: adminer 28 | restart: always 29 | ports: 30 | - 8080:8080 31 | environment: 32 | - ADMINER_DEFAULT_SERVER=postgres 33 | - ADMINER_DESIGN=hever 34 | networks: 35 | - quark_net 36 | 37 | volumes: 38 | postgres-db: 39 | 40 | networks: 41 | quark_net: 42 | name: quark_net 43 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import { FlatCompat } from "@eslint/eslintrc" 2 | import js from "@eslint/js" 3 | import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended" 4 | import { dirname } from "path" 5 | import tseslint from "typescript-eslint" 6 | import { fileURLToPath } from "url" 7 | 8 | const __filename = fileURLToPath(import.meta.url) 9 | const __dirname = dirname(__filename) 10 | 11 | const compat = new FlatCompat({ 12 | baseDirectory: __dirname, 13 | recommendedConfig: js.configs.recommended, 14 | allConfig: js.configs.all, 15 | }) 16 | 17 | const eslintConfig = [ 18 | { 19 | ignores: [ 20 | "**/dist", 21 | "**/node_modules", 22 | "**/.next", 23 | "**/build", 24 | "src/components/ui", 25 | ], 26 | }, 27 | ...compat.extends("next/core-web-vitals", "next/typescript"), 28 | js.configs.recommended, 29 | ...tseslint.configs.recommended, 30 | eslintPluginPrettierRecommended, 31 | { 32 | rules: { 33 | "no-var": "off", 34 | "no-constant-condition": "off", 35 | "@typescript-eslint/no-explicit-any": "off", 36 | "@typescript-eslint/no-non-null-asserted-optional-chain": "off", 37 | "@typescript-eslint/no-empty-object-type": "off", 38 | }, 39 | }, 40 | ] 41 | 42 | export default eslintConfig 43 | -------------------------------------------------------------------------------- /next.config.ts: -------------------------------------------------------------------------------- 1 | import type { NextConfig } from "next" 2 | 3 | import "./src/env" 4 | 5 | const nextConfig: NextConfig = { 6 | logging: { 7 | fetches: { 8 | fullUrl: true, 9 | }, 10 | }, 11 | experimental: { 12 | reactCompiler: true, 13 | }, 14 | } 15 | 16 | export default nextConfig 17 | -------------------------------------------------------------------------------- /postcss.config.mjs: -------------------------------------------------------------------------------- 1 | const config = { 2 | plugins: ["@tailwindcss/postcss"], 3 | } 4 | 5 | export default config 6 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /prisma/models/activity.prisma: -------------------------------------------------------------------------------- 1 | model ActivityLog { 2 | id Int @id @default(autoincrement()) 3 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 4 | userId Int @map("user_id") 5 | action String 6 | createdAt DateTime @default(now()) @map("created_at") 7 | 8 | @@map("activity_logs") 9 | } 10 | -------------------------------------------------------------------------------- /prisma/models/feedback.prisma: -------------------------------------------------------------------------------- 1 | model Feedback { 2 | id Int @id @default(autoincrement()) 3 | userId Int @map("user_id") 4 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 5 | feedback String 6 | nps Int? 7 | createdAt DateTime @default(now()) @map("created_at") 8 | 9 | @@map("feedbacks") 10 | } 11 | -------------------------------------------------------------------------------- /prisma/models/permission.prisma: -------------------------------------------------------------------------------- 1 | model Permission { 2 | id Int @id @default(autoincrement()) 3 | name String @unique @db.VarChar(45) 4 | key String @unique @db.VarChar(255) 5 | description String @db.VarChar(255) 6 | isActive Boolean @default(false) @map("is_active") 7 | createdAt DateTime @default(now()) @map("created_at") 8 | updatedAt DateTime @default(now()) @updatedAt @map("updated_at") 9 | roles RolePermission[] 10 | 11 | @@map("permissions") 12 | } 13 | -------------------------------------------------------------------------------- /prisma/models/token.prisma: -------------------------------------------------------------------------------- 1 | model Token { 2 | id String @id @default(cuid()) 3 | name String @db.VarChar(255) 4 | hashedToken String @unique @map("hashed_token") 5 | partialToken String @map("partial_token") 6 | expires DateTime? 7 | lastUsed DateTime? @map("last_used") 8 | userId Int @map("user_id") 9 | user User @relation(fields: [userId], references: [id]) 10 | createdAt DateTime @default(now()) @map("created_at") 11 | updatedAt DateTime @default(now()) @updatedAt @map("updated_at") 12 | 13 | @@index([userId]) 14 | @@index([hashedToken]) 15 | @@map("tokens") 16 | } 17 | -------------------------------------------------------------------------------- /prisma/models/tool.prisma: -------------------------------------------------------------------------------- 1 | model Tool { 2 | id Int @id @default(autoincrement()) 3 | name String @unique @db.VarChar(45) 4 | href String @unique @db.VarChar(255) 5 | icon String @db.VarChar(45) 6 | description String @db.VarChar(255) 7 | createdAt DateTime @default(now()) @map("created_at") 8 | updatedAt DateTime @default(now()) @updatedAt @map("updated_at") 9 | roles RoleTool[] 10 | favorites UserToolFavorites[] 11 | 12 | @@map("tools") 13 | } 14 | 15 | model UserToolFavorites { 16 | tool Tool @relation(fields: [toolId], references: [id], onDelete: Cascade) 17 | toolId Int @map("tool_id") 18 | user User @relation(fields: [userId], references: [id], onDelete: Cascade) 19 | userId Int @map("user_id") 20 | createdAt DateTime @default(now()) @map("created_at") 21 | 22 | @@id([toolId, userId]) 23 | @@map("users_tools_favorites") 24 | } 25 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | previewFeatures = ["relationJoins"] 7 | } 8 | 9 | datasource db { 10 | provider = "postgresql" 11 | url = env("DATABASE_URL") 12 | } 13 | -------------------------------------------------------------------------------- /prisma/seed.ts: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from "@prisma/client" 2 | 3 | const prisma = new PrismaClient() 4 | 5 | async function main() { 6 | const permission = await prisma.permission.upsert({ 7 | where: { key: "admin:all" }, 8 | update: {}, 9 | create: { 10 | name: "Admin permission", 11 | description: "Permission to access the admin section", 12 | key: "admin:all", 13 | isActive: true, 14 | }, 15 | }) 16 | 17 | const role = await prisma.role.upsert({ 18 | where: { key: "admin" }, 19 | update: {}, 20 | create: { 21 | name: "Admin", 22 | description: "Admin role", 23 | key: "admin", 24 | isActive: true, 25 | }, 26 | }) 27 | 28 | await prisma.rolePermission.create({ 29 | data: { 30 | permission: { 31 | connect: { 32 | id: permission.id, 33 | }, 34 | }, 35 | role: { 36 | connect: { 37 | id: role.id, 38 | }, 39 | }, 40 | }, 41 | }) 42 | 43 | const tool = await prisma.tool.upsert({ 44 | where: { name: "Admin" }, 45 | update: {}, 46 | create: { 47 | name: "Admin", 48 | href: "/admin", 49 | icon: "shield-check", 50 | description: "Admin tool", 51 | }, 52 | }) 53 | 54 | await prisma.roleTool.create({ 55 | data: { 56 | tool: { 57 | connect: { 58 | id: tool.id, 59 | }, 60 | }, 61 | role: { 62 | connect: { 63 | id: role.id, 64 | }, 65 | }, 66 | }, 67 | }) 68 | 69 | console.log({ permission, role }) 70 | } 71 | 72 | main() 73 | .then(async () => { 74 | await prisma.$disconnect() 75 | }) 76 | .catch(async (e) => { 77 | console.error(e) 78 | await prisma.$disconnect() 79 | process.exit(1) 80 | }) 81 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ezeparziale/quark/c8c096f02e115cd37a4321a346cb6ad5f49a0c1c/public/favicon.ico -------------------------------------------------------------------------------- /public/logo/logo-dark.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /public/logo/logo-light.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/actions/auth/change-email.ts: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import { redirect } from "next/navigation" 4 | 5 | import { verifyUserToken } from "@/lib/jwt" 6 | import prismadb from "@/lib/prismadb" 7 | 8 | interface IPros { 9 | token: string 10 | } 11 | 12 | export async function changeEmail({ token }: IPros) { 13 | let result: { success: boolean; error?: string } = { 14 | success: false, 15 | error: "Default", 16 | } 17 | try { 18 | const userEmail = verifyUserToken(token) 19 | 20 | if (userEmail) { 21 | const tokenExists = await prismadb.changeEmailRequest.findFirst({ 22 | where: { token }, 23 | }) 24 | if (tokenExists && !tokenExists?.isUsed) { 25 | await prismadb.user.update({ 26 | where: { 27 | email: userEmail, 28 | }, 29 | data: { 30 | email: tokenExists.newEmail, 31 | emailVerified: false, 32 | }, 33 | }) 34 | 35 | await prismadb.changeEmailRequest.update({ 36 | where: { id: tokenExists.id }, 37 | data: { 38 | isUsed: true, 39 | }, 40 | }) 41 | result = { success: true } 42 | } else { 43 | result = { success: false, error: "TokenExpired" } 44 | } 45 | } 46 | } catch { 47 | redirect(`/auth/error`) 48 | } finally { 49 | if (result.success) { 50 | redirect("/auth/logout?callbackUrl=/auth/login?updatedEmail=1") 51 | } else { 52 | redirect(`/auth/error?error=${result.error}`) 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/actions/auth/confirm-email.ts: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import { redirect } from "next/navigation" 4 | 5 | import { verifyUserToken } from "@/lib/jwt" 6 | import prismadb from "@/lib/prismadb" 7 | 8 | interface IPros { 9 | token: string 10 | } 11 | 12 | export async function confirmEmail({ token }: IPros) { 13 | let result: { success: boolean } = { 14 | success: false, 15 | } 16 | 17 | try { 18 | const userEmail = verifyUserToken(token) 19 | 20 | if (userEmail) { 21 | await prismadb.user.update({ 22 | where: { 23 | email: userEmail, 24 | }, 25 | data: { 26 | emailVerified: true, 27 | }, 28 | }) 29 | 30 | result = { success: true } 31 | } 32 | } catch { 33 | redirect(`/auth/error`) 34 | } finally { 35 | if (result.success) { 36 | redirect("/auth/login?activated=1") 37 | } else { 38 | redirect("/auth/confirm?error=1") 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/actions/feedback.ts: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import { redirect } from "next/navigation" 4 | 5 | import { z } from "zod" 6 | 7 | import { DataResult } from "@/types/types" 8 | 9 | import { logActivity } from "@/lib/activity" 10 | import prismadb from "@/lib/prismadb" 11 | import { validateSchemaAction } from "@/lib/validate-schema-action" 12 | 13 | import { ActivityType } from "@/schemas/activity-logs" 14 | import { feedbackSchema } from "@/schemas/feedbacks" 15 | 16 | import { getCurrentUser } from "./users/get-current-user" 17 | 18 | type FormData = z.infer 19 | 20 | async function handler(formData: FormData): Promise> { 21 | const currentUser = await getCurrentUser() 22 | 23 | if (!currentUser) { 24 | redirect("/auth/error") 25 | } 26 | try { 27 | const feedback = formData.feedback 28 | 29 | const nps = formData.nps ? parseInt(formData.nps) : undefined 30 | 31 | await prismadb.feedback.create({ data: { feedback, nps, userId: currentUser.id } }) 32 | 33 | await logActivity(currentUser.id, ActivityType.SEND_FEEDBACK) 34 | 35 | return { success: true } 36 | } catch (error) { 37 | console.error("Error creating feedback:", error) 38 | return { success: false, message: "Something went wrong" } 39 | } 40 | } 41 | 42 | export const addFeedback = validateSchemaAction(feedbackSchema, handler) 43 | -------------------------------------------------------------------------------- /src/actions/permissions/delete.ts: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import { revalidatePath } from "next/cache" 4 | 5 | import prismadb from "@/lib/prismadb" 6 | import { has } from "@/lib/rbac" 7 | 8 | export async function deletePermission(id: number) { 9 | try { 10 | const isAuthorized = await has({ role: "admin" }) 11 | 12 | if (!isAuthorized) { 13 | return { success: false, message: "Unauthorized" } 14 | } 15 | 16 | await prismadb.permission.delete({ where: { id } }) 17 | 18 | revalidatePath(`/admin/permissions/`) 19 | 20 | return { success: true } 21 | } catch (error) { 22 | console.error("Error deleting permission:", error) 23 | return { success: false, message: "Something went wrong" } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/actions/permissions/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./create" 2 | export * from "./delete" 3 | export * from "./update" 4 | -------------------------------------------------------------------------------- /src/actions/roles/add-tools.ts: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import { revalidatePath } from "next/cache" 4 | 5 | import { DataResult } from "@/types/types" 6 | 7 | import prismadb from "@/lib/prismadb" 8 | import { has } from "@/lib/rbac" 9 | 10 | interface Props { 11 | roleId: number 12 | toolsIds?: number[] | undefined 13 | } 14 | 15 | export async function addToolsToRoles({ 16 | roleId, 17 | toolsIds, 18 | }: Props): Promise> { 19 | try { 20 | const isAuthorized = await has({ role: "admin" }) 21 | 22 | if (!isAuthorized) { 23 | return { success: false, message: "Unauthorized" } 24 | } 25 | 26 | const currentTools = await prismadb.roleTool.findMany({ 27 | where: { roleId }, 28 | select: { toolId: true }, 29 | }) 30 | 31 | const currentToolIds = new Set(currentTools.map((tool) => tool.toolId)) 32 | const newToolIds = new Set(toolsIds?.map((toolId) => toolId) || []) 33 | const toolsToDelete = currentTools.filter((tool) => !newToolIds.has(tool.toolId)) 34 | const toolsToAdd = toolsIds?.filter((toolId) => !currentToolIds.has(toolId)) || [] 35 | const dataToInsert = toolsToAdd.map((toolId) => ({ 36 | roleId, 37 | toolId, 38 | })) 39 | 40 | await prismadb.roleTool.deleteMany({ 41 | where: { 42 | roleId, 43 | toolId: { 44 | in: toolsToDelete.map((tool) => tool.toolId), 45 | }, 46 | }, 47 | }) 48 | 49 | await prismadb.roleTool.createMany({ data: dataToInsert }) 50 | 51 | revalidatePath(`/admin/roles/${roleId}/tools`) 52 | 53 | return { success: true } 54 | } catch (error) { 55 | console.error("Error adding tools to role:", error) 56 | return { success: false, message: "Something went wrong" } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/actions/roles/add-users.ts: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import { revalidatePath } from "next/cache" 4 | 5 | import { DataResult } from "@/types/types" 6 | 7 | import prismadb from "@/lib/prismadb" 8 | import { has } from "@/lib/rbac" 9 | 10 | interface IRole { 11 | roleId: number 12 | userIds?: number[] | undefined 13 | } 14 | 15 | export async function addUsersToRoles({ 16 | roleId, 17 | userIds, 18 | }: IRole): Promise> { 19 | try { 20 | const isAuthorized = await has({ role: "admin" }) 21 | 22 | if (!isAuthorized) { 23 | return { success: false, message: "Unauthorized" } 24 | } 25 | 26 | const currentUsers = await prismadb.userRole.findMany({ 27 | where: { roleId }, 28 | select: { userId: true }, 29 | }) 30 | 31 | const currentUserIds = new Set(currentUsers.map((user) => user.userId)) 32 | const newUserIds = new Set(userIds?.map((userId) => userId) || []) 33 | const usersToDelete = currentUsers.filter((user) => !newUserIds.has(user.userId)) 34 | const usersToAdd = userIds?.filter((userId) => !currentUserIds.has(userId)) || [] 35 | const dataToInsert = usersToAdd.map((userId) => ({ 36 | roleId, 37 | userId, 38 | })) 39 | 40 | await prismadb.userRole.deleteMany({ 41 | where: { 42 | roleId, 43 | userId: { 44 | in: usersToDelete.map((user) => user.userId), 45 | }, 46 | }, 47 | }) 48 | 49 | await prismadb.userRole.createMany({ data: dataToInsert }) 50 | 51 | revalidatePath(`/admin/roles/${roleId}/users`) 52 | 53 | return { success: true } 54 | } catch (error) { 55 | console.error("Error adding user:", error) 56 | return { success: false, message: "Something went wrong" } 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/actions/roles/delete.ts: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import { revalidatePath } from "next/cache" 4 | 5 | import prismadb from "@/lib/prismadb" 6 | import { has } from "@/lib/rbac" 7 | 8 | export async function deleteRole(id: number) { 9 | try { 10 | const isAuthorized = await has({ role: "admin" }) 11 | 12 | if (!isAuthorized) { 13 | return { success: false, message: "Unauthorized" } 14 | } 15 | 16 | await prismadb.role.delete({ where: { id } }) 17 | 18 | revalidatePath(`/admin/roles/`) 19 | 20 | return { success: true } 21 | } catch (error) { 22 | console.error("Error deleting role:", error) 23 | return { success: false, message: "Something went wrong" } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/actions/roles/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./add-permissions" 2 | export * from "./add-users" 3 | export * from "./create" 4 | export * from "./delete" 5 | export * from "./remove-permission" 6 | export * from "./remove-user" 7 | export * from "./update" 8 | -------------------------------------------------------------------------------- /src/actions/roles/remove-permission.ts: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import { revalidatePath } from "next/cache" 4 | 5 | import prismadb from "@/lib/prismadb" 6 | import { has } from "@/lib/rbac" 7 | 8 | interface IDeleteRolePermission { 9 | roleId: number 10 | permissionId: number 11 | } 12 | 13 | export async function removePermission({ 14 | roleId, 15 | permissionId, 16 | }: IDeleteRolePermission) { 17 | try { 18 | const isAuthorized = await has({ role: "admin" }) 19 | 20 | if (!isAuthorized) { 21 | return { success: false, message: "Unauthorized" } 22 | } 23 | 24 | await prismadb.rolePermission.deleteMany({ 25 | where: { roleId, permissionId }, 26 | }) 27 | 28 | revalidatePath(`/admin/roles/${roleId}/permissions`) 29 | 30 | return { success: true } 31 | } catch (error) { 32 | console.error("Error removing permission:", error) 33 | return { success: false, message: "Something went wrong" } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/actions/roles/remove-user.ts: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import { revalidatePath } from "next/cache" 4 | 5 | import prismadb from "@/lib/prismadb" 6 | import { has } from "@/lib/rbac" 7 | 8 | interface IDeleteRoleUser { 9 | roleId: number 10 | userId: number 11 | } 12 | 13 | export async function removeUser({ roleId, userId }: IDeleteRoleUser) { 14 | try { 15 | const isAuthorized = await has({ role: "admin" }) 16 | 17 | if (!isAuthorized) { 18 | return { success: false, message: "Unauthorized" } 19 | } 20 | 21 | await prismadb.userRole.deleteMany({ 22 | where: { roleId, userId }, 23 | }) 24 | 25 | revalidatePath(`/admin/roles/${roleId}/users`) 26 | 27 | return { success: true } 28 | } catch (error) { 29 | console.error("Error removing user:", error) 30 | return { success: false, message: "Something went wrong" } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/actions/tokens/create.ts: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import { revalidatePath } from "next/cache" 4 | 5 | import { z } from "zod" 6 | 7 | import { DataResult } from "@/types/types" 8 | 9 | import { createTokenApi } from "@/lib/auth" 10 | import prismadb from "@/lib/prismadb" 11 | import { has } from "@/lib/rbac" 12 | import { validateSchemaAction } from "@/lib/validate-schema-action" 13 | 14 | import { tokenCreateServerActionSchema } from "@/schemas/tokens" 15 | 16 | type FormData = z.infer 17 | 18 | async function handler(formData: FormData): Promise> { 19 | try { 20 | const isAuthorized = await has({ role: "admin" }) 21 | 22 | if (!isAuthorized) { 23 | return { success: false, message: "Unauthorized" } 24 | } 25 | 26 | const { name, userId } = formData 27 | 28 | const { token, hashedToken, partialToken } = await createTokenApi() 29 | 30 | await prismadb.token.create({ 31 | data: { name, hashedToken, partialToken, userId }, 32 | }) 33 | 34 | revalidatePath(`/admin/tokens/`) 35 | 36 | return { success: true, data: { token } } 37 | } catch (error) { 38 | console.error("Error creating token:", error) 39 | return { success: false, message: "Something went wrong" } 40 | } 41 | } 42 | 43 | export const createToken = validateSchemaAction(tokenCreateServerActionSchema, handler) 44 | -------------------------------------------------------------------------------- /src/actions/tokens/delete.ts: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import { revalidatePath } from "next/cache" 4 | 5 | import prismadb from "@/lib/prismadb" 6 | import { has } from "@/lib/rbac" 7 | 8 | export async function deleteToken(id: string) { 9 | try { 10 | const isAuthorized = await has({ role: "admin" }) 11 | 12 | if (!isAuthorized) { 13 | return { success: false, message: "Unauthorized" } 14 | } 15 | 16 | await prismadb.token.delete({ where: { id } }) 17 | 18 | revalidatePath(`/admin/tokens/`) 19 | 20 | return { success: true } 21 | } catch (error) { 22 | console.error("Error deleting token:", error) 23 | return { success: false, message: "Something went wrong" } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/actions/tokens/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./create" 2 | export * from "./delete" 3 | export * from "./update" 4 | -------------------------------------------------------------------------------- /src/actions/tokens/update.ts: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import { revalidatePath } from "next/cache" 4 | 5 | import { z } from "zod" 6 | 7 | import { DataResult } from "@/types/types" 8 | 9 | import prismadb from "@/lib/prismadb" 10 | import { has } from "@/lib/rbac" 11 | import { validateSchemaAction } from "@/lib/validate-schema-action" 12 | 13 | import { tokenUpdateServerActionSchema } from "@/schemas/tokens" 14 | 15 | type FormData = z.infer 16 | 17 | async function handler(formData: FormData): Promise> { 18 | try { 19 | const isAuthorized = await has({ role: "admin" }) 20 | 21 | if (!isAuthorized) { 22 | return { success: false, message: "Unauthorized" } 23 | } 24 | 25 | const { id, name } = formData 26 | 27 | await prismadb.token.update({ where: { id }, data: { name } }) 28 | 29 | revalidatePath(`/admin/tokens/`) 30 | 31 | return { success: true } 32 | } catch (error) { 33 | console.error("Error editing token:", error) 34 | return { success: false, message: "Something went wrong" } 35 | } 36 | } 37 | 38 | export const updateToken = validateSchemaAction(tokenUpdateServerActionSchema, handler) 39 | -------------------------------------------------------------------------------- /src/actions/tools/create.ts: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import { revalidatePath } from "next/cache" 4 | 5 | import { z } from "zod" 6 | 7 | import { DataResult } from "@/types/types" 8 | 9 | import prismadb from "@/lib/prismadb" 10 | import { has } from "@/lib/rbac" 11 | import { validateSchemaAction } from "@/lib/validate-schema-action" 12 | 13 | import { toolSchema } from "@/schemas/tools" 14 | 15 | type FormData = z.infer 16 | 17 | async function handler(formData: FormData): Promise> { 18 | const { name, description, href, icon } = formData 19 | 20 | try { 21 | const isAuthorized = await has({ role: "admin" }) 22 | 23 | if (!isAuthorized) { 24 | return { success: false, message: "Unauthorized" } 25 | } 26 | 27 | const errors: Record = { 28 | name: [], 29 | description: [], 30 | href: [], 31 | icon: [], 32 | } 33 | 34 | const toolAlreadyExist = await prismadb.tool.findFirst({ 35 | where: { 36 | OR: [{ name }], 37 | }, 38 | }) 39 | 40 | if (toolAlreadyExist) { 41 | if (toolAlreadyExist.name === name) { 42 | errors.name.push(`A tool with the name '${name}' already exists.`) 43 | } 44 | } 45 | 46 | if (Object.values(errors).some((errorArray) => errorArray.length > 0)) { 47 | return { success: false, errors } 48 | } 49 | 50 | await prismadb.tool.create({ 51 | data: { name, description, href, icon }, 52 | }) 53 | 54 | revalidatePath(`/admin/tools/`) 55 | 56 | return { success: true } 57 | } catch (error) { 58 | console.error("Error creating tool:", error) 59 | return { success: false, message: "Something went wrong" } 60 | } 61 | } 62 | 63 | export const createTool = validateSchemaAction(toolSchema, handler) 64 | -------------------------------------------------------------------------------- /src/actions/tools/delete.ts: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import { revalidatePath } from "next/cache" 4 | 5 | import prismadb from "@/lib/prismadb" 6 | import { has } from "@/lib/rbac" 7 | 8 | export async function deleteTool(id: number) { 9 | try { 10 | const isAuthorized = await has({ role: "admin" }) 11 | 12 | if (!isAuthorized) { 13 | return { success: false, message: "Unauthorized" } 14 | } 15 | 16 | await prismadb.tool.delete({ where: { id } }) 17 | 18 | revalidatePath(`/admin/tools/`) 19 | 20 | return { success: true } 21 | } catch (error) { 22 | console.error("Error deleting tool:", error) 23 | return { success: false, message: "Something went wrong" } 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/actions/tools/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./create" 2 | export * from "./delete" 3 | export * from "./update" 4 | -------------------------------------------------------------------------------- /src/actions/users/delete-account.ts: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import { auth } from "@/auth" 4 | 5 | import { DataResult } from "@/types/types" 6 | 7 | import { logActivity } from "@/lib/activity" 8 | import prismadb from "@/lib/prismadb" 9 | 10 | import { ActivityType } from "@/schemas/activity-logs" 11 | 12 | type FormDataDeleteAccount = { 13 | userEmail: string 14 | confirmString: string 15 | } 16 | 17 | export async function deleteAccount({ 18 | userEmail, 19 | confirmString, 20 | }: FormDataDeleteAccount): Promise> { 21 | const session = await auth() 22 | const errors: { userEmail: string[]; confirmString: string[] } = { 23 | userEmail: [], 24 | confirmString: [], 25 | } 26 | try { 27 | const email = session?.user.email 28 | 29 | if (confirmString != "delete my account") { 30 | errors.confirmString.push("Please type 'delete my account'") 31 | } 32 | 33 | if (email) { 34 | const user = await prismadb.user.findUnique({ where: { email } }) 35 | 36 | if (user) { 37 | if (user.email === userEmail) { 38 | await prismadb.user.delete({ 39 | where: { 40 | id: user.id, 41 | }, 42 | }) 43 | await logActivity(session.user.userId, ActivityType.DELETE_ACCOUNT) 44 | return { success: true } 45 | } else { 46 | errors.userEmail.push("Wrong email") 47 | } 48 | } 49 | } 50 | 51 | return { success: false, errors: errors } 52 | } catch { 53 | return { success: false } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/actions/users/delete-user.ts: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import { revalidatePath } from "next/cache" 4 | 5 | import { auth } from "@/auth" 6 | 7 | import { logActivity } from "@/lib/activity" 8 | import prismadb from "@/lib/prismadb" 9 | import { has } from "@/lib/rbac" 10 | 11 | import { ActivityType } from "@/schemas/activity-logs" 12 | 13 | export async function deleteUser(id: number) { 14 | try { 15 | const session = await auth() 16 | 17 | const isAuthorized = await has({ role: "admin" }) 18 | 19 | if (!isAuthorized) { 20 | return { success: false, message: "Unauthorized" } 21 | } 22 | 23 | await prismadb.user.delete({ where: { id } }) 24 | 25 | revalidatePath(`/admin/users`) 26 | 27 | await logActivity(session?.user.userId!, ActivityType.DELETE_USER) 28 | 29 | return { success: true } 30 | } catch { 31 | return { success: false, message: "Something went wrong" } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/actions/users/get-current-user.ts: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import { redirect } from "next/navigation" 4 | 5 | import { auth } from "@/auth" 6 | 7 | import prismadb from "@/lib/prismadb" 8 | 9 | export async function getCurrentUser(redirectPage?: string) { 10 | const session = await auth() 11 | 12 | if (redirectPage) { 13 | if (!session) { 14 | redirect(redirectPage) 15 | } 16 | } 17 | 18 | try { 19 | const email = session?.user.email 20 | 21 | if (email) { 22 | const user = await prismadb.user.findUnique({ where: { email } }) 23 | return user 24 | } 25 | return null 26 | } catch { 27 | return null 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/actions/users/remove-role.ts: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import { revalidatePath } from "next/cache" 4 | 5 | import { auth } from "@/auth" 6 | 7 | import { logActivity } from "@/lib/activity" 8 | import prismadb from "@/lib/prismadb" 9 | import { has } from "@/lib/rbac" 10 | 11 | import { ActivityType } from "@/schemas/activity-logs" 12 | 13 | interface IDeleteRoleUser { 14 | roleId: number 15 | userId: number 16 | } 17 | 18 | export async function removeRolToUser({ roleId, userId }: IDeleteRoleUser) { 19 | try { 20 | const session = await auth() 21 | 22 | const isAuthorized = await has({ role: "admin" }) 23 | 24 | if (!isAuthorized) { 25 | return { success: false, message: "Unauthorized" } 26 | } 27 | 28 | await prismadb.userRole.deleteMany({ 29 | where: { roleId, userId }, 30 | }) 31 | 32 | revalidatePath(`/admin/users/${userId}/roles`) 33 | 34 | await logActivity(session?.user.userId!, ActivityType.REMOVE_ROLE) 35 | 36 | return { success: true } 37 | } catch (error) { 38 | console.error("Error removing role:", error) 39 | return { success: false, message: "Something went wrong" } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/actions/users/update-tool-favorite.ts: -------------------------------------------------------------------------------- 1 | "use server" 2 | 3 | import { revalidatePath } from "next/cache" 4 | 5 | import { logActivity } from "@/lib/activity" 6 | import prismadb from "@/lib/prismadb" 7 | 8 | import { ActivityType } from "@/schemas/activity-logs" 9 | 10 | import { getCurrentUser } from "./get-current-user" 11 | 12 | export async function AddFavTool({ toolId }: { toolId: number }) { 13 | const currentUser = await getCurrentUser() 14 | 15 | if (currentUser) { 16 | try { 17 | await prismadb.userToolFavorites.create({ 18 | data: { toolId: toolId, userId: currentUser.id }, 19 | }) 20 | revalidatePath("/tools") 21 | 22 | await logActivity(currentUser.id, ActivityType.ADD_TOOL_FAVORITE) 23 | 24 | return { success: true, message: "Tool added to favorites!" } 25 | } catch { 26 | return { 27 | success: false, 28 | message: "Failed to add tool to favorites. Please try again.", 29 | } 30 | } 31 | } 32 | } 33 | 34 | export async function RemoveFavTool({ toolId }: { toolId: number }) { 35 | const currentUser = await getCurrentUser() 36 | 37 | if (currentUser) { 38 | try { 39 | await prismadb.userToolFavorites.deleteMany({ 40 | where: { toolId: toolId, userId: currentUser.id }, 41 | }) 42 | revalidatePath("/tools") 43 | 44 | await logActivity(currentUser.id, ActivityType.REMOVE_TOOL_FAVORITE) 45 | 46 | return { success: true, message: "Tool removed from favorites!" } 47 | } catch { 48 | return { 49 | success: false, 50 | message: "Failed to remove tool from favorites. Please try again.", 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/_components/card-kpi-loading.tsx: -------------------------------------------------------------------------------- 1 | import { Card, CardContent, CardHeader } from "@/components/ui/card" 2 | import { Skeleton } from "@/components/ui/skeleton" 3 | 4 | export default function CardKpiLoading() { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/_components/card-kpi.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | Card, 3 | CardDescription, 4 | CardFooter, 5 | CardHeader, 6 | CardTitle, 7 | } from "@/components/ui/card" 8 | 9 | interface IProps { 10 | title: string 11 | Icon: React.ElementType 12 | kpi: number 13 | extra?: string 14 | } 15 | 16 | export default function CardKpi({ title, Icon, kpi, extra }: IProps) { 17 | return ( 18 | 19 | 20 | {title} 21 | {kpi} 22 |
23 | 24 |
25 |
26 | {extra && ( 27 | 28 |

{extra}

29 |
30 | )} 31 |
32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/page.tsx: -------------------------------------------------------------------------------- 1 | import { Suspense } from "react" 2 | 3 | import { KeySquare, UnlockIcon, User } from "lucide-react" 4 | 5 | import prismadb from "@/lib/prismadb" 6 | import { protectPage } from "@/lib/rbac" 7 | 8 | import { PageHeader } from "@/components/page-header" 9 | 10 | import CardKpi from "./_components/card-kpi" 11 | import CardKpiLoading from "./_components/card-kpi-loading" 12 | 13 | export default async function AdminPage() { 14 | await protectPage({ permission: "admin:all" }) 15 | 16 | const [totalUsers, totalRoles, totalPermissions] = await Promise.all([ 17 | prismadb.user.count(), 18 | prismadb.role.count(), 19 | prismadb.permission.count(), 20 | ]) 21 | 22 | return ( 23 | <> 24 | 25 |
26 | }> 27 | 28 | 29 | }> 30 | 31 | 32 | }> 33 | 34 | 35 |
36 | 37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/permissions/[permissionId]/_components/delete-permission-button.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useState } from "react" 4 | 5 | import { Trash2 } from "lucide-react" 6 | 7 | import { Button } from "@/components/ui/button" 8 | import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip" 9 | 10 | import DeletePermissionDialog from "../../_components/delete-permission-dialog" 11 | 12 | export default function DeletePermissionButton({ 13 | permissionId, 14 | permissionKey, 15 | }: { 16 | permissionId: number 17 | permissionKey: string 18 | }) { 19 | const [isOpen, setIsOpen] = useState(false) 20 | 21 | return ( 22 | <> 23 | 24 | 25 | 30 | 31 | 32 |

Delete permission

33 |
34 |
35 | 41 | 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/permissions/[permissionId]/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from "@/components/ui/skeleton" 2 | 3 | import { PageHeader } from "@/components/page-header" 4 | 5 | export default function LoadingEditPermissionPage() { 6 | return ( 7 |
8 | 9 | 10 |
11 |
12 | 13 | 14 |
15 |
16 | 17 | 18 |
19 |
20 | 21 | 22 |
23 |
24 | 25 | 26 |
27 |
28 |
29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/permissions/[permissionId]/not-found.tsx: -------------------------------------------------------------------------------- 1 | import NotFound404 from "@/components/404" 2 | 3 | export default function PermissionNotFound() { 4 | return ( 5 | 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/permissions/[permissionId]/page.tsx: -------------------------------------------------------------------------------- 1 | import { notFound } from "next/navigation" 2 | 3 | import { protectPage } from "@/lib/rbac" 4 | 5 | import { getPermissionById } from "@/data/permission" 6 | 7 | import { PageHeader } from "@/components/page-header" 8 | 9 | import DeletePermissionButton from "./_components/delete-permission-button" 10 | import EditPermissionForm from "./_components/edit-permission-form" 11 | 12 | type Params = Promise<{ permissionId: number }> 13 | 14 | export default async function EditPermissionPage(props: { params: Params }) { 15 | await protectPage({ permission: "admin:all" }) 16 | 17 | const params = await props.params 18 | const id = Number(params.permissionId) 19 | 20 | if (!id) { 21 | return notFound() 22 | } 23 | 24 | const permission = await getPermissionById(id) 25 | 26 | if (!permission) { 27 | return notFound() 28 | } 29 | 30 | return ( 31 | <> 32 | 44 | } 45 | /> 46 | 47 | 48 | ) 49 | } 50 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/permissions/_components/create-permission-button.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link" 2 | 3 | import { Plus } from "lucide-react" 4 | 5 | import { Button } from "@/components/ui/button" 6 | import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip" 7 | 8 | export default function CreatePermissionButton() { 9 | return ( 10 | 11 | 12 | 19 | 20 | 21 |

Create permission

22 |
23 |
24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/permissions/_components/permissions-empty-state-table.tsx: -------------------------------------------------------------------------------- 1 | import { FilePlus } from "lucide-react" 2 | 3 | import EmptyState from "@/components/ui/data-tables/empty-state" 4 | 5 | import CreatePermissionButton from "./create-permission-button" 6 | 7 | export default function PermissionsEmptyStateTable() { 8 | return ( 9 | } 14 | /> 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/permissions/_components/permissions-table.tsx: -------------------------------------------------------------------------------- 1 | import { getAllPermissions } from "@/data/permission" 2 | 3 | import { DataTable } from "@/components/ui/data-tables/data-table" 4 | 5 | import { columns } from "./columns" 6 | import PermissionsEmptyStateTable from "./permissions-empty-state-table" 7 | 8 | export default async function PermissionsTable() { 9 | const data = await getAllPermissions() 10 | 11 | return ( 12 | } 17 | hiddenColumns={{ ID: false, "Created At": false }} 18 | /> 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/permissions/loading.tsx: -------------------------------------------------------------------------------- 1 | import React from "react" 2 | 3 | import TableLoading from "@/components/admin/table-loading" 4 | import { PageHeader } from "@/components/page-header" 5 | 6 | export default function LoadingPermissions() { 7 | return ( 8 | <> 9 | 10 | 11 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/permissions/new/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from "@/components/ui/skeleton" 2 | 3 | import { PageHeader } from "@/components/page-header" 4 | 5 | export default function LoadingNewPermissionPage() { 6 | return ( 7 |
8 | 9 | 10 |
11 |
12 | 13 | 14 |
15 |
16 | 17 | 18 |
19 |
20 | 21 | 22 |
23 |
24 | 25 | 26 |
27 |
28 |
29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/permissions/new/page.tsx: -------------------------------------------------------------------------------- 1 | import { protectPage } from "@/lib/rbac" 2 | 3 | import { PageHeader } from "@/components/page-header" 4 | 5 | import CreatePermissionForm from "./_components/create-permission-form" 6 | 7 | export default async function NewPermissionPage() { 8 | await protectPage({ permission: "admin:all" }) 9 | 10 | return ( 11 | <> 12 | 17 | 18 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/permissions/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation" 2 | 3 | import { Suspense } from "react" 4 | 5 | import { auth } from "@/auth" 6 | 7 | import { protectPage } from "@/lib/rbac" 8 | 9 | import TableLoading from "@/components/admin/table-loading" 10 | import { PageHeader } from "@/components/page-header" 11 | 12 | import CreatePermissionButton from "./_components/create-permission-button" 13 | import PermissionsTable from "./_components/permissions-table" 14 | 15 | export default async function PermissionsAdminPage() { 16 | const session = await auth() 17 | 18 | if (!session) { 19 | redirect("/auth/login?callbackUrl=/admin/permissions") 20 | } 21 | 22 | await protectPage({ permission: "admin:all" }) 23 | 24 | return ( 25 | <> 26 | } 30 | /> 31 | }> 32 | 33 | 34 | 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/roles/[roleId]/(routes)/permissions/_components/add-permission-button.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link" 2 | 3 | import { Plus } from "lucide-react" 4 | 5 | import { Button } from "@/components/ui/button" 6 | import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip" 7 | 8 | export default function AddPermissionButton({ id }: { id: number }) { 9 | return ( 10 | 11 | 12 | 19 | 20 | 21 |

Add permissions

22 |
23 |
24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/roles/[roleId]/(routes)/permissions/_components/columns.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import Link from "next/link" 4 | 5 | import { type Permission, type RolePermission } from "@prisma/client" 6 | import { ColumnDef } from "@tanstack/react-table" 7 | 8 | import { Badge } from "@/components/ui/badge" 9 | import { DataTableColumnHeader } from "@/components/ui/data-tables/data-table-column-header" 10 | 11 | import CellActions from "./cell-actions" 12 | 13 | export interface IColumns extends RolePermission { 14 | permission: Permission 15 | } 16 | 17 | export const columns: ColumnDef[] = [ 18 | { 19 | id: "permission.name", 20 | accessorKey: "permission.name", 21 | header: ({ column }) => ( 22 | 23 | ), 24 | cell: ({ row }) => { 25 | const linkName = ( 26 | 27 | {row.original.permission.name} 28 | 29 | ) 30 | return linkName 31 | }, 32 | enableGlobalFilter: true, 33 | }, 34 | { 35 | accessorKey: "permission.description", 36 | header: ({ column }) => ( 37 | 38 | ), 39 | enableGlobalFilter: true, 40 | }, 41 | { 42 | accessorKey: "permission.key", 43 | header: ({ column }) => ( 44 | 45 | ), 46 | cell: ({ row }) => { 47 | return {row.original?.permission.key} 48 | }, 49 | enableGlobalFilter: true, 50 | }, 51 | { 52 | id: "actions", 53 | cell: ({ row }) => { 54 | return 55 | }, 56 | }, 57 | ] 58 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/roles/[roleId]/(routes)/permissions/_components/permissions-empty-state-table.tsx: -------------------------------------------------------------------------------- 1 | import { FilePlus } from "lucide-react" 2 | 3 | import EmptyState from "@/components/ui/data-tables/empty-state" 4 | 5 | import AddPermissionButton from "./add-permission-button" 6 | 7 | export default function AddPermissionsEmptyStateTable({ id }: { id: number }) { 8 | return ( 9 | } 14 | /> 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/roles/[roleId]/(routes)/permissions/add/page.tsx: -------------------------------------------------------------------------------- 1 | import prismadb from "@/lib/prismadb" 2 | import { protectPage } from "@/lib/rbac" 3 | 4 | import { PageSection } from "@/components/page-header" 5 | 6 | import AddPermissionForm from "../_components/add-permission-form" 7 | 8 | type Params = Promise<{ roleId: number }> 9 | 10 | export default async function RolesAdminAddPermissionsPage(props: { params: Params }) { 11 | await protectPage({ permission: "admin:all" }) 12 | 13 | const params = await props.params 14 | const roleId = Number(params.roleId) 15 | 16 | const selectedOptions = await prismadb.role.findUnique({ 17 | where: { id: roleId }, 18 | include: { permissions: { include: { permission: true } } }, 19 | }) 20 | 21 | const permissions = await prismadb.permission.findMany() 22 | 23 | const options = permissions.map((permission) => ({ 24 | value: permission.id, 25 | label: permission.key, 26 | })) 27 | 28 | const selectedValues = new Set( 29 | selectedOptions?.permissions.map((permission) => permission.permissionId), 30 | ) 31 | 32 | return ( 33 | <> 34 | 35 | 41 | 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/roles/[roleId]/(routes)/permissions/page.tsx: -------------------------------------------------------------------------------- 1 | import prismadb from "@/lib/prismadb" 2 | import { protectPage } from "@/lib/rbac" 3 | 4 | import { DataTable } from "@/components/ui/data-tables/data-table" 5 | 6 | import { PageSection } from "@/components/page-header" 7 | 8 | import AddPermissionButton from "./_components/add-permission-button" 9 | import { columns } from "./_components/columns" 10 | import AddPermissionsEmptyStateTable from "./_components/permissions-empty-state-table" 11 | 12 | type Params = Promise<{ roleId: number }> 13 | 14 | export default async function RolesAdminPermissionsPage(props: { params: Params }) { 15 | await protectPage({ permission: "admin:all" }) 16 | 17 | const params = await props.params 18 | const roleId = Number(params.roleId) 19 | 20 | const data = await prismadb.role.findUnique({ 21 | where: { id: roleId }, 22 | include: { permissions: { include: { permission: true } } }, 23 | }) 24 | 25 | const dataPermissions = data?.permissions || [] 26 | 27 | return ( 28 | <> 29 | } 33 | /> 34 | } 39 | hideTableViewOption 40 | /> 41 | 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/roles/[roleId]/(routes)/tools/loading.tsx: -------------------------------------------------------------------------------- 1 | import SectionLoading from "@/components/admin/section-loading" 2 | 3 | export default function LoadingRolesAdminToolsPage() { 4 | return 5 | } 6 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/roles/[roleId]/(routes)/tools/page.tsx: -------------------------------------------------------------------------------- 1 | import prismadb from "@/lib/prismadb" 2 | import { protectPage } from "@/lib/rbac" 3 | 4 | import { PageSection } from "@/components/page-header" 5 | 6 | import ToolsSelect from "./_components/tools-select" 7 | 8 | type Params = Promise<{ roleId: number }> 9 | 10 | export default async function RolesAdminToolsPage(props: { params: Params }) { 11 | await protectPage({ permission: "admin:all" }) 12 | 13 | const params = await props.params 14 | const roleId = Number(params.roleId) 15 | 16 | const data = await prismadb.role.findUnique({ 17 | where: { id: roleId }, 18 | include: { tools: { include: { tool: true } } }, 19 | }) 20 | 21 | const tools = await prismadb.tool.findMany() 22 | 23 | const toolSelected = data?.tools?.[0]?.toolId 24 | 25 | return ( 26 | <> 27 | 28 | 29 | 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/roles/[roleId]/(routes)/users/_components/add-user-button.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link" 2 | 3 | import { Plus } from "lucide-react" 4 | 5 | import { Button } from "@/components/ui/button" 6 | import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip" 7 | 8 | export default function AddUserButton({ id }: { id: number }) { 9 | return ( 10 | 11 | 12 | 19 | 20 | 21 |

Add users

22 |
23 |
24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/roles/[roleId]/(routes)/users/_components/columns.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import Link from "next/link" 4 | 5 | import { User, UserRole } from "@prisma/client" 6 | import { ColumnDef } from "@tanstack/react-table" 7 | 8 | import { DataTableColumnHeader } from "@/components/ui/data-tables/data-table-column-header" 9 | 10 | import CellActions from "./cell-actions" 11 | 12 | export interface IColumns extends UserRole { 13 | user: User 14 | } 15 | 16 | export const columns: ColumnDef[] = [ 17 | { 18 | id: "email", 19 | accessorKey: "user.email", 20 | header: ({ column }) => ( 21 | 22 | ), 23 | cell: ({ row }) => { 24 | const linkName = ( 25 | 26 | {row.original.user.email} 27 | 28 | ) 29 | return linkName 30 | }, 31 | enableGlobalFilter: true, 32 | }, 33 | { 34 | id: "actions", 35 | cell: ({ row }) => { 36 | return 37 | }, 38 | }, 39 | ] 40 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/roles/[roleId]/(routes)/users/_components/users-empty-state-table.tsx: -------------------------------------------------------------------------------- 1 | import { FilePlus } from "lucide-react" 2 | 3 | import EmptyState from "@/components/ui/data-tables/empty-state" 4 | 5 | import AddUserButton from "./add-user-button" 6 | 7 | export default function AddUsersEmptyStateTable({ id }: { id: number }) { 8 | return ( 9 | } 14 | /> 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/roles/[roleId]/(routes)/users/add/page.tsx: -------------------------------------------------------------------------------- 1 | import prismadb from "@/lib/prismadb" 2 | import { protectPage } from "@/lib/rbac" 3 | 4 | import { PageSection } from "@/components/page-header" 5 | 6 | import AddUserForm from "../_components/add-user-form" 7 | 8 | type Params = Promise<{ roleId: number }> 9 | 10 | export default async function RolesAdminAddUsersPage(props: { params: Params }) { 11 | await protectPage({ permission: "admin:all" }) 12 | 13 | const params = await props.params 14 | const roleId = Number(params.roleId) 15 | 16 | const selectedOptions = await prismadb.role.findUnique({ 17 | where: { id: roleId }, 18 | include: { users: { include: { user: true } } }, 19 | }) 20 | 21 | const users = await prismadb.user.findMany() 22 | 23 | const options = users.map((user) => ({ 24 | value: user.id, 25 | label: user.email, 26 | })) 27 | 28 | const selectedValues = new Set(selectedOptions?.users.map((user) => user.userId)) 29 | 30 | return ( 31 | <> 32 | 33 | 39 | 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/roles/[roleId]/(routes)/users/page.tsx: -------------------------------------------------------------------------------- 1 | import prismadb from "@/lib/prismadb" 2 | import { protectPage } from "@/lib/rbac" 3 | 4 | import { DataTable } from "@/components/ui/data-tables/data-table" 5 | 6 | import { PageSection } from "@/components/page-header" 7 | 8 | import AddUserButton from "./_components/add-user-button" 9 | import { columns } from "./_components/columns" 10 | import AddUsersEmptyStateTable from "./_components/users-empty-state-table" 11 | 12 | type Params = Promise<{ roleId: number }> 13 | 14 | export default async function RolesAdminUsersPage(props: { params: Params }) { 15 | await protectPage({ permission: "admin:all" }) 16 | 17 | const params = await props.params 18 | const roleId = Number(params.roleId) 19 | 20 | const data = await prismadb.role.findUnique({ 21 | where: { id: roleId }, 22 | include: { users: { include: { user: true } } }, 23 | }) 24 | 25 | const dataUsers = data?.users || [] 26 | 27 | return ( 28 | <> 29 | } 33 | /> 34 | } 39 | hideTableViewOption 40 | /> 41 | 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/roles/[roleId]/_components/delete-role-button.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useState } from "react" 4 | 5 | import { Trash2 } from "lucide-react" 6 | 7 | import { Button } from "@/components/ui/button" 8 | import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip" 9 | 10 | import DeleteRoleDialog from "../../_components/delete-role-dialog" 11 | 12 | export default function DeleteRoleButton({ 13 | roleId, 14 | roleKey, 15 | }: { 16 | roleId: number 17 | roleKey: string 18 | }) { 19 | const [isOpen, setIsOpen] = useState(false) 20 | 21 | return ( 22 | <> 23 | 24 | 25 | 30 | 31 | 32 |

Delete role

33 |
34 |
35 | 41 | 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/roles/[roleId]/loading.tsx: -------------------------------------------------------------------------------- 1 | import SectionLoading from "@/components/admin/section-loading" 2 | 3 | export default function LoadingEditRolePage() { 4 | return 5 | } 6 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/roles/[roleId]/page.tsx: -------------------------------------------------------------------------------- 1 | import { notFound } from "next/navigation" 2 | 3 | import prismadb from "@/lib/prismadb" 4 | import { protectPage } from "@/lib/rbac" 5 | 6 | import { PageSection } from "@/components/page-header" 7 | 8 | import EditRoleForm from "./_components/edit-role-form" 9 | 10 | type Params = Promise<{ roleId: number }> 11 | 12 | export default async function RoleAdminPage(props: { params: Params }) { 13 | await protectPage({ permission: "admin:all" }) 14 | 15 | const params = await props.params 16 | const id = Number(params.roleId) 17 | 18 | if (!id) { 19 | return notFound() 20 | } 21 | 22 | const role = await prismadb.role.findUnique({ 23 | where: { id }, 24 | include: { 25 | permissions: { include: { permission: { select: { id: true, name: true } } } }, 26 | }, 27 | }) 28 | 29 | if (!role) { 30 | return notFound() 31 | } 32 | 33 | const permissionsSelectedOptions = role.permissions.map((permission) => ({ 34 | value: String(permission.permission.id), 35 | label: permission.permission.name, 36 | })) 37 | 38 | const roleWithFormattedPermissions = { 39 | ...role, 40 | permissionsSelectedOptions, 41 | } 42 | 43 | return ( 44 | <> 45 | 46 | 47 | 48 | ) 49 | } 50 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/roles/_components/create-role-button.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link" 2 | 3 | import { Plus } from "lucide-react" 4 | 5 | import { Button } from "@/components/ui/button" 6 | import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip" 7 | 8 | export default function CreateRoleButton() { 9 | return ( 10 | 11 | 12 | 19 | 20 | 21 |

Create role

22 |
23 |
24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/roles/_components/roles-empty-state-table.tsx: -------------------------------------------------------------------------------- 1 | import { FilePlus } from "lucide-react" 2 | 3 | import EmptyState from "@/components/ui/data-tables/empty-state" 4 | 5 | import CreateRoleButton from "./create-role-button" 6 | 7 | export default function RolesEmptyStateTable() { 8 | return ( 9 | } 14 | /> 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/roles/_components/roles-table.tsx: -------------------------------------------------------------------------------- 1 | import prismadb from "@/lib/prismadb" 2 | 3 | import { DataTable } from "@/components/ui/data-tables/data-table" 4 | 5 | import { columns } from "./columns" 6 | import RolesEmptyStateTable from "./roles-empty-state-table" 7 | 8 | export default async function RolesTable() { 9 | const data = await prismadb.role.findMany({ orderBy: { updatedAt: "desc" } }) 10 | 11 | return ( 12 | } 17 | hiddenColumns={{ ID: false, "Created At": false }} 18 | /> 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/roles/loading.tsx: -------------------------------------------------------------------------------- 1 | import TableLoading from "@/components/admin/table-loading" 2 | import { PageHeader } from "@/components/page-header" 3 | 4 | export default function LoadingRolesAdminPage() { 5 | return ( 6 | <> 7 | 8 | 9 | 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/roles/new/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from "@/components/ui/skeleton" 2 | 3 | import { PageHeader } from "@/components/page-header" 4 | 5 | export default function LoadingCreateRolePage() { 6 | return ( 7 |
8 | 9 | 10 |
11 |
12 | 13 | 14 |
15 |
16 | 17 | 18 |
19 |
20 | 21 | 22 |
23 |
24 | 25 | 26 |
27 |
28 |
29 | ) 30 | } 31 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/roles/new/page.tsx: -------------------------------------------------------------------------------- 1 | import { protectPage } from "@/lib/rbac" 2 | 3 | import { PageHeader } from "@/components/page-header" 4 | 5 | import CreateRoleForm from "./_components/create-role-form" 6 | 7 | export default async function NewRolePage() { 8 | await protectPage({ permission: "admin:all" }) 9 | 10 | return ( 11 | <> 12 | 17 | 18 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/roles/not-found.tsx: -------------------------------------------------------------------------------- 1 | import NotFound404 from "@/components/404" 2 | 3 | export default function RoleNotFound() { 4 | return ( 5 | 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/roles/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation" 2 | 3 | import { Suspense } from "react" 4 | 5 | import { auth } from "@/auth" 6 | 7 | import { protectPage } from "@/lib/rbac" 8 | 9 | import TableLoading from "@/components/admin/table-loading" 10 | import { PageHeader } from "@/components/page-header" 11 | 12 | import CreateRoleButton from "./_components/create-role-button" 13 | import RolesTable from "./_components/roles-table" 14 | 15 | export default async function RolesAdminPage() { 16 | const session = await auth() 17 | 18 | if (!session) { 19 | redirect("/auth/login?callbackUrl=/admin/roles") 20 | } 21 | 22 | await protectPage({ permission: "admin:all" }) 23 | 24 | return ( 25 | <> 26 | } 30 | /> 31 | }> 32 | 33 | 34 | 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/tokens/_components/tokens-empty-state-table.tsx: -------------------------------------------------------------------------------- 1 | import { KeySquare } from "lucide-react" 2 | 3 | import EmptyState from "@/components/ui/data-tables/empty-state" 4 | 5 | import CreateTokenButton from "./create-token-button" 6 | 7 | export default function TokensEmptyStateTable() { 8 | return ( 9 | } 14 | /> 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/tokens/_components/tokens-table.tsx: -------------------------------------------------------------------------------- 1 | import prismadb from "@/lib/prismadb" 2 | 3 | import { DataTable } from "@/components/ui/data-tables/data-table" 4 | 5 | import { columns } from "./columns" 6 | import TokensEmptyStateTable from "./tokens-empty-state-table" 7 | 8 | export default async function TokensTable() { 9 | const data = await prismadb.token.findMany({ 10 | include: { user: { select: { id: true, username: true } } }, 11 | orderBy: { createdAt: "desc" }, 12 | }) 13 | 14 | return ( 15 | } 20 | hiddenColumns={{ "Created At": false, "Updated At": false }} 21 | /> 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/tokens/page.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link" 2 | import { redirect } from "next/navigation" 3 | 4 | import { Suspense } from "react" 5 | 6 | import { auth } from "@/auth" 7 | import { ExternalLink } from "lucide-react" 8 | 9 | import { protectPage } from "@/lib/rbac" 10 | 11 | import { Button } from "@/components/ui/button" 12 | 13 | import TableLoading from "@/components/admin/table-loading" 14 | import { PageHeader } from "@/components/page-header" 15 | 16 | import CreateTokenButton from "./_components/create-token-button" 17 | import TokensTable from "./_components/tokens-table" 18 | 19 | export default async function TokensAdminPage() { 20 | const session = await auth() 21 | 22 | if (!session) { 23 | redirect("/auth/login?callbackUrl=/admin/users") 24 | } 25 | 26 | await protectPage({ permission: "admin:all" }) 27 | 28 | return ( 29 | <> 30 | , 35 | , 36 | ]} 37 | /> 38 | }> 39 | 40 | 41 | 42 | ) 43 | } 44 | 45 | function ApiDocsLinkButton() { 46 | return ( 47 | 53 | ) 54 | } 55 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/tools/[toolId]/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from "@/components/ui/skeleton" 2 | 3 | import { PageHeader } from "@/components/page-header" 4 | 5 | export default function LoadingEditToolPage() { 6 | return ( 7 |
8 | 9 | 10 |
11 |
12 | 13 | 14 |
15 |
16 | 17 | 18 |
19 |
20 | 21 | 22 |
23 |
24 | 25 | 26 |
27 |
28 | 29 | 30 |
31 |
32 |
33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/tools/[toolId]/not-found.tsx: -------------------------------------------------------------------------------- 1 | import NotFound404 from "@/components/404" 2 | 3 | export default function ToolNotFound() { 4 | return ( 5 | 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/tools/[toolId]/page.tsx: -------------------------------------------------------------------------------- 1 | import { notFound } from "next/navigation" 2 | 3 | import { protectPage } from "@/lib/rbac" 4 | 5 | import { getToolById } from "@/data/tools" 6 | 7 | import { PageHeader } from "@/components/page-header" 8 | 9 | import DeleteToolButton from "../_components/delete-tool-button" 10 | import ToolForm from "../_components/tool-form" 11 | 12 | type Params = Promise<{ toolId: number }> 13 | 14 | export default async function EditToolPage(props: { params: Params }) { 15 | await protectPage({ permission: "admin:all" }) 16 | 17 | const params = await props.params 18 | const id = Number(params.toolId) 19 | 20 | if (!id) { 21 | return notFound() 22 | } 23 | 24 | const tool = await getToolById(id) 25 | 26 | if (!tool) { 27 | return notFound() 28 | } 29 | 30 | return ( 31 | <> 32 | } 38 | /> 39 | 40 | 41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/tools/_components/create-tool-button.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useState } from "react" 4 | 5 | import { Plus } from "lucide-react" 6 | 7 | import { Button } from "@/components/ui/button" 8 | import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip" 9 | 10 | import ToolDialog from "./tool-dialog" 11 | 12 | export default function CreateToolButton() { 13 | const [isOpen, setIsOpen] = useState(false) 14 | 15 | return ( 16 | <> 17 | 18 | 19 | 24 | 25 | 26 |

Create tool

27 |
28 |
29 | 30 | 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/tools/_components/delete-tool-button.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useState } from "react" 4 | 5 | import { Trash2 } from "lucide-react" 6 | 7 | import { Button } from "@/components/ui/button" 8 | import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip" 9 | 10 | import DeleteToolDialog from "./delete-tool-dialog" 11 | 12 | export default function DeleteToolButton({ 13 | toolId, 14 | toolName, 15 | }: { 16 | toolId: number 17 | toolName: string 18 | }) { 19 | const [isOpen, setIsOpen] = useState(false) 20 | 21 | return ( 22 | <> 23 | 24 | 25 | 30 | 31 | 32 |

Delete tool

33 |
34 |
35 | 41 | 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/tools/_components/tools-empty-state-table.tsx: -------------------------------------------------------------------------------- 1 | import { LayoutGrid } from "lucide-react" 2 | 3 | import EmptyState from "@/components/ui/data-tables/empty-state" 4 | 5 | import CreateToolButton from "./create-tool-button" 6 | 7 | export default function PermissionsEmptyStateTable() { 8 | return ( 9 | } 14 | /> 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/tools/_components/tools-table.tsx: -------------------------------------------------------------------------------- 1 | import prismadb from "@/lib/prismadb" 2 | 3 | import { DataTable } from "@/components/ui/data-tables/data-table" 4 | 5 | import { columns } from "./columns" 6 | import ToolsEmptyStateTable from "./tools-empty-state-table" 7 | 8 | export default async function ToolsTable() { 9 | const data = await prismadb.tool.findMany({ 10 | orderBy: { updatedAt: "desc" }, 11 | }) 12 | 13 | return ( 14 | } 19 | hiddenColumns={{ ID: false, "Created At": false, "Updated At": false }} 20 | /> 21 | ) 22 | } 23 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/tools/new/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from "@/components/ui/skeleton" 2 | 3 | import { PageHeader } from "@/components/page-header" 4 | 5 | export default function LoadingCreateToolPage() { 6 | return ( 7 |
8 | 9 | 10 |
11 |
12 | 13 | 14 |
15 |
16 | 17 | 18 |
19 |
20 | 21 | 22 |
23 |
24 | 25 | 26 |
27 |
28 | 29 | 30 |
31 |
32 |
33 | ) 34 | } 35 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/tools/new/page.tsx: -------------------------------------------------------------------------------- 1 | import { protectPage } from "@/lib/rbac" 2 | 3 | import { PageHeader } from "@/components/page-header" 4 | 5 | import ToolForm from "../_components/tool-form" 6 | 7 | export default async function NewToolPage() { 8 | await protectPage({ permission: "admin:all" }) 9 | 10 | return ( 11 | <> 12 | 17 | 18 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/tools/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation" 2 | 3 | import { Suspense } from "react" 4 | 5 | import { auth } from "@/auth" 6 | 7 | import { protectPage } from "@/lib/rbac" 8 | 9 | import TableLoading from "@/components/admin/table-loading" 10 | import { PageHeader } from "@/components/page-header" 11 | 12 | import CreateToolButton from "./_components/create-tool-button" 13 | import ToolsTable from "./_components/tools-table" 14 | 15 | export default async function ToolsAdminPage() { 16 | const session = await auth() 17 | 18 | if (!session) { 19 | redirect("/auth/login?callbackUrl=/admin/tools") 20 | } 21 | 22 | await protectPage({ permission: "admin:all" }) 23 | 24 | return ( 25 | <> 26 | } 30 | /> 31 | }> 32 | 33 | 34 | 35 | ) 36 | } 37 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/users/[userId]/(routes)/actions/page.tsx: -------------------------------------------------------------------------------- 1 | import { protectPage } from "@/lib/rbac" 2 | 3 | import { PageSection } from "@/components/page-header" 4 | 5 | import { ResetPasswordCard } from "./_components/reset-password-card" 6 | import { SetTemporaryPasswordCard } from "./_components/set-temporary-password-card" 7 | 8 | type Params = Promise<{ userId: number }> 9 | 10 | export default async function UserActionsPage(props: { params: Params }) { 11 | await protectPage({ permission: "admin:all" }) 12 | 13 | const params = await props.params 14 | const userId = Number(params.userId) 15 | 16 | return ( 17 | <> 18 | 19 |
20 | 21 | 22 |
23 | 24 | ) 25 | } 26 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/users/[userId]/(routes)/logs/page.tsx: -------------------------------------------------------------------------------- 1 | import prismadb from "@/lib/prismadb" 2 | import { protectPage } from "@/lib/rbac" 3 | 4 | import { PageSection } from "@/components/page-header" 5 | 6 | import UserLogsTables from "./user-logs-table" 7 | 8 | type Params = Promise<{ userId: number }> 9 | 10 | export default async function UserLogsPage(props: { params: Params }) { 11 | await protectPage({ permission: "admin:all" }) 12 | 13 | const params = await props.params 14 | const userId = Number(params.userId) 15 | 16 | const data = await prismadb.activityLog.findMany({ 17 | where: { userId }, 18 | orderBy: { createdAt: "desc" }, 19 | }) 20 | 21 | return ( 22 | <> 23 | 24 | 25 | 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/users/[userId]/(routes)/metadata/page.tsx: -------------------------------------------------------------------------------- 1 | import { notFound } from "next/navigation" 2 | 3 | import prismadb from "@/lib/prismadb" 4 | import { protectPage } from "@/lib/rbac" 5 | 6 | import { PageSection } from "@/components/page-header" 7 | 8 | import { MetadataEditor } from "./_components/metadata-editor" 9 | 10 | type Params = Promise<{ userId: number }> 11 | 12 | export default async function UserMetadataPage(props: { params: Params }) { 13 | await protectPage({ permission: "admin:all" }) 14 | 15 | const params = await props.params 16 | const userId = Number(params.userId) 17 | 18 | const user = await prismadb.user.findUnique({ 19 | select: { id: true, userMetadata: { select: { metadata: true } } }, 20 | where: { id: userId }, 21 | }) 22 | 23 | if (!user) notFound() 24 | 25 | const metadata = JSON.stringify(user.userMetadata?.metadata || {}, null, 2) 26 | 27 | return ( 28 | <> 29 | 30 | 31 | 32 | ) 33 | } 34 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/users/[userId]/(routes)/roles/_components/add-role-button.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link" 2 | 3 | import { Plus } from "lucide-react" 4 | 5 | import { Button } from "@/components/ui/button" 6 | import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip" 7 | 8 | export default function AddRoleButton({ id }: { id: string }) { 9 | return ( 10 | 11 | 12 | 23 | 24 | 25 |

Add roles

26 |
27 |
28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/users/[userId]/(routes)/roles/_components/columns.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import Link from "next/link" 4 | 5 | import { type Role, type UserRole } from "@prisma/client" 6 | import { ColumnDef } from "@tanstack/react-table" 7 | 8 | import { DataTableColumnHeader } from "@/components/ui/data-tables/data-table-column-header" 9 | 10 | import CellActions from "./cell-actions" 11 | 12 | export interface IColumns extends UserRole { 13 | role: Role 14 | } 15 | 16 | export const columns: ColumnDef[] = [ 17 | { 18 | id: "name", 19 | accessorKey: "role.name", 20 | header: ({ column }) => ( 21 | 22 | ), 23 | cell: ({ row }) => { 24 | const linkName = ( 25 | 26 | {row.original.role.name} 27 | 28 | ) 29 | return linkName 30 | }, 31 | enableGlobalFilter: true, 32 | }, 33 | { 34 | id: "actions", 35 | cell: ({ row }) => { 36 | return 37 | }, 38 | }, 39 | ] 40 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/users/[userId]/(routes)/roles/_components/roles-empty-state-table.tsx: -------------------------------------------------------------------------------- 1 | import { FilePlus } from "lucide-react" 2 | 3 | import EmptyState from "@/components/ui/data-tables/empty-state" 4 | 5 | import AddRoleButton from "./add-role-button" 6 | 7 | export default function AddRolesEmptyStateTable({ id }: { id: string }) { 8 | return ( 9 | } 14 | /> 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/users/[userId]/(routes)/roles/add/page.tsx: -------------------------------------------------------------------------------- 1 | import prismadb from "@/lib/prismadb" 2 | import { protectPage } from "@/lib/rbac" 3 | 4 | import { PageSection } from "@/components/page-header" 5 | 6 | import AddRoleForm from "../_components/add-role-form" 7 | 8 | type Params = Promise<{ userId: number }> 9 | 10 | export default async function UsersAdminAddRolesToUserPage(props: { params: Params }) { 11 | await protectPage({ permission: "admin:all" }) 12 | 13 | const params = await props.params 14 | const userId = Number(params.userId) 15 | 16 | const selectedOptions = await prismadb.user.findUnique({ 17 | where: { id: userId }, 18 | include: { roles: { include: { role: true } } }, 19 | }) 20 | 21 | const roles = await prismadb.role.findMany() 22 | 23 | const options = roles.map((role) => ({ 24 | value: role.id, 25 | label: role.name, 26 | })) 27 | 28 | const selectedValues = new Set(selectedOptions?.roles.map((role) => role.roleId)) 29 | 30 | return ( 31 | <> 32 | 33 | 39 | 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/users/[userId]/(routes)/roles/page.tsx: -------------------------------------------------------------------------------- 1 | import prismadb from "@/lib/prismadb" 2 | import { protectPage } from "@/lib/rbac" 3 | 4 | import { DataTable } from "@/components/ui/data-tables/data-table" 5 | 6 | import { PageSection } from "@/components/page-header" 7 | 8 | import AddRoleButton from "./_components/add-role-button" 9 | import { columns } from "./_components/columns" 10 | import AddRolesEmptyStateTable from "./_components/roles-empty-state-table" 11 | 12 | type Params = Promise<{ userId: number }> 13 | 14 | export default async function UsersAdminRolesPage(props: { params: Params }) { 15 | await protectPage({ permission: "admin:all" }) 16 | 17 | const params = await props.params 18 | const userId = Number(params.userId) 19 | 20 | const data = await prismadb.user.findUnique({ 21 | where: { id: userId }, 22 | include: { roles: { include: { role: true } } }, 23 | }) 24 | 25 | const dataRoles = data?.roles || [] 26 | 27 | return ( 28 | <> 29 | } 33 | /> 34 | } 39 | hideTableViewOption 40 | /> 41 | 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/users/[userId]/_components/delete-user-button.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useState } from "react" 4 | 5 | import { Trash2 } from "lucide-react" 6 | 7 | import { Button } from "@/components/ui/button" 8 | import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip" 9 | 10 | import DeleteUserDialog from "../../_components/delete-user-dialog" 11 | 12 | export default function DeleteUserButton({ 13 | userId, 14 | userEmail, 15 | }: { 16 | userId: number 17 | userEmail: string 18 | }) { 19 | const [isOpen, setIsOpen] = useState(false) 20 | 21 | return ( 22 | <> 23 | 24 | 25 | 33 | 34 | 35 |

Delete user

36 |
37 |
38 | 44 | 45 | ) 46 | } 47 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/users/[userId]/loading.tsx: -------------------------------------------------------------------------------- 1 | import SectionLoading from "@/components/admin/section-loading" 2 | 3 | export default function LoadingEditUserPage() { 4 | return 5 | } 6 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/users/[userId]/page.tsx: -------------------------------------------------------------------------------- 1 | import { notFound } from "next/navigation" 2 | 3 | import { protectPage } from "@/lib/rbac" 4 | 5 | import { getUserById } from "@/data/user" 6 | 7 | import { PageSection } from "@/components/page-header" 8 | 9 | import EditUserForm from "./_components/edit-user-form" 10 | 11 | type Params = Promise<{ userId: number }> 12 | 13 | export default async function UserAdminPage(props: { params: Params }) { 14 | await protectPage({ permission: "admin:all" }) 15 | 16 | const params = await props.params 17 | const userId = Number(params.userId) 18 | 19 | const user = await getUserById(userId) 20 | 21 | if (!user) { 22 | return notFound() 23 | } 24 | 25 | return ( 26 | <> 27 | 28 | 29 | 30 | ) 31 | } 32 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/users/_components/create-user-button.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link" 2 | 3 | import { Plus } from "lucide-react" 4 | 5 | import { Button } from "@/components/ui/button" 6 | import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip" 7 | 8 | export default function CreateUserButton() { 9 | return ( 10 | 11 | 12 | 23 | 24 | 25 |

Create user

26 |
27 |
28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/users/_components/users-empty-state-table.tsx: -------------------------------------------------------------------------------- 1 | import { FilePlus } from "lucide-react" 2 | 3 | import EmptyState from "@/components/ui/data-tables/empty-state" 4 | 5 | import CreateUserButton from "./create-user-button" 6 | 7 | export default function UsersEmptyStateTable() { 8 | return ( 9 | } 14 | /> 15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/users/loading.tsx: -------------------------------------------------------------------------------- 1 | import TableLoading from "@/components/admin/table-loading" 2 | import { PageHeader } from "@/components/page-header" 3 | 4 | export default function LoadingUsersAdminPage() { 5 | return ( 6 | <> 7 | 8 | 9 | 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/users/new/page.tsx: -------------------------------------------------------------------------------- 1 | import { protectPage } from "@/lib/rbac" 2 | 3 | import { PageHeader } from "@/components/page-header" 4 | 5 | import CreateUserForm from "./_components/create-user-form" 6 | 7 | export default async function NewUserPage() { 8 | await protectPage({ permission: "admin:all" }) 9 | 10 | return ( 11 | <> 12 | 17 | 18 | 19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/users/not-found.tsx: -------------------------------------------------------------------------------- 1 | import NotFound404 from "@/components/404" 2 | 3 | export default function NotFound() { 4 | return ( 5 | 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /src/app/(admin)/admin/users/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation" 2 | 3 | import { Suspense } from "react" 4 | 5 | import { auth } from "@/auth" 6 | 7 | import { protectPage } from "@/lib/rbac" 8 | 9 | import { getUsers } from "@/data/user" 10 | 11 | import TableLoading from "@/components/admin/table-loading" 12 | import { ErrorBoundary } from "@/components/error-boundary" 13 | import { PageHeader } from "@/components/page-header" 14 | 15 | import CreateUserButton from "./_components/create-user-button" 16 | import UsersTable from "./_components/users-table" 17 | 18 | export default async function UsersAdminPage() { 19 | const session = await auth() 20 | 21 | if (!session) { 22 | redirect("/auth/login?callbackUrl=/admin/users") 23 | } 24 | 25 | await protectPage({ permission: "admin:all" }) 26 | 27 | const usersPromise = getUsers() 28 | 29 | return ( 30 | <> 31 | } 35 | /> 36 | 37 | }> 38 | 39 | 40 | 41 | 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /src/app/(admin)/layout.tsx: -------------------------------------------------------------------------------- 1 | import MaxWidthWrapper from "@/components/max-width-wrapper" 2 | import NavTabs from "@/components/navbar/nav-tabs" 3 | import Navbar from "@/components/navbar/navbar" 4 | import { SiteFooter } from "@/components/site-footer" 5 | 6 | export interface INavigation { 7 | title: string 8 | href: string 9 | type: "parent" | "child" 10 | } 11 | 12 | const navigation: INavigation[] = [ 13 | { title: "Overview", href: "/admin", type: "parent" }, 14 | { title: "Users", href: "/admin/users", type: "child" }, 15 | { title: "Roles", href: "/admin/roles", type: "child" }, 16 | { title: "Permissions", href: "/admin/permissions", type: "child" }, 17 | { title: "Tools", href: "/admin/tools", type: "child" }, 18 | { title: "Tokens", href: "/admin/tokens", type: "child" }, 19 | ] 20 | 21 | export default function AdminLayout({ children }: { children: React.ReactNode }) { 22 | return ( 23 | <> 24 | 25 | 26 |
27 | {children} 28 |
29 | 30 | 31 | ) 32 | } 33 | -------------------------------------------------------------------------------- /src/app/(auth)/auth/change-email/[token]/loading.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image" 2 | 3 | import { Loader2 } from "lucide-react" 4 | 5 | import Logo from "@/components/logo" 6 | import MaxWidthWrapper from "@/components/max-width-wrapper" 7 | 8 | export default function LoadingChangeEmail() { 9 | return ( 10 | 11 |
12 | 13 | changing email 21 | changing email 29 |
30 | 31 |

32 | Changing email 33 |

34 |
35 |
36 |
37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /src/app/(auth)/auth/change-email/[token]/page.tsx: -------------------------------------------------------------------------------- 1 | import { changeEmail } from "@/actions/auth/change-email" 2 | 3 | type Params = Promise<{ token: string }> 4 | 5 | export default async function ChangeEmailTokenPage(props: { params: Params }) { 6 | const params = await props.params 7 | await changeEmail({ token: params.token }) 8 | } 9 | -------------------------------------------------------------------------------- /src/app/(auth)/auth/confirm/[token]/loading.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image" 2 | 3 | import { Loader2 } from "lucide-react" 4 | 5 | import Logo from "@/components/logo" 6 | import MaxWidthWrapper from "@/components/max-width-wrapper" 7 | 8 | export default function LoadingConfirmEmail() { 9 | return ( 10 | 11 |
12 | 13 | confirm email 21 | confirm email 29 |
30 | 31 |

32 | Confirm email 33 |

34 |
35 |
36 |
37 | ) 38 | } 39 | -------------------------------------------------------------------------------- /src/app/(auth)/auth/confirm/[token]/page.tsx: -------------------------------------------------------------------------------- 1 | import { confirmEmail } from "@/actions/auth/confirm-email" 2 | 3 | type Params = Promise<{ token: string }> 4 | 5 | export default async function ConfirmTokenPage(props: { params: Params }) { 6 | const params = await props.params 7 | await confirmEmail({ token: params.token }) 8 | } 9 | -------------------------------------------------------------------------------- /src/app/(auth)/auth/logout/logout.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useRouter, useSearchParams } from "next/navigation" 4 | 5 | import { useEffect } from "react" 6 | 7 | import { signOut } from "next-auth/react" 8 | 9 | export function Logout() { 10 | const router = useRouter() 11 | const searchParams = useSearchParams() 12 | const callbackUrl: string = (searchParams.get("callbackUrl") as string) ?? "/" 13 | 14 | useEffect(() => { 15 | const handleLogout = async () => { 16 | await signOut({ redirect: true, callbackUrl: callbackUrl }) 17 | } 18 | handleLogout() 19 | }, [router, callbackUrl]) 20 | 21 | return null 22 | } 23 | -------------------------------------------------------------------------------- /src/app/(auth)/auth/logout/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation" 2 | 3 | import { Suspense } from "react" 4 | 5 | import { auth } from "@/auth" 6 | 7 | import { logActivity } from "@/lib/activity" 8 | 9 | import { ActivityType } from "@/schemas/activity-logs" 10 | 11 | import { Logout } from "./logout" 12 | 13 | export default async function LogoutPage() { 14 | const session = await auth() 15 | 16 | if (!session) { 17 | redirect("/auth/login") 18 | } 19 | 20 | await logActivity(session.user.userId, ActivityType.SIGN_OUT) 21 | 22 | return ( 23 | 24 | 25 | 26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /src/app/(healthcheck)/healthcheck/page.tsx: -------------------------------------------------------------------------------- 1 | import Image from "next/image" 2 | import Link from "next/link" 3 | 4 | import { ArrowLeft } from "lucide-react" 5 | 6 | import { Button } from "@/components/ui/button" 7 | 8 | import Logo from "@/components/logo" 9 | import MaxWidthWrapper from "@/components/max-width-wrapper" 10 | 11 | export default function Healthcheck() { 12 | return ( 13 | 14 |
15 | 16 | missing site 23 | missing site 30 |

31 | Under construction 32 |

33 | 39 |
40 |
41 | ) 42 | } 43 | -------------------------------------------------------------------------------- /src/app/(home)/layout.tsx: -------------------------------------------------------------------------------- 1 | import MaxWidthWrapper from "@/components/max-width-wrapper" 2 | import Navbar from "@/components/navbar/navbar" 3 | 4 | export default function HomeLayout({ children }: { children: React.ReactNode }) { 5 | return ( 6 | <> 7 | 8 |
9 | {children} 10 |
11 | 12 | ) 13 | } 14 | -------------------------------------------------------------------------------- /src/app/(root)/about/page.tsx: -------------------------------------------------------------------------------- 1 | export default function About() { 2 | return ( 3 | <> 4 |

5 | About 6 |

7 | 8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /src/app/(root)/layout.tsx: -------------------------------------------------------------------------------- 1 | import MaxWidthWrapper from "@/components/max-width-wrapper" 2 | import Navbar from "@/components/navbar/navbar" 3 | import { SiteFooter } from "@/components/site-footer" 4 | 5 | export default function RootLayout({ children }: { children: React.ReactNode }) { 6 | return ( 7 | <> 8 | 9 |
10 | {children} 11 |
12 | 13 | 14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /src/app/(root)/profile/page.tsx: -------------------------------------------------------------------------------- 1 | export default function About() { 2 | return ( 3 | <> 4 |

5 | Profile 6 |

7 | 8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /src/app/(root)/settings/layout.tsx: -------------------------------------------------------------------------------- 1 | import { PageHeader } from "@/components/page-header" 2 | 3 | export default function Layout({ children }: { children: React.ReactNode }) { 4 | return ( 5 | <> 6 | 7 | {children} 8 | 9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /src/app/(root)/settings/loading.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from "@/components/ui/skeleton" 2 | 3 | export default function LoadingSettingsPage() { 4 | return ( 5 |
6 | 7 | 8 | 9 |
10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /src/app/(root)/settings/page.tsx: -------------------------------------------------------------------------------- 1 | import { getCurrentUser } from "@/actions/users/get-current-user" 2 | 3 | import DeleteAccount from "./_components/delete-account-form" 4 | import EmailForm from "./_components/email-form" 5 | import UsernameForm from "./_components/username-form" 6 | 7 | export default async function SettingsPage() { 8 | const currentUser = await getCurrentUser("/auth/login?callbackUrl=/settings") 9 | 10 | return ( 11 |
12 | 13 | 14 | 15 |
16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /src/app/(tools)/layout.tsx: -------------------------------------------------------------------------------- 1 | import Navbar from "@/components/navbar/navbar" 2 | import { SiteFooter } from "@/components/site-footer" 3 | 4 | export default function ToolsLayout({ children }: { children: React.ReactNode }) { 5 | return ( 6 | <> 7 | 8 |
{children}
9 | 10 | 11 | ) 12 | } 13 | -------------------------------------------------------------------------------- /src/app/(tools)/tools/_components/add-tool-button.tsx: -------------------------------------------------------------------------------- 1 | import { Plus } from "lucide-react" 2 | 3 | import { Button } from "@/components/ui/button" 4 | import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip" 5 | 6 | export default function AddToolButton() { 7 | return ( 8 | 9 | 10 | 15 | 16 | 17 |

Add new tool

18 |
19 |
20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /src/app/(tools)/tools/_components/card-skeleton.tsx: -------------------------------------------------------------------------------- 1 | import GridCard from "./grid-card" 2 | import ListCard from "./list-card" 3 | 4 | export default function CardSkeleton({ view }: { view: string }) { 5 | return ( 6 |
7 | {view === "grid" && ( 8 |
9 | {Array(6) 10 | .fill(null) 11 | .map((_, i) => ( 12 | 13 | ))} 14 |
15 | )} 16 | {view === "list" && ( 17 |
18 | {Array(6) 19 | .fill(null) 20 | .map((_, i) => ( 21 | 22 | ))} 23 |
24 | )} 25 |
26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /src/app/(tools)/tools/_components/tool-empty-state.tsx: -------------------------------------------------------------------------------- 1 | import { LayoutGrid } from "lucide-react" 2 | 3 | import EmptyState from "@/components/ui/data-tables/empty-state" 4 | 5 | export default function ToolEmptyState() { 6 | return ( 7 |
8 | 13 |
14 | ) 15 | } 16 | -------------------------------------------------------------------------------- /src/app/(tools)/tools/_components/tools-section.tsx: -------------------------------------------------------------------------------- 1 | import { getCurrentUser } from "@/actions/users/get-current-user" 2 | 3 | import { getUserTools } from "@/data/user" 4 | 5 | import ToolEmptyState from "./tool-empty-state" 6 | import ToolsCards from "./tools-cards" 7 | 8 | export default async function ToolsSection({ 9 | view, 10 | search, 11 | }: { 12 | view: string 13 | search: string 14 | }) { 15 | const currentUser = await getCurrentUser() 16 | 17 | const data = (await getUserTools(currentUser?.id!, search)) || [] 18 | 19 | const favoriteTools = data?.filter((tool) => tool.isFavorite === true) || [] 20 | 21 | if (!data.length) { 22 | return 23 | } 24 | 25 | return 26 | } 27 | -------------------------------------------------------------------------------- /src/app/(tools)/tools/blog/layout.tsx: -------------------------------------------------------------------------------- 1 | import MaxWidthWrapper from "@/components/max-width-wrapper" 2 | 3 | export default function BlogLayout({ children }: { children: React.ReactNode }) { 4 | return ( 5 | <> 6 | {children} 7 | 8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /src/app/(tools)/tools/blog/page.tsx: -------------------------------------------------------------------------------- 1 | export default function BlogPage() { 2 | return
My blog page
3 | } 4 | -------------------------------------------------------------------------------- /src/app/(tools)/tools/feedbacks/_components/data-table-column-header.tsx: -------------------------------------------------------------------------------- 1 | import { Column } from "@tanstack/react-table" 2 | import { ArrowDown, ArrowUp, ArrowUpDown } from "lucide-react" 3 | 4 | import { cn } from "@/lib/utils" 5 | 6 | import { Button } from "@/components/ui/button" 7 | 8 | interface DataTableColumnHeaderProps 9 | extends React.HTMLAttributes { 10 | column: Column 11 | title: string 12 | } 13 | 14 | export function DataTableColumnHeader({ 15 | column, 16 | title, 17 | className, 18 | }: DataTableColumnHeaderProps) { 19 | if (!column.getCanSort()) { 20 | return
{title}
21 | } 22 | 23 | return ( 24 | 38 | ) 39 | } 40 | -------------------------------------------------------------------------------- /src/app/(tools)/tools/feedbacks/_components/data-table-utils-ssr.tsx: -------------------------------------------------------------------------------- 1 | import { SortingState } from "@tanstack/react-table" 2 | 3 | export interface IDataTableSearchParamsSSR { 4 | q?: string 5 | page?: string 6 | per_page?: string 7 | sort?: string 8 | } 9 | 10 | export function getSearchParamsSSR( 11 | searchParams: IDataTableSearchParamsSSR, 12 | defaultSort: string, 13 | ) { 14 | // Search 15 | const search: string | undefined = searchParams?.q || undefined 16 | 17 | // Pages 18 | const defaultPerPage: number = 10 19 | const currentPage: number = Number(searchParams?.page) || 1 20 | const limit: number = Number(searchParams?.per_page) || defaultPerPage 21 | const offset: number = (currentPage - 1) * limit 22 | 23 | // Sort 24 | const sort: string = searchParams?.sort || defaultSort 25 | const sortState: SortingState = createSortingState(sort) 26 | const orderBy: { 27 | [x: string]: string 28 | }[] = sortState.map((sortObject) => ({ 29 | [sortObject.id]: sortObject.desc ? "desc" : "asc", 30 | })) 31 | 32 | return { search, currentPage, limit, offset, sort, orderBy, sortState } 33 | } 34 | 35 | function createSortingState(sort: string): SortingState { 36 | const sortObjects: SortingState = sort.split(",").map((sortField) => { 37 | const desc: boolean = sortField.startsWith("-") 38 | const id: string = desc ? sortField.substring(1) : sortField 39 | return { id, desc } 40 | }) 41 | return sortObjects 42 | } 43 | -------------------------------------------------------------------------------- /src/app/(tools)/tools/feedbacks/_components/empty-state.tsx: -------------------------------------------------------------------------------- 1 | import { FilePlus, LucideIcon } from "lucide-react" 2 | 3 | type Props = { 4 | title?: string 5 | description?: string 6 | action?: React.ReactNode 7 | icon?: LucideIcon 8 | } 9 | 10 | export default function EmptyState({ 11 | title = "No data available", 12 | description = "You can add new data to get started", 13 | action, 14 | icon = FilePlus, 15 | }: Props) { 16 | const Icon = icon 17 | return ( 18 |
19 | 20 |
21 |

{title}

22 |

{description}

23 |
24 | {action ?
{action}
: null} 25 |
26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /src/app/(tools)/tools/feedbacks/_components/feedbacks-table.tsx: -------------------------------------------------------------------------------- 1 | import { SortingState } from "@tanstack/react-table" 2 | import { MessageSquareHeart } from "lucide-react" 3 | 4 | import { getFeedbacksSSR } from "@/data/feedback" 5 | 6 | import { columns } from "./columns" 7 | import { DataTableSSR } from "./data-table-ssr" 8 | import EmptyState from "./empty-state" 9 | 10 | interface DataTableProps { 11 | currentPage: number 12 | sortState: SortingState 13 | search?: string 14 | limit: number 15 | offset: number 16 | orderBy: { 17 | [x: string]: string 18 | }[] 19 | } 20 | 21 | export default async function FeedbacksDataTableSSR({ 22 | currentPage, 23 | sortState, 24 | search, 25 | limit, 26 | offset, 27 | orderBy, 28 | }: DataTableProps) { 29 | // Get data 30 | const { data, pageCount } = await getFeedbacksSSR(offset, limit, orderBy, search) 31 | 32 | return ( 33 | 44 | } 45 | /> 46 | ) 47 | } 48 | -------------------------------------------------------------------------------- /src/app/(tools)/tools/feedbacks/layout.tsx: -------------------------------------------------------------------------------- 1 | import MaxWidthWrapper from "@/components/max-width-wrapper" 2 | 3 | export default function FeedbackLayout({ children }: { children: React.ReactNode }) { 4 | return {children} 5 | } 6 | -------------------------------------------------------------------------------- /src/app/(tools)/tools/feedbacks/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation" 2 | 3 | import { Suspense } from "react" 4 | 5 | import { auth } from "@/auth" 6 | 7 | import { protectPage } from "@/lib/rbac" 8 | 9 | import { Skeleton } from "@/components/ui/skeleton" 10 | 11 | import { PageHeader } from "@/components/page-header" 12 | 13 | import { getSearchParamsSSR } from "./_components/data-table-utils-ssr" 14 | import FeedbacksDataTableSSR from "./_components/feedbacks-table" 15 | 16 | type SearchParams = Promise<{ [key: string]: string | string[] | undefined }> 17 | 18 | export default async function FeedbackPage(props: { searchParams: SearchParams }) { 19 | const session = await auth() 20 | 21 | if (!session) { 22 | redirect("/auth/login?callbackUrl=/tools/feedbacks") 23 | } 24 | 25 | await protectPage({ permission: "feedbacks:view" }) 26 | 27 | const searchParams = await props.searchParams 28 | 29 | // Get params 30 | const defaultSort: string = "-createdAt" 31 | const { search, currentPage, limit, offset, orderBy, sortState } = getSearchParamsSSR( 32 | searchParams, 33 | defaultSort, 34 | ) 35 | 36 | return ( 37 | <> 38 | 42 | } 45 | > 46 | 54 | 55 | 56 | ) 57 | } 58 | -------------------------------------------------------------------------------- /src/app/(tools)/tools/page.tsx: -------------------------------------------------------------------------------- 1 | import { redirect } from "next/navigation" 2 | 3 | import { Suspense } from "react" 4 | 5 | import { auth } from "@/auth" 6 | 7 | import MaxWidthWrapper from "@/components/max-width-wrapper" 8 | 9 | import AddToolButton from "./_components/add-tool-button" 10 | import CardSkeleton from "./_components/card-skeleton" 11 | import ToolSearch from "./_components/tool-search" 12 | import ToolsSection from "./_components/tools-section" 13 | import ViewSwitch from "./_components/view-switch" 14 | 15 | type SearchParams = Promise<{ q?: string; view?: string }> 16 | 17 | export default async function ToolsPage(props: { searchParams: SearchParams }) { 18 | const session = await auth() 19 | 20 | if (!session) { 21 | redirect("/auth/login?callbackUrl=/tools") 22 | } 23 | 24 | const searchParams = await props.searchParams 25 | const search = searchParams?.q || "" 26 | const view = searchParams?.view || "grid" 27 | 28 | return ( 29 | <> 30 |
31 | 32 |

33 | My Tools 34 |

35 |
36 |
37 |
38 | 39 |
40 | 41 | 42 | 43 |
44 | }> 45 | 46 | 47 |
48 |
49 | 50 | ) 51 | } 52 | -------------------------------------------------------------------------------- /src/app/api/auth/[...nextauth]/route.ts: -------------------------------------------------------------------------------- 1 | import { handlers } from "@/auth" 2 | 3 | export const { GET, POST } = handlers 4 | -------------------------------------------------------------------------------- /src/app/api/auth/confirm/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server" 2 | 3 | import ConfirmEmail from "@/emails/confirm-email" 4 | import { env } from "@/env" 5 | import { render } from "@react-email/render" 6 | 7 | import { generateUserToken } from "@/lib/jwt" 8 | import { sendMail } from "@/lib/mail" 9 | import prismadb from "@/lib/prismadb" 10 | 11 | export async function POST(req: Request) { 12 | try { 13 | const body = await req.json() 14 | 15 | const { email } = body 16 | 17 | const userExist = await prismadb.user.findFirst({ where: { email } }) 18 | 19 | if (!userExist) { 20 | return NextResponse.json({ message: "Email not found" }, { status: 404 }) 21 | } 22 | 23 | if (!userExist.isActive) { 24 | return NextResponse.json({ message: "Something went wrong" }, { status: 404 }) 25 | } 26 | 27 | const token: string = generateUserToken(email) 28 | 29 | const url: string = `${env.AUTH_URL}/auth/confirm/${token}` 30 | 31 | const emailHtml = await render(ConfirmEmail({ url })) 32 | 33 | await sendMail(email, "Active account", emailHtml) 34 | 35 | return NextResponse.json({ message: "Email sent" }, { status: 200 }) 36 | } catch (error: any) { 37 | return NextResponse.json({ error: error.message }, { status: 500 }) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/app/api/auth/register/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server" 2 | 3 | import bcrypt from "bcrypt" 4 | 5 | import prismadb from "@/lib/prismadb" 6 | 7 | export async function POST(req: Request) { 8 | try { 9 | const body = await req.json() 10 | 11 | const { username, email, password } = body 12 | 13 | const errors = [] 14 | 15 | const usernameExists = await prismadb.user.findUnique({ where: { username } }) 16 | 17 | if (usernameExists) { 18 | errors.push({ message: "Username already exists", field: "username" }) 19 | } 20 | 21 | const emailExists = await prismadb.user.findUnique({ where: { email } }) 22 | 23 | if (emailExists) { 24 | errors.push({ message: "Email already exists", field: "email" }) 25 | } 26 | 27 | if (errors.length) { 28 | return NextResponse.json(errors, { status: 400 }) 29 | } 30 | const hashedPassword = await bcrypt.hash(password, 12) 31 | 32 | await prismadb.user.create({ 33 | data: { 34 | username, 35 | email, 36 | password: hashedPassword, 37 | isActive: true, 38 | emailVerified: false, 39 | }, 40 | }) 41 | 42 | return NextResponse.json({ message: "User created successfully" }, { status: 201 }) 43 | } catch { 44 | return NextResponse.json({ error: "Something went wrong" }, { status: 500 }) 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/app/api/auth/reset-password/[token]/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server" 2 | 3 | import bcrypt from "bcrypt" 4 | 5 | import { verifyUserToken } from "@/lib/jwt" 6 | import prismadb from "@/lib/prismadb" 7 | 8 | import { getUserByEmail } from "@/data/user" 9 | 10 | type Params = Promise<{ token: string }> 11 | 12 | export async function POST(req: Request, segmentData: { params: Params }) { 13 | try { 14 | const params = await segmentData.params 15 | 16 | const body = await req.json() 17 | const { password } = body 18 | const { token } = params 19 | 20 | const email = verifyUserToken(token) 21 | 22 | if (!email) { 23 | return NextResponse.json( 24 | { message: "Token is invalid or expired" }, 25 | { status: 401 }, 26 | ) 27 | } 28 | 29 | const existingUser = await getUserByEmail(email) 30 | 31 | if (!existingUser) { 32 | return NextResponse.json({ error: "Email does not exists" }, { status: 404 }) 33 | } 34 | 35 | const hashedPassword = await bcrypt.hash(password, 12) 36 | 37 | await prismadb.user.update({ 38 | where: { 39 | id: existingUser.id, 40 | }, 41 | data: { 42 | password: hashedPassword, 43 | }, 44 | }) 45 | return NextResponse.json({ message: "Password updated" }, { status: 200 }) 46 | } catch (error: any) { 47 | return NextResponse.json({ error: error.message }, { status: 500 }) 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/app/api/auth/reset-password/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server" 2 | 3 | import ResetPasswordEmail from "@/emails/reset-email" 4 | import { env } from "@/env" 5 | import { render } from "@react-email/render" 6 | 7 | import { generateUserToken } from "@/lib/jwt" 8 | import { sendMail } from "@/lib/mail" 9 | 10 | import { getUserByEmail } from "@/data/user" 11 | 12 | export async function POST(req: Request) { 13 | try { 14 | const body = await req.json() 15 | 16 | const { email } = body 17 | 18 | const existingUser = await getUserByEmail(email) 19 | 20 | if (!existingUser) { 21 | return NextResponse.json({ error: "Email does not exists" }, { status: 404 }) 22 | } 23 | 24 | const token: string = generateUserToken(email) 25 | 26 | const url: string = `${env.AUTH_URL}/auth/reset-password/${token}` 27 | 28 | const emailHtml = await render(ResetPasswordEmail({ url })) 29 | 30 | await sendMail(email, "Reset password", emailHtml) 31 | 32 | return NextResponse.json({ message: "Email sent" }, { status: 201 }) 33 | } catch (error: any) { 34 | return NextResponse.json({ error: error.message }, { status: 500 }) 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/app/api/docs/page.tsx: -------------------------------------------------------------------------------- 1 | import SwaggerUI from "swagger-ui-react" 2 | import "swagger-ui-react/swagger-ui.css" 3 | 4 | import { document } from "@/lib/openapi" 5 | 6 | export const dynamic = "force-static" 7 | 8 | export default async function ApiDocsPage() { 9 | const spec = { ...document } 10 | 11 | return 12 | } 13 | -------------------------------------------------------------------------------- /src/app/api/openapi/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server" 2 | 3 | import { document } from "@/lib/openapi" 4 | 5 | export function GET() { 6 | return NextResponse.json({ ...document }) 7 | } 8 | -------------------------------------------------------------------------------- /src/app/api/permissions/options/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server" 2 | 3 | import { withAdmin } from "@/lib/auth" 4 | 5 | import { getPermissionOptions } from "@/data/permission" 6 | 7 | /** 8 | * Handles GET requests to retrieve permission options for a permissions multiple selector in the role page. 9 | * 10 | * This endpoint is intended for use in the admin tool only. 11 | * 12 | **/ 13 | export const GET = withAdmin(async ({ context }) => { 14 | try { 15 | const searchParams = context.searchParams 16 | const search: string | undefined = searchParams["search"] || undefined 17 | 18 | const data = await getPermissionOptions(search) 19 | 20 | return NextResponse.json(data, { status: 200 }) 21 | } catch (error) { 22 | console.error("Error fetching permissions:", error) 23 | return NextResponse.json({ message: "Internal Server Error" }, { status: 500 }) 24 | } 25 | }) 26 | -------------------------------------------------------------------------------- /src/app/api/tools/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from "next/server" 2 | 3 | import { getCurrentUser } from "@/actions/users/get-current-user" 4 | 5 | import { getUserTools } from "@/data/user" 6 | 7 | export async function GET() { 8 | const currentUser = await getCurrentUser() 9 | 10 | if (currentUser) { 11 | const data = await getUserTools(currentUser.id) 12 | 13 | return NextResponse.json(data, { status: 200 }) 14 | } 15 | 16 | return NextResponse.json({ message: "Unauthorized" }, { status: 401 }) 17 | } 18 | -------------------------------------------------------------------------------- /src/app/layout.tsx: -------------------------------------------------------------------------------- 1 | import type { Metadata } from "next" 2 | import { Geist, Geist_Mono } from "next/font/google" 3 | 4 | import { Analytics } from "@vercel/analytics/react" 5 | import { SpeedInsights } from "@vercel/speed-insights/next" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | import { Toaster } from "@/components/ui/sonner" 10 | 11 | import Providers from "@/components/providers" 12 | 13 | import "@/styles/globals.css" 14 | 15 | const geistSans = Geist({ 16 | variable: "--font-geist-sans", 17 | subsets: ["latin"], 18 | }) 19 | 20 | const geistMono = Geist_Mono({ 21 | variable: "--font-geist-mono", 22 | subsets: ["latin"], 23 | }) 24 | 25 | export const metadata: Metadata = { 26 | title: "Quark", 27 | description: "Quark template", 28 | } 29 | 30 | export default function RootLayout({ children }: { children: React.ReactNode }) { 31 | return ( 32 | 33 | 41 |
42 | 43 |
44 | {children} 45 | 46 | 47 |
48 | 49 |
50 |
51 | 52 | 53 | ) 54 | } 55 | -------------------------------------------------------------------------------- /src/app/not-found.tsx: -------------------------------------------------------------------------------- 1 | import NotFound404 from "@/components/404" 2 | import MaxWidthWrapper from "@/components/max-width-wrapper" 3 | 4 | export default function NotFoundPage() { 5 | return ( 6 | <> 7 | 8 | 14 | 15 | 16 | ) 17 | } 18 | -------------------------------------------------------------------------------- /src/components/404.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import Image from "next/image" 4 | import Link from "next/link" 5 | 6 | import React from "react" 7 | 8 | import { ArrowLeft } from "lucide-react" 9 | 10 | import Logo from "./logo" 11 | import MaxWidthWrapper from "./max-width-wrapper" 12 | import { Button } from "./ui/button" 13 | 14 | export default function NotFound404({ 15 | message, 16 | linkText, 17 | link, 18 | showLogo = false, 19 | }: { 20 | message: string 21 | linkText: string 22 | link: string 23 | showLogo?: boolean 24 | }) { 25 | return ( 26 | 27 |
28 | {showLogo && } 29 | missing site 36 | missing site 43 |

{message}

44 | 50 |
51 |
52 | ) 53 | } 54 | -------------------------------------------------------------------------------- /src/components/admin/section-loading.tsx: -------------------------------------------------------------------------------- 1 | import { PageSection } from "../page-header" 2 | import TableLoading from "./table-loading" 3 | 4 | export default function SectionLoading() { 5 | return ( 6 | <> 7 | 8 | 9 | 10 | ) 11 | } 12 | -------------------------------------------------------------------------------- /src/components/admin/sidebar-nav.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import Link from "next/link" 4 | import { usePathname } from "next/navigation" 5 | 6 | import { NavItem } from "@/types/types" 7 | 8 | import { cn } from "@/lib/utils" 9 | 10 | import { buttonVariants } from "@/components/ui/button" 11 | 12 | interface SidebarNavProps extends React.HTMLAttributes { 13 | items: NavItem[] 14 | } 15 | 16 | export function SidebarNav({ className, items, ...props }: SidebarNavProps) { 17 | const pathname = usePathname() 18 | 19 | return ( 20 | 42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /src/components/admin/table-loading.tsx: -------------------------------------------------------------------------------- 1 | import { Skeleton } from "@/components/ui/skeleton" 2 | 3 | export default function TableLoading() { 4 | return ( 5 |
6 | 7 |
8 | ) 9 | } 10 | -------------------------------------------------------------------------------- /src/components/auth/auth-template.tsx: -------------------------------------------------------------------------------- 1 | import Logo from "../logo" 2 | 3 | export default function AuthTemplate({ children }: { children: React.ReactNode }) { 4 | return ( 5 |
6 |
7 |
8 |
9 | 10 |
11 |
12 |
13 |
14 |
15 | {children} 16 |
17 |
18 |
19 | ) 20 | } 21 | -------------------------------------------------------------------------------- /src/components/back-link-button.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link" 2 | 3 | import React from "react" 4 | 5 | import { ArrowLeft } from "lucide-react" 6 | 7 | import { Button } from "./ui/button" 8 | 9 | export default function BackLinkButton({ link }: { link: string }) { 10 | return ( 11 | 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /src/components/copy-clipboard-button.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { Check, Copy } from "lucide-react" 4 | import { toast } from "sonner" 5 | 6 | import { useCopyToClipboard } from "@/lib/hooks/use-copy-to-clipboard" 7 | import { cn } from "@/lib/utils" 8 | 9 | import { Button } from "@/components/ui/button" 10 | 11 | import { Tooltip, TooltipContent, TooltipTrigger } from "./ui/tooltip" 12 | 13 | interface IProps extends React.ComponentProps<"div"> { 14 | textToCopy: string 15 | label?: string 16 | successMessage?: string 17 | } 18 | 19 | export function CopyButtonData({ 20 | textToCopy, 21 | label = "Copy", 22 | successMessage = "Copied to clipboard!", 23 | className, 24 | ...props 25 | }: IProps) { 26 | const { isCopied, copyToClipboard } = useCopyToClipboard() 27 | 28 | const onCopy = () => { 29 | if (isCopied) return 30 | 31 | try { 32 | copyToClipboard(textToCopy) 33 | toast.info(successMessage) 34 | } catch { 35 | toast.error("Failed to copy. Please try again.") 36 | } 37 | } 38 | 39 | return ( 40 |
41 | 42 | 43 | 51 | 52 | {label} 53 | 54 |
55 | ) 56 | } 57 | -------------------------------------------------------------------------------- /src/components/copy-clipboard-dropdown-menu-item.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { Copy } from "lucide-react" 4 | import { toast } from "sonner" 5 | 6 | import { useCopyToClipboard } from "@/lib/hooks/use-copy-to-clipboard" 7 | 8 | import { DropdownMenuItem } from "./ui/dropdown-menu" 9 | 10 | interface IProps extends React.ComponentProps<"div"> { 11 | textToCopy: string 12 | label?: string 13 | successMessage?: string 14 | } 15 | 16 | export function CopyClipboardDropdownMenuItem({ 17 | textToCopy, 18 | label = "Copy ID", 19 | successMessage = "Copied to clipboard!", 20 | }: IProps) { 21 | const { isCopied, copyToClipboard } = useCopyToClipboard() 22 | 23 | const onCopy = () => { 24 | if (isCopied) return 25 | 26 | try { 27 | copyToClipboard(textToCopy) 28 | toast.info(successMessage) 29 | } catch { 30 | toast.error("Failed to copy. Please try again.") 31 | } 32 | } 33 | 34 | return ( 35 | onCopy()}> 36 | 37 | {label} 38 | 39 | ) 40 | } 41 | -------------------------------------------------------------------------------- /src/components/health-status-button.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link" 2 | 3 | import { Button } from "./ui/button" 4 | 5 | export default function HealthStatusButton() { 6 | return ( 7 | 20 | ) 21 | } 22 | -------------------------------------------------------------------------------- /src/components/max-width-wrapper.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | export default function MaxWidthWrapper({ 4 | children, 5 | className, 6 | ...props 7 | }: React.HTMLAttributes) { 8 | return ( 9 |
13 | {children} 14 |
15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /src/components/navbar/login-button.tsx: -------------------------------------------------------------------------------- 1 | import Link from "next/link" 2 | 3 | import { Button } from "@/components/ui/button" 4 | 5 | export default function LoginButton() { 6 | return ( 7 |
8 | 11 | 14 |
15 | ) 16 | } 17 | -------------------------------------------------------------------------------- /src/components/navbar/navbar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useEffect, useState } from "react" 4 | 5 | import { useMotionValueEvent, useScroll } from "framer-motion" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | import DesktopNav from "./desktop-nav" 10 | import MobileNav from "./mobile-nav" 11 | 12 | export interface INavigation { 13 | name: string 14 | href: string 15 | } 16 | const navigation: INavigation[] = [ 17 | { name: "Home", href: "/" }, 18 | { name: "About", href: "/about" }, 19 | ] 20 | 21 | export default function Navbar() { 22 | const [showStickyNav, setShowStickyNav] = useState(false) 23 | const [mounted, setMounted] = useState(false) 24 | 25 | const { scrollY } = useScroll() 26 | const [y, setY] = useState(0) 27 | 28 | useMotionValueEvent(scrollY, "change", (latest) => { 29 | setY(latest) 30 | }) 31 | 32 | useEffect(() => { 33 | if (!mounted) { 34 | setMounted(true) 35 | } else { 36 | const isNavTabsUsed = document.querySelector("#nav-tabs") !== null 37 | setShowStickyNav(!isNavTabsUsed) 38 | } 39 | }, [mounted]) 40 | 41 | return ( 42 |
56 && 48 | "after:bg-border after:absolute after:bottom-0 after:left-0 after:h-px after:w-full after:content-['']", 49 | )} 50 | > 51 | 52 | 53 |
54 | ) 55 | } 56 | -------------------------------------------------------------------------------- /src/components/navbar/user-avatar.tsx: -------------------------------------------------------------------------------- 1 | import { useSession } from "next-auth/react" 2 | 3 | import { Avatar, AvatarFallback, AvatarImage } from "@/components/ui/avatar" 4 | 5 | export default function UserAvatar() { 6 | const { data: session } = useSession() 7 | 8 | return ( 9 | 10 | 11 | {session?.user?.email?.charAt(0).toUpperCase()} 12 | 13 | ) 14 | } 15 | -------------------------------------------------------------------------------- /src/components/providers.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { SessionProvider } from "next-auth/react" 4 | import { ThemeProvider } from "next-themes" 5 | 6 | import { TooltipProvider } from "./ui/tooltip" 7 | 8 | export default function Providers({ children }: { children: React.ReactNode }) { 9 | return ( 10 | <> 11 | 12 | 18 | {children} 19 | 20 | 21 | 22 | ) 23 | } 24 | -------------------------------------------------------------------------------- /src/components/site-footer.tsx: -------------------------------------------------------------------------------- 1 | import { CommandMenu } from "./command-menu" 2 | import HealthStatusButton from "./health-status-button" 3 | import Logo from "./logo" 4 | import MaxWidthWrapper from "./max-width-wrapper" 5 | import ThemeSwitch from "./theme-switch" 6 | 7 | export function SiteFooter() { 8 | return ( 9 |
10 | 11 |
12 |
13 | 14 |

2025

15 | 16 |
17 |
18 |
19 | 20 |
21 | 22 |
23 |
24 |
25 |
26 | ) 27 | } 28 | -------------------------------------------------------------------------------- /src/components/ui/avatar.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as AvatarPrimitive from "@radix-ui/react-avatar" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | function Avatar({ 9 | className, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 21 | ) 22 | } 23 | 24 | function AvatarImage({ 25 | className, 26 | ...props 27 | }: React.ComponentProps) { 28 | return ( 29 | 34 | ) 35 | } 36 | 37 | function AvatarFallback({ 38 | className, 39 | ...props 40 | }: React.ComponentProps) { 41 | return ( 42 | 50 | ) 51 | } 52 | 53 | export { Avatar, AvatarImage, AvatarFallback } 54 | -------------------------------------------------------------------------------- /src/components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as CheckboxPrimitive from "@radix-ui/react-checkbox" 5 | import { CheckIcon } from "lucide-react" 6 | 7 | import { cn } from "@/lib/utils" 8 | 9 | function Checkbox({ 10 | className, 11 | ...props 12 | }: React.ComponentProps) { 13 | return ( 14 | 22 | 26 | 27 | 28 | 29 | ) 30 | } 31 | 32 | export { Checkbox } 33 | -------------------------------------------------------------------------------- /src/components/ui/data-tables/data-table-filters.tsx: -------------------------------------------------------------------------------- 1 | import { Table } from "@tanstack/react-table" 2 | import { X } from "lucide-react" 3 | 4 | import { Button } from "../button" 5 | import { Input } from "../input" 6 | 7 | interface DataTablePaginationProps { 8 | table: Table 9 | searchField: string 10 | searchFieldLabel?: string 11 | } 12 | 13 | export function DataTableHeaderFilters({ 14 | table, 15 | searchField, 16 | searchFieldLabel = "", 17 | }: DataTablePaginationProps) { 18 | const column = table.getColumn(searchField) 19 | const filterValue = (column?.getFilterValue() as string) ?? "" 20 | 21 | return ( 22 |
23 | ) => 27 | column?.setFilterValue(e.target.value) 28 | } 29 | className="h-8 w-[150px] pr-8 lg:w-[250px]" 30 | /> 31 | {filterValue && ( 32 | 40 | )} 41 |
42 | ) 43 | } 44 | -------------------------------------------------------------------------------- /src/components/ui/data-tables/data-table-view-options.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { Table } from "@tanstack/react-table" 4 | import { SlidersHorizontal } from "lucide-react" 5 | 6 | import { Button } from "@/components/ui/button" 7 | import { 8 | DropdownMenu, 9 | DropdownMenuCheckboxItem, 10 | DropdownMenuContent, 11 | DropdownMenuLabel, 12 | DropdownMenuSeparator, 13 | DropdownMenuTrigger, 14 | } from "@/components/ui/dropdown-menu" 15 | 16 | interface DataTableViewOptionsProps { 17 | table: Table 18 | } 19 | 20 | export function DataTableViewOptions({ 21 | table, 22 | }: DataTableViewOptionsProps) { 23 | return ( 24 | 25 | 26 | 30 | 31 | 32 | Toggle columns 33 | 34 | {table 35 | .getAllColumns() 36 | .filter( 37 | (column) => typeof column.accessorFn !== "undefined" && column.getCanHide(), 38 | ) 39 | .map((column) => { 40 | return ( 41 | column.toggleVisibility(!!value)} 46 | > 47 | {column.id} 48 | 49 | ) 50 | })} 51 | 52 | 53 | ) 54 | } 55 | -------------------------------------------------------------------------------- /src/components/ui/data-tables/empty-state.tsx: -------------------------------------------------------------------------------- 1 | import { FilePlus, LucideIcon } from "lucide-react" 2 | 3 | type Props = { 4 | title?: string 5 | description?: string 6 | action?: React.ReactNode 7 | icon?: LucideIcon 8 | } 9 | 10 | export default function EmptyState({ 11 | title = "No data available", 12 | description = "You can add new data to get started", 13 | action, 14 | icon = FilePlus, 15 | }: Props) { 16 | const Icon = icon 17 | return ( 18 |
19 |
20 | 21 |
22 |
23 |

{title}

24 |

{description}

25 |
26 | {action ?
{action}
: null} 27 |
28 | ) 29 | } 30 | -------------------------------------------------------------------------------- /src/components/ui/hover-card.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as HoverCardPrimitive from "@radix-ui/react-hover-card" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | function HoverCard({ 9 | ...props 10 | }: React.ComponentProps) { 11 | return 12 | } 13 | 14 | function HoverCardTrigger({ 15 | ...props 16 | }: React.ComponentProps) { 17 | return ( 18 | 19 | ) 20 | } 21 | 22 | function HoverCardContent({ 23 | className, 24 | align = "center", 25 | sideOffset = 4, 26 | ...props 27 | }: React.ComponentProps) { 28 | return ( 29 | 30 | 40 | 41 | ) 42 | } 43 | 44 | export { HoverCard, HoverCardTrigger, HoverCardContent } 45 | -------------------------------------------------------------------------------- /src/components/ui/input.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | function Input({ className, type, ...props }: React.ComponentProps<"input">) { 6 | return ( 7 | 18 | ) 19 | } 20 | 21 | export { Input } 22 | -------------------------------------------------------------------------------- /src/components/ui/label.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as LabelPrimitive from "@radix-ui/react-label" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | function Label({ 9 | className, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 21 | ) 22 | } 23 | 24 | export { Label } 25 | -------------------------------------------------------------------------------- /src/components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as PopoverPrimitive from "@radix-ui/react-popover" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | function Popover({ 9 | ...props 10 | }: React.ComponentProps) { 11 | return 12 | } 13 | 14 | function PopoverTrigger({ 15 | ...props 16 | }: React.ComponentProps) { 17 | return 18 | } 19 | 20 | function PopoverContent({ 21 | className, 22 | align = "center", 23 | sideOffset = 4, 24 | ...props 25 | }: React.ComponentProps) { 26 | return ( 27 | 28 | 38 | 39 | ) 40 | } 41 | 42 | function PopoverAnchor({ 43 | ...props 44 | }: React.ComponentProps) { 45 | return 46 | } 47 | 48 | export { Popover, PopoverTrigger, PopoverContent, PopoverAnchor } 49 | -------------------------------------------------------------------------------- /src/components/ui/separator.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SeparatorPrimitive from "@radix-ui/react-separator" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | function Separator({ 9 | className, 10 | orientation = "horizontal", 11 | decorative = true, 12 | ...props 13 | }: React.ComponentProps) { 14 | return ( 15 | 25 | ) 26 | } 27 | 28 | export { Separator } 29 | -------------------------------------------------------------------------------- /src/components/ui/skeleton.tsx: -------------------------------------------------------------------------------- 1 | import { cn } from "@/lib/utils" 2 | 3 | function Skeleton({ className, ...props }: React.ComponentProps<"div">) { 4 | return ( 5 |
10 | ) 11 | } 12 | 13 | export { Skeleton } 14 | -------------------------------------------------------------------------------- /src/components/ui/sonner.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import { useTheme } from "next-themes" 4 | import { Toaster as Sonner, ToasterProps } from "sonner" 5 | 6 | const Toaster = ({ ...props }: ToasterProps) => { 7 | const { theme = "system" } = useTheme() 8 | 9 | return ( 10 | 22 | ) 23 | } 24 | 25 | export { Toaster } 26 | -------------------------------------------------------------------------------- /src/components/ui/switch.tsx: -------------------------------------------------------------------------------- 1 | "use client" 2 | 3 | import * as React from "react" 4 | import * as SwitchPrimitive from "@radix-ui/react-switch" 5 | 6 | import { cn } from "@/lib/utils" 7 | 8 | function Switch({ 9 | className, 10 | ...props 11 | }: React.ComponentProps) { 12 | return ( 13 | 21 | 27 | 28 | ) 29 | } 30 | 31 | export { Switch } 32 | -------------------------------------------------------------------------------- /src/components/ui/textarea.tsx: -------------------------------------------------------------------------------- 1 | import * as React from "react" 2 | 3 | import { cn } from "@/lib/utils" 4 | 5 | function Textarea({ className, ...props }: React.ComponentProps<"textarea">) { 6 | return ( 7 |