From 6f2523e524e9a64848408287eaa22a23b94123e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Th=C3=A9o=20LAGACHE?= Date: Fri, 16 May 2025 15:54:17 +0200 Subject: [PATCH] migration --- .env.exemple | 33 + .vscode/settings.json | 3 + Makefile | 2 +- README.md | 273 +- .../dashboard/(admin)/admin/page.tsx | 45 +- .../(admin)/agents/[agentId]/edit/page.tsx | 37 +- .../dashboard/(admin)/agents/new/page.tsx | 26 +- .../dashboard/(admin)/agents/page.tsx | 49 +- app/(customer)/dashboard/(admin)/layout.tsx | 22 +- .../databases/[databaseId]/restore/page.tsx | 64 +- app/(customer)/dashboard/[slug]/home/page.tsx | 33 +- .../database/[databaseId]/edit/page.tsx | 43 +- .../database/[databaseId]/page.tsx | 124 +- .../[slug]/projects/[projectId]/edit/page.tsx | 88 +- .../[slug]/projects/[projectId]/page.tsx | 87 +- .../dashboard/[slug]/projects/new/page.tsx | 66 +- .../dashboard/[slug]/projects/page.tsx | 75 +- .../dashboard/[slug]/statistics/page.tsx | 93 +- app/(customer)/dashboard/layout.tsx | 32 +- app/(customer)/dashboard/loading.tsx | 6 +- app/(customer)/dashboard/profile/page.tsx | 80 +- app/(landing)/home/page.tsx | 10 +- app/(landing)/page.tsx | 15 +- app/api/auth/[...all]/route.ts | 4 + app/api/auth/[...nextauth]/route.ts | 4 - app/api/images/[fileName]/route.ts | 14 +- app/api/init/route.ts | 23 +- app/error/page.tsx | 4 +- app/globals.css | 217 +- app/layout.tsx | 25 +- app/not-found.tsx | 7 +- app/providers.tsx | 37 +- docker-compose.yml | 127 +- docker/entrypoints/app-dev-entrypoint.sh | 5 +- docker/entrypoints/app-prod-entrypoint.sh | 2 +- drizzle.config.ts | 16 + emails/Button.tsx | 15 +- emails/EmailLayout.tsx | 32 +- emails/HelloEmail.tsx | 9 +- emails/TestEmailSettings.tsx | 38 +- emails/Text.tsx | 15 +- instrumentation.ts | 10 +- middleware.ts | 57 +- next.config.mjs => next.config.ts | 13 +- package.json | 216 +- postcss.config.mjs | 2 +- .../20241103104432_2024_11_03/migration.sql | 66 - .../20241103192308_2024_11_03/migration.sql | 2 - .../20241110103733_2024_11_10/migration.sql | 46 - .../20241110140612_2024_11_10/migration.sql | 2 - .../20241111152326_2024_11_11/migration.sql | 15 - .../20241113175420_2024_11_13/migration.sql | 33 - .../20241113182543_2024_11_13/migration.sql | 13 - .../20241116144000_2024_11_16/migration.sql | 2 - .../20241116144522_2024_11_16/migration.sql | 6 - .../20241116201945_2024_11_16/migration.sql | 8 - .../20241116203552_2024_11_16/migration.sql | 8 - .../20241117143016_2024_11_17/migration.sql | 118 - .../20241117174438_2024_11_17/migration.sql | 11 - .../20241117184204_2024_11_17/migration.sql | 2 - .../20241117190445_2024_11_17/migration.sql | 2 - .../20241123102722_2024_11_23/migration.sql | 12 - .../20241127175717_2024_11_27/migration.sql | 2 - .../20241127182048_2024_11_27/migration.sql | 8 - .../20241129115417_2024_11_29/migration.sql | 8 - .../20241129165221_2024_11_29/migration.sql | 9 - .../20241129173131_2024_11_29/migration.sql | 3 - .../20241130153218_2024_11_30/migration.sql | 35 - .../20241212103214_2024_12_12/migration.sql | 17 - .../20241212103831_2024_12_12/migration.sql | 32 - .../20241212110141_2024_12_12/migration.sql | 32 - .../20241214165019_2024_12_14/migration.sql | 31 - .../20241217174747_2024_12_17/migration.sql | 45 - .../20241217183424_2024_12_17/migration.sql | 8 - .../20241217183746_2024_12_17/migration.sql | 2 - .../20241217192704_2024_12_17/migration.sql | 9 - .../20241229180505_2024_12_29/migration.sql | 2 - .../20241231084839_2024_12_31/migration.sql | 11 - prisma/migrations/migration_lock.toml | 3 - prisma/schema.prisma | 228 - public/google-icon.png | Bin 16277 -> 0 bytes schema.zmodel | 240 - src/auth/auth.ts | 162 - src/auth/current-user.ts | 29 - src/components/layout.tsx | 19 +- src/components/ui/accordion.tsx | 97 +- src/components/ui/alert-dialog.tsx | 218 +- src/components/ui/alert.tsx | 81 +- src/components/ui/aspect-ratio.tsx | 6 +- src/components/ui/avatar.tsx | 81 +- src/components/ui/badge.tsx | 30 +- src/components/ui/breadcrumb.tsx | 159 +- src/components/ui/button.tsx | 58 +- src/components/ui/calendar.tsx | 117 +- src/components/ui/card.tsx | 148 +- src/components/ui/carousel.tsx | 248 +- src/components/ui/chart.tsx | 392 +- src/components/ui/checkbox.tsx | 42 +- src/components/ui/collapsible.tsx | 28 +- src/components/ui/command.tsx | 4 +- src/components/ui/context-menu.tsx | 364 +- src/components/ui/dialog.tsx | 201 +- src/components/ui/drawer.tsx | 180 +- src/components/ui/dropdown-menu.tsx | 408 +- src/components/ui/hover-card.tsx | 51 +- src/components/ui/input-otp.tsx | 92 +- src/components/ui/input.tsx | 31 +- src/components/ui/label.tsx | 32 +- src/components/ui/menubar.tsx | 456 +- src/components/ui/navigation-menu.tsx | 250 +- src/components/ui/pagination.tsx | 174 +- src/components/ui/popover.tsx | 57 +- src/components/ui/progress.tsx | 41 +- src/components/ui/radio-group.tsx | 39 +- src/components/ui/resizable.tsx | 67 +- src/components/ui/scroll-area.tsx | 86 +- src/components/ui/select.tsx | 287 +- src/components/ui/separator.tsx | 23 +- src/components/ui/sheet.tsx | 221 +- src/components/ui/sidebar.tsx | 879 +- src/components/ui/skeleton.tsx | 8 +- src/components/ui/slider.tsx | 73 +- src/components/ui/sonner.tsx | 22 +- src/components/ui/switch.tsx | 40 +- src/components/ui/table.tsx | 190 +- src/components/ui/tabs.tsx | 97 +- src/components/ui/textarea.tsx | 30 +- src/components/ui/toast.tsx | 8 +- src/components/ui/toggle-group.tsx | 64 +- src/components/ui/toggle.tsx | 38 +- src/components/ui/tooltip.tsx | 71 +- .../auth/PaswordInput/password-input.tsx | 42 +- .../Register/register-form/RegisterForm.tsx | 102 +- .../register-form/register-form.action.ts | 69 +- .../register-form/register-form.schema.ts | 41 +- .../login/buttonAuth/SocialAuthButton.tsx | 54 + .../auth/login/loginForm/LoginForm.tsx | 105 +- .../auth/login/loginForm/login-form.action.ts | 0 .../auth/login/loginForm/login-form.schema.ts | 3 +- .../socialAuthButtons/SocialAuthButton.tsx | 22 - .../CodeSnippet.tsx | 20 +- .../common/button/button-with-confirm.tsx | 52 +- .../common/button/button-with-loading.tsx | 77 +- .../wrappers/common/button/copy-button.tsx | 42 +- .../wrappers/common/cards-with-pagination.tsx | 63 +- src/components/wrappers/common/combobox.tsx | 155 +- .../wrappers/common/connection-circle.tsx | 36 +- .../wrappers/common/day-time-picker.tsx | 129 + .../wrappers/common/daytime-picker.tsx | 770 - src/components/wrappers/common/dialog.tsx | 31 +- .../wrappers/common/file-uploader.tsx | 272 + .../common/loading/LoadingSpinner.tsx | 10 +- .../multi-select.tsx} | 156 +- .../common/pagination/pagination-indexes.tsx | 67 +- .../pagination/pagination-navigation.tsx | 44 +- .../table/data-table-with-pagination.tsx | 47 - .../wrappers/common/table/data-table.tsx | 195 +- .../table/table-pagination-navigation.tsx | 39 +- .../common/table/table-pagination-size.tsx | 14 +- .../common/table/table-pagination.tsx | 21 +- .../common/table/table-sort-button.tsx | 18 + .../common/tooltipCustom/TooltipCustom.tsx | 26 +- .../AdminEmailTab/EmailForm/EmailForm.tsx | 110 +- .../EmailForm/email-form.action.ts | 45 +- .../EmailForm/email-form.schema.ts | 2 +- .../admin/AdminEmailTab/SettingsEmailTab.tsx | 54 +- .../AdminStorageTab/SettingsStorageTab.tsx | 87 +- .../StorageS3Form/StorageS3Form.tsx | 91 +- .../StorageS3Form/s3-form.action.ts | 71 +- .../StorageS3Form/s3-form.schema.ts | 9 +- .../wrappers/dashboard/admin/admin-tabs.tsx | 47 +- .../dashboard/admin/admin-user-table.tsx | 92 +- .../dashboard/admin/columns-users.tsx | 71 +- .../dashboard/agent/AgentCard/AgentCard.tsx | 27 +- .../agent/AgentCardKey/AgentCardKey.tsx | 35 +- .../dashboard/agent/AgentForm/AgentForm.tsx | 99 +- .../agent/AgentForm/agent-form.action.ts | 76 +- .../agent/AgentForm/agent-form.schema.ts | 10 +- .../agent/AgentModalKey/AgentModalKey.tsx | 41 +- .../backup-button/backup-button.action.ts | 76 +- .../backup/backup-button/backup-button.tsx | 41 +- .../CronButton/AdvancedCronSelect.tsx | 22 +- .../database/CronButton/CronButton.tsx | 88 +- .../database/CronButton/CronInput.tsx | 32 +- .../database/CronButton/cron.action.ts | 48 +- .../database/DatabaseForm/DatabaseForm.tsx | 98 +- .../DatabaseForm/form-database.action.ts | 34 +- .../DatabaseForm/form-database.schema.ts | 7 +- .../database/EditButton/EditButton.tsx | 21 +- .../dashboard/database/RestoreForm.tsx | 133 +- .../dashboard/database/restore-form.schema.ts | 39 +- .../loggedInButton/LoggedInButton.tsx | 44 +- .../loggedInDropdown/LoggedInDropdown.tsx | 74 +- .../DeleteOrganizationButton.tsx | 42 +- .../OrganizationForm/OrganizationForm.tsx | 129 +- .../create-organisation-modal.tsx | 93 +- .../organization/organization-combobox.tsx | 92 +- .../organization/organization.action.ts | 278 +- .../organization/organization.schema.ts | 14 +- .../profile/Avatar/AvatarWithUpload.tsx | 78 +- .../dashboard/profile/Avatar/avatar.action.ts | 34 +- .../ButtonDeleteAccount.tsx | 36 +- .../delete-account.action.ts | 63 +- .../profile/UserAvatar/UserAvatar.tsx | 23 +- .../dashboard/profile/UserForm/UserForm.tsx | 105 +- .../profile/UserForm/user-form.action.ts | 34 +- .../ButtonDeleteProject.tsx | 40 +- .../delete-project.action.ts | 82 +- .../projects/Database/DatabaseKpi.tsx | 41 +- .../projects/Database/DatabaseTabs.tsx | 51 +- .../projects/ProjectCard/ProjectCard.tsx | 22 +- .../ProjectCard/ProjectDatabaseCard.tsx | 52 +- .../ProjectsForm/ProjectForm.schema.ts | 3 +- .../projects/ProjectsForm/ProjectForm.tsx | 162 +- .../ProjectsForm/project-form.action.ts | 129 +- .../columns-users-settings.tsx | 59 +- .../SideBarFooterCredit.tsx | 17 +- .../sideBar/SideBarMenu/SideBarMenu.tsx | 246 +- .../sideBar/SideBarMenu/SideBarMenuAdmin.tsx | 69 - .../dashboard/sideBar/app-sidebar.tsx | 103 +- .../dashboard/sideBar/sidebar-content.tsx | 88 + .../charts/evolution-line-chart.tsx | 87 +- .../charts/percentage-line-chart.tsx | 75 +- src/db.ts | 16 - src/db/index.ts | 13 + src/db/migrations/0000_round_king_cobra.sql | 174 + src/db/migrations/0001_small_anthem.sql | 1 + src/db/migrations/meta/0000_snapshot.json | 1162 + src/db/migrations/meta/0001_snapshot.json | 1168 + src/db/migrations/meta/_journal.json | 20 + src/db/schema.ts | 7 + src/db/schema/00_setting.ts | 24 + src/db/schema/01_user.ts | 94 + src/db/schema/02_organization.ts | 80 + src/db/schema/03_project.ts | 34 + src/db/schema/04_agent.ts | 16 + src/db/schema/05_database.ts | 87 + src/db/schema/types.ts | 16 + src/env.mjs | 37 +- src/features/auth/auth.action.ts | 36 - src/features/dashboard/backup/columns.tsx | 131 +- src/features/dashboard/restore/columns.tsx | 46 +- .../dashboard/restore/restore.action.ts | 113 +- src/features/layout/Header.tsx | 25 +- src/features/layout/page.tsx | 34 +- src/features/permissions.ts | 14 - src/features/permissions/permissions.ts | 18 - src/features/upload/private/upload.action.ts | 24 +- src/features/upload/public/upload.action.ts | 144 +- src/hooks/use-mobile.ts | 19 + src/hooks/use-toast.ts | 194 - src/lib/auth/auth-client.ts | 30 + src/lib/auth/auth.ts | 299 + src/lib/auth/current-user.ts | 15 + src/lib/auth/permissions.ts | 59 + src/lib/hooks/__model_meta.ts | 766 - src/lib/hooks/account.ts | 334 - src/lib/hooks/agent.ts | 334 - src/lib/hooks/backup.ts | 335 - src/lib/hooks/database.ts | 335 - src/lib/hooks/index.ts | 22 - src/lib/hooks/organization.ts | 334 - src/lib/hooks/project.ts | 334 - src/lib/hooks/restoration.ts | 335 - src/lib/hooks/session.ts | 334 - src/lib/hooks/settings.ts | 335 - src/lib/hooks/user-organization.ts | 335 - src/lib/hooks/user.ts | 335 - src/lib/hooks/verification-token.ts | 334 - src/openapi.yaml | 27938 ---------------- src/prisma.ts | 14 - src/safe-actions.ts | 27 +- src/types/action-type.ts | 34 +- src/types/events.ts | 6 +- src/types/next.ts | 4 +- src/utils/email-helper.ts | 105 +- src/utils/get-server-url.ts | 12 +- src/utils/init.ts | 88 +- src/utils/mock-data.ts | 197 +- src/utils/password.ts | 14 - src/utils/s3-file-management.ts | 148 +- src/utils/verify-uuid.ts | 3 +- tailwind.config.ts | 84 - tsconfig.json | 62 +- yarn.lock | 7333 ++-- 285 files changed, 15492 insertions(+), 46090 deletions(-) create mode 100644 .env.exemple create mode 100644 .vscode/settings.json create mode 100644 app/api/auth/[...all]/route.ts delete mode 100644 app/api/auth/[...nextauth]/route.ts create mode 100644 drizzle.config.ts rename next.config.mjs => next.config.ts (50%) delete mode 100755 prisma/migrations/20241103104432_2024_11_03/migration.sql delete mode 100755 prisma/migrations/20241103192308_2024_11_03/migration.sql delete mode 100644 prisma/migrations/20241110103733_2024_11_10/migration.sql delete mode 100755 prisma/migrations/20241110140612_2024_11_10/migration.sql delete mode 100644 prisma/migrations/20241111152326_2024_11_11/migration.sql delete mode 100644 prisma/migrations/20241113175420_2024_11_13/migration.sql delete mode 100644 prisma/migrations/20241113182543_2024_11_13/migration.sql delete mode 100644 prisma/migrations/20241116144000_2024_11_16/migration.sql delete mode 100644 prisma/migrations/20241116144522_2024_11_16/migration.sql delete mode 100644 prisma/migrations/20241116201945_2024_11_16/migration.sql delete mode 100644 prisma/migrations/20241116203552_2024_11_16/migration.sql delete mode 100644 prisma/migrations/20241117143016_2024_11_17/migration.sql delete mode 100644 prisma/migrations/20241117174438_2024_11_17/migration.sql delete mode 100644 prisma/migrations/20241117184204_2024_11_17/migration.sql delete mode 100644 prisma/migrations/20241117190445_2024_11_17/migration.sql delete mode 100644 prisma/migrations/20241123102722_2024_11_23/migration.sql delete mode 100644 prisma/migrations/20241127175717_2024_11_27/migration.sql delete mode 100644 prisma/migrations/20241127182048_2024_11_27/migration.sql delete mode 100644 prisma/migrations/20241129115417_2024_11_29/migration.sql delete mode 100644 prisma/migrations/20241129165221_2024_11_29/migration.sql delete mode 100644 prisma/migrations/20241129173131_2024_11_29/migration.sql delete mode 100644 prisma/migrations/20241130153218_2024_11_30/migration.sql delete mode 100644 prisma/migrations/20241212103214_2024_12_12/migration.sql delete mode 100644 prisma/migrations/20241212103831_2024_12_12/migration.sql delete mode 100644 prisma/migrations/20241212110141_2024_12_12/migration.sql delete mode 100644 prisma/migrations/20241214165019_2024_12_14/migration.sql delete mode 100644 prisma/migrations/20241217174747_2024_12_17/migration.sql delete mode 100644 prisma/migrations/20241217183424_2024_12_17/migration.sql delete mode 100644 prisma/migrations/20241217183746_2024_12_17/migration.sql delete mode 100644 prisma/migrations/20241217192704_2024_12_17/migration.sql delete mode 100644 prisma/migrations/20241229180505_2024_12_29/migration.sql delete mode 100644 prisma/migrations/20241231084839_2024_12_31/migration.sql delete mode 100644 prisma/migrations/migration_lock.toml delete mode 100644 prisma/schema.prisma delete mode 100644 public/google-icon.png delete mode 100644 schema.zmodel delete mode 100644 src/auth/auth.ts delete mode 100644 src/auth/current-user.ts create mode 100644 src/components/wrappers/auth/login/buttonAuth/SocialAuthButton.tsx delete mode 100644 src/components/wrappers/auth/login/loginForm/login-form.action.ts delete mode 100644 src/components/wrappers/auth/login/socialAuth/socialAuthButtons/SocialAuthButton.tsx rename src/components/wrappers/{codeSnippet => code-snippet}/CodeSnippet.tsx (63%) create mode 100644 src/components/wrappers/common/day-time-picker.tsx delete mode 100644 src/components/wrappers/common/daytime-picker.tsx create mode 100644 src/components/wrappers/common/file-uploader.tsx rename src/components/wrappers/common/{multiSelect/MultiSelect.tsx => multiselect/multi-select.tsx} (68%) delete mode 100644 src/components/wrappers/common/table/data-table-with-pagination.tsx create mode 100644 src/components/wrappers/common/table/table-sort-button.tsx delete mode 100644 src/components/wrappers/dashboard/sideBar/SideBarMenu/SideBarMenuAdmin.tsx create mode 100644 src/components/wrappers/dashboard/sideBar/sidebar-content.tsx delete mode 100644 src/db.ts create mode 100644 src/db/index.ts create mode 100644 src/db/migrations/0000_round_king_cobra.sql create mode 100644 src/db/migrations/0001_small_anthem.sql create mode 100644 src/db/migrations/meta/0000_snapshot.json create mode 100644 src/db/migrations/meta/0001_snapshot.json create mode 100644 src/db/migrations/meta/_journal.json create mode 100644 src/db/schema.ts create mode 100644 src/db/schema/00_setting.ts create mode 100644 src/db/schema/01_user.ts create mode 100644 src/db/schema/02_organization.ts create mode 100644 src/db/schema/03_project.ts create mode 100644 src/db/schema/04_agent.ts create mode 100644 src/db/schema/05_database.ts create mode 100644 src/db/schema/types.ts delete mode 100644 src/features/auth/auth.action.ts delete mode 100644 src/features/permissions.ts delete mode 100644 src/features/permissions/permissions.ts create mode 100644 src/hooks/use-mobile.ts delete mode 100644 src/hooks/use-toast.ts create mode 100644 src/lib/auth/auth-client.ts create mode 100644 src/lib/auth/auth.ts create mode 100644 src/lib/auth/current-user.ts create mode 100644 src/lib/auth/permissions.ts delete mode 100644 src/lib/hooks/__model_meta.ts delete mode 100644 src/lib/hooks/account.ts delete mode 100644 src/lib/hooks/agent.ts delete mode 100644 src/lib/hooks/backup.ts delete mode 100644 src/lib/hooks/database.ts delete mode 100644 src/lib/hooks/index.ts delete mode 100644 src/lib/hooks/organization.ts delete mode 100644 src/lib/hooks/project.ts delete mode 100644 src/lib/hooks/restoration.ts delete mode 100644 src/lib/hooks/session.ts delete mode 100644 src/lib/hooks/settings.ts delete mode 100644 src/lib/hooks/user-organization.ts delete mode 100644 src/lib/hooks/user.ts delete mode 100644 src/lib/hooks/verification-token.ts delete mode 100644 src/openapi.yaml delete mode 100644 src/prisma.ts delete mode 100644 src/utils/password.ts delete mode 100644 tailwind.config.ts diff --git a/.env.exemple b/.env.exemple new file mode 100644 index 00000000..c902275f --- /dev/null +++ b/.env.exemple @@ -0,0 +1,33 @@ +# Environnement +NODE_ENV=production + +# Base de donnรฉes +DATABASE_URL=postgresql://devuser:changeme@db:5432/devdb?schema=public + +# Projet +NEXT_PUBLIC_PROJECT_NAME="Portabase" +NEXT_PUBLIC_PROJECT_DESCRIPTION="Portabase is a powerful database manager" +NEXT_PUBLIC_PROJECT_URL=http://app.portabase.io +PROJECT_SECRET= + +# SMTP (email) +SMTP_HOST= +SMTP_PORT=587 +SMTP_USER= +SMTP_PASSWORD= +SMTP_FROM= + +# Google +AUTH_GOOGLE_ID= +AUTH_GOOGLE_SECRET= + +# S3 +S3_ENDPOINT=http://app.s3.portabase.io +S3_ACCESS_KEY= +S3_SECRET_KEY= +S3_BUCKET_NAME=portabase +S3_PORT=9000 +S3_USE_SSL=true + +# Storage Type (s3, local) +STORAGE_TYPE=local \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 00000000..b68b5659 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1,3 @@ +{ + "WillLuke.nextjs.hasPrompted": true +} diff --git a/Makefile b/Makefile index 429121c3..40d48ee9 100644 --- a/Makefile +++ b/Makefile @@ -1,2 +1,2 @@ migrate: - docker compose run --rm app sh -c "npx prisma migrate dev" \ No newline at end of file + docker compose run --rm app \ No newline at end of file diff --git a/README.md b/README.md index 62c41f31..03472309 100644 --- a/README.md +++ b/README.md @@ -1,21 +1,15 @@ - +
+
+ + Logo + -[//]: # (
) - -[//]: # (
) - -[//]: # ( ) - -[//]: # ( Logo) - -[//]: # ( ) - -

Portabase

+

Portabase

- Backup / Restore your databases instances + Easily backup and restore your database instances with Portabase
- Explore the docs ยป + Explore the Docs ยป

View Demo @@ -24,159 +18,206 @@ ยท Request Feature

+
+## ๐Ÿ“š Table of Contents +- [About The Project](#about-the-project) + - [Built With](#built-with) +- [Getting Started](#getting-started) + - [Prerequisites](#prerequisites) + - [Installation](#installation) +- [Usage](#usage) +- [Roadmap](#roadmap) +- [Contributing](#contributing) +- [License](#license) +- [Contact](#contact) +- [Acknowledgments](#acknowledgments) - -
- Table of Contents -
    -
  1. - About The Project - -
  2. -
  3. - Getting Started - -
  4. -
  5. Usage
  6. -
  7. Roadmap
  8. -
  9. Contributing
  10. -
  11. License
  12. -
  13. Contact
  14. -
  15. Acknowledgments
  16. -
-
+--- - - ## About The Project -Portabase is the server dashboard service working with portabase agents. -see Portabase +**Portabase** is a server dashboard tool designed to simplify the backup and restoration of your database instances. It integrates seamlessly with Portabase agents for managing operations securely and efficiently. -

(back to top)

+GitHub Repository: [Portabase](https://github.com/Soluce-Technologies/portabase) +### ๐Ÿ”ง Built With +- [![NextJS][NextJS]][NextJS-url] (v15 with App Router) +- [![Drizzle][Drizzle]][Drizzle-url] +- [![ShadcnUI][ShadcnUI]][ShadcnUI-url] +- BetterAuth +- React Email -### Built With +--- -* [![NextJS][NextJS]][NextJS-url] -* [![Prisma][Prisma]][Prisma-url] - - -

(back to top)

- - - - -## Getting Started +## ๐Ÿš€ Getting Started ### Prerequisites -Install docker on your machine +Ensure Docker is installed on your machine before getting started. ### Installation -Installation steps in order to use it locally and work on some features improvements +To run Portabase locally: -1. Clone the repo - ```sh - git clone https://github.com/Soluce-Technologies/portabase - ``` -3. Start the project - ```sh - docker compose up - ``` +1. Clone the repository: + ```bash + git clone https://github.com/Soluce-Technologies/portabase + cd portabase + ``` +2. Start the development environment: + ```bash + docker compose up + ``` -

(back to top)

+--- +## ๐Ÿ› ๏ธ Usage - -## Roadmap +Portabase provides a dashboard to manage database instances and backups. It supports: -- [ ] Add Changelog -- [ ] Improve documentation and repositories -- [ ] Add tests procedure -- [ ] Add Release File -- [ ] Security improvements -- [ ] Improve UX/UI -- [ ] Improve Workspace system -- [ ] Migrate to Drizzle ORM -- [ ] Multi locale support -- [ ] Multi db support +- PostgreSQL (current) +- MongoDB (coming soon) +- MySQL (coming soon) + +You can access the dashboard at `http://localhost:3000` after starting the project. + +--- + +## ๐Ÿ—บ๏ธ Roadmap + +- [ ] Add changelog +- [ ] Enhance documentation +- [ ] Implement testing procedures +- [ ] Include release file +- [ ] Improve security +- [ ] Enhance UX/UI +- [ ] Improve workspace management system +- [x] Migrate to Drizzle ORM +- [ ] Add multi-language support +- [ ] Extend multi-database support: - [x] PostgreSQL - [ ] MongoDB - [ ] MySQL -See the [open issues](https://github.com/Soluce-Technologies/portabase/issues) for a full list of proposed features (and known issues). +Check out [open issues](https://github.com/Soluce-Technologies/portabase/issues) for more. -

(back to top)

+--- +## ๐Ÿค Contributing +Contributions are welcome and appreciated! Here's how to get started: - -## Contributing +1. Fork the repository +2. Create a new branch: + ```bash + git checkout -b feature/YourFeature + ``` +3. Commit your changes: + ```bash + git commit -m "Add YourFeature" + ``` +4. Push to the branch: + ```bash + git push origin feature/YourFeature + ``` +5. Open a pull request -Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**. +Give the project a โญ if you like it! -If you have a suggestion that would make this better, please fork the repo and create a pull request. You can also simply open an issue with the tag "enhancement". -Don't forget to give the project a star! Thanks again! +### Top Contributors -1. Fork the Project -2. Create your Feature Branch (`git checkout -b feature/AmazingFeature`) -3. Commit your Changes (`git commit -m 'Add some AmazingFeature'`) -4. Push to the Branch (`git push origin feature/AmazingFeature`) -5. Open a Pull Request +[![Contributors](https://contrib.rocks/image?repo=Soluce-Technologies/portabase)](https://github.com/Soluce-Technologies/portabase/graphs/contributors) -### Top contributors: +--- - - contrib.rocks image - +## Developer Notes -

(back to top)

+### Environment Variables +```yml +# Environment +NODE_ENV=production -## Memo +# Database +DATABASE_URL=postgresql://devuser:changeme@db:5432/devdb?schema=public -If you're using Semantic Versioning (SemVer) to version your Docker images, you can use the following format: +# Project Info +NEXT_PUBLIC_PROJECT_NAME="Portabase" +NEXT_PUBLIC_PROJECT_DESCRIPTION="Portabase is a powerful database manager" +NEXT_PUBLIC_PROJECT_URL=http://app.portabase.io +PROJECT_SECRET= -```major.minor.patch-rc.release (e.g. 1.0.0-rc.1)``` +# SMTP (Email) +SMTP_HOST= +SMTP_PORT=587 +SMTP_USER= +SMTP_PASSWORD= +SMTP_FROM= -```major.minor.patch-rc.release-tag (e.g. 1.0.0-rc.1-dev)``` +# Google OAuth +AUTH_GOOGLE_ID= +AUTH_GOOGLE_SECRET= -For zenstask +# S3/MinIO Configuration +S3_ENDPOINT=http://app.s3.portabase.io +S3_ACCESS_KEY= +S3_SECRET_KEY= +S3_BUCKET_NAME=portabase +S3_PORT=9000 +S3_USE_SSL=true -```zenstack generate``` +# Storage Backend: 'local' or 's3' +STORAGE_TYPE=local +``` - -## License +### Semantic Versioning -Distributed under the Apache License. See `LICENSE.txt` for more information. +Use the following format for Docker image versioning: -

(back to top)

+```bash +major.minor.patch-rc.release +# Example: 1.0.0-rc.1 +major.minor.patch-rc.release-tag +# Example: 1.0.0-rc.1-dev +``` - -## Contact +### Zenstack Support -Killian Larcher - killian.larcher@soluce-technologies.com -Charles Gauthereau - charles.gauthereau@soluce-technologies.com +To regenerate schema: -Project Link: [https://github.com/Soluce-Technologies/portabase](https://github.com/Soluce-Technologies/agent-portabase) +```bash +zenstack generate +``` -

(back to top)

+--- - - +## ๐Ÿ“„ License + +Distributed under the Apache License. See `LICENSE.txt` for more details. + +--- + +## ๐Ÿ“ฌ Contact + +- Killian Larcher - killian.larcher@soluce-technologies.com +- Charles Gauthereau - charles.gauthereau@soluce-technologies.com +- Project Link: [Portabase GitHub](https://github.com/Soluce-Technologies/portabase) + +--- + +## ๐Ÿ™ Acknowledgments + +Thanks to all contributors and the open-source community! + +--- [NextJS]: https://img.shields.io/badge/next.js-000000?style=for-the-badge&logo=nextdotjs&logoColor=white -[Prisma]: https://img.shields.io/badge/Prisma-3982CE?style=for-the-badge&logo=Prisma&logoColor=white -[Prisma-url]: https://www.prisma.io/ -[NextJS-url]: https://nextjs.org/ \ No newline at end of file +[Drizzle]: https://img.shields.io/badge/Drizzle-111?style=for-the-badge&logo=Drizzle&logoColor=c5f74f +[ShadcnUI]: https://img.shields.io/badge/shadcn/ui-000000?style=for-the-badge&logo=shadcn/ui&logoColor=white +[NextJS-url]: https://nextjs.org/ +[Drizzle-url]: https://orm.drizzle.team/ +[ShadcnUI-url]: https://ui.shadcn.com/ diff --git a/app/(customer)/dashboard/(admin)/admin/page.tsx b/app/(customer)/dashboard/(admin)/admin/page.tsx index 30da0e7a..767f32d0 100644 --- a/app/(customer)/dashboard/(admin)/admin/page.tsx +++ b/app/(customer)/dashboard/(admin)/admin/page.tsx @@ -1,42 +1,29 @@ -import {PageParams} from "@/types/next"; - -import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page"; -import {requiredCurrentUser} from "@/auth/current-user"; -import {prisma} from "@/prisma"; -import {AdminTabs} from "@/components/wrappers/dashboard/admin/admin-tabs"; +import { PageParams } from "@/types/next"; +import { Page, PageContent, PageHeader, PageTitle } from "@/features/layout/page"; +import { AdminTabs } from "@/components/wrappers/dashboard/admin/admin-tabs"; +import { currentUser } from "@/lib/auth/current-user"; +import { db } from "@/db"; export default async function RoutePage(props: PageParams<{}>) { + const user = await currentUser()!; + const users = await db.query.user.findMany({ + where: (fields, { isNull, not }) => not(isNull(fields.deletedAt)), + }); - const user = await requiredCurrentUser() - - const users = await prisma.user.findMany({ - where: { - // id: { - // not: user.id - // }, - deleted: {not: true}, - - } - }) - - const settings = await prisma.settings.findUnique({ - where: { - name: "system" - } - }) + const settings = await db.query.setting.findFirst({ + where: (fields, { eq }) => eq(fields.name, "system"), + }); return ( - - Administration Panel - + Administration Panel - + - ) -} \ No newline at end of file + ); +} diff --git a/app/(customer)/dashboard/(admin)/agents/[agentId]/edit/page.tsx b/app/(customer)/dashboard/(admin)/agents/[agentId]/edit/page.tsx index bfa50f9c..58a272ab 100644 --- a/app/(customer)/dashboard/(admin)/agents/[agentId]/edit/page.tsx +++ b/app/(customer)/dashboard/(admin)/agents/[agentId]/edit/page.tsx @@ -1,19 +1,18 @@ -import {PageParams} from "@/types/next"; -import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page"; -import {AgentForm} from "@/components/wrappers/dashboard/agent/AgentForm/AgentForm"; -import {prisma} from "@/prisma"; -import {notFound} from "next/navigation"; +import { PageParams } from "@/types/next"; +import { Page, PageContent, PageHeader, PageTitle } from "@/features/layout/page"; +import { AgentForm } from "@/components/wrappers/dashboard/agent/AgentForm/AgentForm"; +import { notFound } from "next/navigation"; +import { db } from "@/db"; +export default async function RoutePage( + props: PageParams<{ + agentId: string; + }> +) { + const { agentId } = await props.params; -export default async function RoutePage(props: PageParams<{ - agentId: string; -}>) { - - const {agentId} = await props.params - const agent = await prisma.agent.findUnique({ - where: { - id: agentId, - } + const agent = await db.query.agent.findFirst({ + where: (fields, { eq }) => eq(fields.id, agentId), }); if (!agent) { @@ -23,13 +22,11 @@ export default async function RoutePage(props: PageParams<{ return ( - - Edit {agent.name} - + Edit {agent.name} - + - ) -} \ No newline at end of file + ); +} diff --git a/app/(customer)/dashboard/(admin)/agents/new/page.tsx b/app/(customer)/dashboard/(admin)/agents/new/page.tsx index 3957cf1d..bf72882b 100644 --- a/app/(customer)/dashboard/(admin)/agents/new/page.tsx +++ b/app/(customer)/dashboard/(admin)/agents/new/page.tsx @@ -1,28 +1,16 @@ -import {PageParams} from "@/types/next"; -import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page"; -import {AgentForm} from "@/components/wrappers/dashboard/agent/AgentForm/AgentForm"; -import {currentUser} from "@/auth/current-user"; -import {notFound} from "next/navigation"; - +import { PageParams } from "@/types/next"; +import { Page, PageContent, PageHeader, PageTitle } from "@/features/layout/page"; +import { AgentForm } from "@/components/wrappers/dashboard/agent/AgentForm/AgentForm"; export default async function RoutePage(props: PageParams<{}>) { - - const user = await currentUser(); - - if(user.role != "admin"){ - notFound() - } - return ( - - Create new agent - + Create new agent - + - ) -} \ No newline at end of file + ); +} diff --git a/app/(customer)/dashboard/(admin)/agents/page.tsx b/app/(customer)/dashboard/(admin)/agents/page.tsx index f2ca4bba..0464ba00 100644 --- a/app/(customer)/dashboard/(admin)/agents/page.tsx +++ b/app/(customer)/dashboard/(admin)/agents/page.tsx @@ -1,29 +1,24 @@ -import {PageParams} from "@/types/next"; -import {AgentCard} from "@/components/wrappers/dashboard/agent/AgentCard/AgentCard"; -import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination"; -import {Button} from "@/components/ui/button"; -import Link from 'next/link' -import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page"; -import {prisma} from "@/prisma"; -import {notFound} from "next/navigation"; +import { PageParams } from "@/types/next"; +import { AgentCard } from "@/components/wrappers/dashboard/agent/AgentCard/AgentCard"; +import { CardsWithPagination } from "@/components/wrappers/common/cards-with-pagination"; +import { Button } from "@/components/ui/button"; +import Link from "next/link"; +import { Page, PageActions, PageContent, PageHeader, PageTitle } from "@/features/layout/page"; +import { notFound } from "next/navigation"; +import { db } from "@/db"; export const dynamic = "force-dynamic"; - export default async function RoutePage(props: PageParams<{}>) { + const agents = await db.query.agent.findMany(); - const agents = await prisma.agent.findMany() - - if(!agents){ - notFound() + if (!agents) { + notFound(); } - return ( - - Agents - + Agents {agents.length > 0 && ( @@ -33,21 +28,17 @@ export default async function RoutePage(props: PageParams<{}>) { )} - {agents.length > 0 ? - - : + {agents.length > 0 ? ( + + ) : ( + className=" flex item-center justify-center border-2 border-dashed transition-colors border-primary p-8 lg:p-12 w-full rounded-md" + > Create new Agent - } + )} - ) -} \ No newline at end of file + ); +} diff --git a/app/(customer)/dashboard/(admin)/layout.tsx b/app/(customer)/dashboard/(admin)/layout.tsx index 3fdd7089..59cc2d3c 100644 --- a/app/(customer)/dashboard/(admin)/layout.tsx +++ b/app/(customer)/dashboard/(admin)/layout.tsx @@ -1,17 +1,13 @@ import React from "react"; -import {currentUser, requiredCurrentUser} from "@/auth/current-user"; -import {notFound} from "next/navigation"; +import { notFound } from "next/navigation"; +import { currentUser } from "@/lib/auth/current-user"; -export default async function Layout({children}: { children: React.ReactNode }) { +export default async function Layout({ children }: { children: React.ReactNode }) { + const user = await currentUser(); - const user = await requiredCurrentUser() - - if(user.role != "admin"){ - notFound() + if (user!.role !== "superadmin" && user!.role !== "admin") { + notFound(); } - return ( - <> - {children} - - ) -} \ No newline at end of file + + return <>{children}; +} diff --git a/app/(customer)/dashboard/[slug]/databases/[databaseId]/restore/page.tsx b/app/(customer)/dashboard/[slug]/databases/[databaseId]/restore/page.tsx index 07ade128..15a0a1ea 100644 --- a/app/(customer)/dashboard/[slug]/databases/[databaseId]/restore/page.tsx +++ b/app/(customer)/dashboard/[slug]/databases/[databaseId]/restore/page.tsx @@ -1,55 +1,41 @@ -import {PageParams} from "@/types/next"; -import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page"; -import {requiredCurrentUser} from "@/auth/current-user"; -import {prisma} from "@/prisma"; -import {notFound} from "next/navigation"; -import {RestoreForm} from "@/components/wrappers/dashboard/database/RestoreForm"; +import { PageParams } from "@/types/next"; +import { Page, PageContent, PageHeader, PageTitle } from "@/features/layout/page"; +import { notFound } from "next/navigation"; +import { RestoreForm } from "@/components/wrappers/dashboard/database/RestoreForm"; +import { currentUser } from "@/lib/auth/current-user"; +import { db } from "@/db"; +import { eq } from "drizzle-orm"; +import { database, backup } from "@/db/schema"; -export default async function RoutePage(props: PageParams<{ - databaseId: string; -}>) { +export default async function RoutePage( + props: PageParams<{ + databaseId: string; + }> +) { + const { databaseId } = await props.params; - const {databaseId} = await props.params + const user = await currentUser(); + if (!user) notFound(); + const [dbToRestore] = await db.select().from(database).where(eq(database.id, databaseId)); - const user = await requiredCurrentUser() - - const database = await prisma.database.findUnique({ - where: { - id: databaseId, - } - }); - - - const databases = await prisma.database.findMany({ - where: { - dbms: database.dbms, - } - }) - - const backups = await prisma.backup.findMany({ - where: { - status: "success", - } - }); - - - if (!database) { + if (!dbToRestore) { notFound(); } + const dbsOfSameType = await db.select().from(database).where(eq(database.dbms!, dbToRestore.dbms!)); + + const successfulBackups = await db.select().from(backup).where(eq(backup.status, "success")); return ( - - Restore {database.name} - + Restore {dbToRestore.name} - + - ) -} \ No newline at end of file + ); +} diff --git a/app/(customer)/dashboard/[slug]/home/page.tsx b/app/(customer)/dashboard/[slug]/home/page.tsx index 30a4fee3..062cae16 100644 --- a/app/(customer)/dashboard/[slug]/home/page.tsx +++ b/app/(customer)/dashboard/[slug]/home/page.tsx @@ -1,17 +1,20 @@ -import {PageParams} from "@/types/next"; -import {redirect} from "next/navigation"; +"use client"; +import { authClient } from "@/lib/auth/auth-client"; +import { PageParams } from "@/types/next"; -export default async function RoutePage(props: PageParams<{}>) { +export default function RoutePage(props: PageParams<{}>) { + const { data: activeOrganization } = authClient.useActiveOrganization(); - - // return ( - //
- //
- //
- //
- //
- //
- //
- //
- // ) -} \ No newline at end of file + return ( +
+
+
+
+
+
+
+ {JSON.stringify(activeOrganization)} + {activeOrganization?.name} +
+ ); +} diff --git a/app/(customer)/dashboard/[slug]/projects/[projectId]/database/[databaseId]/edit/page.tsx b/app/(customer)/dashboard/[slug]/projects/[projectId]/database/[databaseId]/edit/page.tsx index 6b76b4b1..00f6d1f0 100644 --- a/app/(customer)/dashboard/[slug]/projects/[projectId]/database/[databaseId]/edit/page.tsx +++ b/app/(customer)/dashboard/[slug]/projects/[projectId]/database/[databaseId]/edit/page.tsx @@ -1,39 +1,34 @@ -import {PageParams} from "@/types/next"; -import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page"; -import {requiredCurrentUser} from "@/auth/current-user"; -import {prisma} from "@/prisma"; -import {notFound} from "next/navigation"; -import {DatabaseForm} from "@/components/wrappers/dashboard/database/DatabaseForm/DatabaseForm"; +import { PageParams } from "@/types/next"; +import { Page, PageContent, PageHeader, PageTitle } from "@/features/layout/page"; +import { notFound } from "next/navigation"; +import { DatabaseForm } from "@/components/wrappers/dashboard/database/DatabaseForm/DatabaseForm"; +import { db } from "@/db"; +import { database } from "@/db/schema"; +import { eq } from "drizzle-orm"; -export default async function RoutePage(props: PageParams<{ - databaseId: string; -}>) { +export default async function RoutePage(props: PageParams<{ databaseId: string }>) { + const { databaseId } = await props.params; - const {databaseId} = await props.params - - const user = await requiredCurrentUser() - const database = await prisma.database.findUnique({ - where: { - id: databaseId, - } + const dbItem = await db.query.database.findFirst({ + where: eq(database.id, databaseId), }); - if (!database) { + if (!dbItem) { notFound(); } - return ( - - Edit {database.name} - + Edit {dbItem.name} - + - ) -} \ No newline at end of file + ); +} diff --git a/app/(customer)/dashboard/[slug]/projects/[projectId]/database/[databaseId]/page.tsx b/app/(customer)/dashboard/[slug]/projects/[projectId]/database/[databaseId]/page.tsx index dc0152f0..95806949 100644 --- a/app/(customer)/dashboard/[slug]/projects/[projectId]/database/[databaseId]/page.tsx +++ b/app/(customer)/dashboard/[slug]/projects/[projectId]/database/[databaseId]/page.tsx @@ -1,91 +1,75 @@ -import {PageParams} from "@/types/next"; -import {prisma} from "@/prisma"; -import {notFound} from "next/navigation"; -import {Page, PageActions, PageContent, PageDescription, PageTitle} from "@/features/layout/page"; -import {BackupButton} from "@/components/wrappers/dashboard/backup/backup-button/backup-button"; -import {DatabaseTabs} from "@/components/wrappers/dashboard/projects/Database/DatabaseTabs"; -import {DatabaseKpi} from "@/components/wrappers/dashboard/projects/Database/DatabaseKpi"; -import {EditButton} from "@/components/wrappers/dashboard/database/EditButton/EditButton"; -import {CronButton} from "@/components/wrappers/dashboard/database/CronButton/CronButton"; +import { PageParams } from "@/types/next"; +import { notFound } from "next/navigation"; +import { Page, PageActions, PageContent, PageDescription, PageTitle } from "@/features/layout/page"; +import { BackupButton } from "@/components/wrappers/dashboard/backup/backup-button/backup-button"; +import { DatabaseTabs } from "@/components/wrappers/dashboard/projects/Database/DatabaseTabs"; +import { DatabaseKpi } from "@/components/wrappers/dashboard/projects/Database/DatabaseKpi"; +import { EditButton } from "@/components/wrappers/dashboard/database/EditButton/EditButton"; +import { CronButton } from "@/components/wrappers/dashboard/database/CronButton/CronButton"; +import { db } from "@/db"; +import { eq, and } from "drizzle-orm"; +import { backup as drizzleBackup, database as drizzleDatabase, restoration as drizzleRestoration } from "@/db/schema"; export default async function RoutePage(props: PageParams<{ databaseId: string }>) { + const { databaseId } = await props.params; - const {databaseId} = await props.params - const database = await prisma.database.findUnique({ - where: { - id: databaseId, - }, - }) - if (!database) { - notFound() + const dbItem = await db.query.database.findFirst({ + where: eq(drizzleDatabase.id, databaseId), + }); + + if (!dbItem) { + notFound(); } - - const backups = await prisma.backup.findMany({ - where: { - databaseId: database.id - }, - include:{ - restorations: {} - }, - orderBy: { - createdAt: "desc" - } - }) - - const restorations = await prisma.restoration.findMany({ - where: { - databaseId: databaseId, - }, - orderBy: { - createdAt: "desc" - } - }) - - - const isAlreadyBackup = !!backups.find(backup => backup.status === "waiting"); - const isAlreadyRestore = !!restorations.find(restoration => restoration.status === "waiting"); - - const totalBackups = await prisma.backup.count({ - where: { - databaseId: databaseId, + const backups = await db.query.backup.findMany({ + where: eq(drizzleBackup.databaseId, dbItem.id), + with: { + restorations: true, }, + orderBy: (b, { desc }) => [desc(b.createdAt)], }); - const successfulBackups = await prisma.backup.count({ - where: { - databaseId: databaseId, - status: 'success', - }, + + const restorations = await db.query.restoration.findMany({ + where: eq(drizzleRestoration.databaseId, dbItem.id), + orderBy: (r, { desc }) => [desc(r.createdAt)], }); - const successRate = totalBackups > 0 ? (successfulBackups / totalBackups) * 100 : null + + const isAlreadyBackup = backups.some((b) => b.status === "waiting"); + const isAlreadyRestore = restorations.some((r) => r.status === "waiting"); + + const [totalBackups, successfulBackups] = await Promise.all([ + db + .select({ count: drizzleBackup.id }) + .from(drizzleBackup) + .where(eq(drizzleBackup.databaseId, dbItem.id)) + .then((rows) => rows.length), + db + .select({ count: drizzleBackup.id }) + .from(drizzleBackup) + .where(and(eq(drizzleBackup.databaseId, dbItem.id), eq(drizzleBackup.status, "success"))) + .then((rows) => rows.length), + ]); + + const successRate = totalBackups > 0 ? (successfulBackups / totalBackups) * 100 : null; return (
- {database.name} - - + {dbItem.name} + + - +
- {database.description} + {dbItem.description} - - + +
- ) -} \ No newline at end of file + ); +} diff --git a/app/(customer)/dashboard/[slug]/projects/[projectId]/edit/page.tsx b/app/(customer)/dashboard/[slug]/projects/[projectId]/edit/page.tsx index e18aaf6b..4313ed8e 100644 --- a/app/(customer)/dashboard/[slug]/projects/[projectId]/edit/page.tsx +++ b/app/(customer)/dashboard/[slug]/projects/[projectId]/edit/page.tsx @@ -1,61 +1,61 @@ -import {notFound} from "next/navigation"; +import { notFound } from "next/navigation"; +import { PageParams } from "@/types/next"; +import { Page, PageContent, PageHeader, PageTitle } from "@/features/layout/page"; +import { ProjectForm } from "@/components/wrappers/dashboard/projects/ProjectsForm/ProjectForm"; -import {PageParams} from "@/types/next"; -import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page"; -import {prisma} from "@/prisma"; -import {ProjectForm} from "@/components/wrappers/dashboard/projects/ProjectsForm/ProjectForm"; +import { db } from "@/db"; +import { eq } from "drizzle-orm"; +import { project as drizzleProject, organization as drizzleOrganization, DatabaseWith } from "@/db/schema"; +export default async function RoutePage(props: PageParams<{ projectId: string }>) { + const { projectId } = await props.params; -export default async function RoutePage(props: PageParams<{ - projectId: string; -}>) { - - const {projectId} = await props.params - - const project = await prisma.project.findUnique({ - where: { - id: projectId, + const proj = await db.query.project.findFirst({ + where: eq(drizzleProject.id, projectId), + with: { + databases: true, }, - include: { - databases: {} - } }); - if (!project) { + if (!proj) { notFound(); } - const organization = await prisma.organization.findFirst({ - where: { - slug: 'default', - } - }) - const availableDatabases = await prisma.database.findMany({ - where: { - OR: [ - {projectId: null}, - {projectId: project.id}, - ], - }, - include: { - agent: {} - }, - orderBy: { - createdAt: 'desc', - }, - }) + // + const org = await db.query.organization.findFirst({ + where: eq(drizzleOrganization.slug, "default"), + }); + + if (!org) { + notFound(); + } + + const availableDatabases = ( + await db.query.database.findMany({ + where: (db, { or, eq, isNull }) => or(isNull(db.projectId), eq(db.projectId, proj.id)), + with: { + agent: true, + project: true, + backups: true, + restorations: true, + }, + orderBy: (db, { desc }) => [desc(db.createdAt)], + }) + ).filter((db): db is DatabaseWith => db.project !== null); return ( - - Edit {project.name} - + Edit {proj.name} - + db.id) }} + projectId={proj.id} + /> - ) -} \ No newline at end of file + ); +} diff --git a/app/(customer)/dashboard/[slug]/projects/[projectId]/page.tsx b/app/(customer)/dashboard/[slug]/projects/[projectId]/page.tsx index 49a02a2f..e257dfef 100644 --- a/app/(customer)/dashboard/[slug]/projects/[projectId]/page.tsx +++ b/app/(customer)/dashboard/[slug]/projects/[projectId]/page.tsx @@ -1,77 +1,70 @@ -import {PageParams} from "@/types/next"; -import {Page, PageActions, PageContent, PageDescription, PageTitle} from "@/features/layout/page"; -import {buttonVariants} from "@/components/ui/button"; -import {prisma} from "@/prisma"; -import {GearIcon} from "@radix-ui/react-icons"; +import { PageParams } from "@/types/next"; +import { Page, PageActions, PageContent, PageDescription, PageTitle } from "@/features/layout/page"; +import { buttonVariants } from "@/components/ui/button"; +import { GearIcon } from "@radix-ui/react-icons"; import Link from "next/link"; -import {ButtonDeleteProject} from "@/components/wrappers/dashboard/projects/ButtonDeleteProject/ButtonDeleteProject"; -import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination"; -import {ProjectDatabaseCard} from "@/components/wrappers/dashboard/projects/ProjectCard/ProjectDatabaseCard"; -import {notFound} from "next/navigation"; -import {getCurrentOrganizationSlug} from "@/features/dashboard/organization-cookie"; +import { ButtonDeleteProject } from "@/components/wrappers/dashboard/projects/ButtonDeleteProject/ButtonDeleteProject"; +import { CardsWithPagination } from "@/components/wrappers/common/cards-with-pagination"; +import { ProjectDatabaseCard } from "@/components/wrappers/dashboard/projects/ProjectCard/ProjectDatabaseCard"; +import { notFound } from "next/navigation"; +import { getCurrentOrganizationSlug } from "@/features/dashboard/organization-cookie"; +import { db } from "@/db"; +import { eq } from "drizzle-orm"; +import { organization as drizzleOrganization } from "@/db/schema"; -export default async function RoutePage(props: PageParams<{ slug:string, projectId: string }>) { +export default async function RoutePage(props: PageParams<{ slug: string; projectId: string }>) { + const { slug: organizationSlug, projectId } = await props.params; - const {slug: organizationSlug, projectId} = await props.params + const currentOrganizationSlug = await getCurrentOrganizationSlug(); - - const currentOrganizationSlug = await getCurrentOrganizationSlug() - - if(currentOrganizationSlug != organizationSlug) { - notFound() + if (currentOrganizationSlug !== organizationSlug) { + notFound(); } + const org = await db.query.organization.findFirst({ + where: eq(drizzleOrganization.slug, currentOrganizationSlug), + }); + if (!org) notFound(); - const project = await prisma.project.findUnique({ - where: { - id: projectId, - isArchived: {not: true} + const proj = await db.query.project.findFirst({ + where: (proj, { and, eq, not }) => and(eq(proj.id, projectId), eq(proj.organizationId, org.id), not(eq(proj.isArchived, true))), + with: { + databases: true, }, - include:{ - databases: {} - } - }) + }); - if(!project){ - notFound() - } + if (!proj) notFound(); return (
- {project.name} - - + {proj.name} + + - +
- - The list of associated databases - + + The list of associated databases + - {project.databases.length > 0 ? + {proj.databases.length > 0 ? ( - : - null - } + ) : null}
- ) -} \ No newline at end of file + ); +} diff --git a/app/(customer)/dashboard/[slug]/projects/new/page.tsx b/app/(customer)/dashboard/[slug]/projects/new/page.tsx index bf59ef1e..b610c568 100644 --- a/app/(customer)/dashboard/[slug]/projects/new/page.tsx +++ b/app/(customer)/dashboard/[slug]/projects/new/page.tsx @@ -1,48 +1,48 @@ -import {PageParams} from "@/types/next"; -import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page"; -import {prisma} from "@/prisma"; -import {ProjectForm} from "@/components/wrappers/dashboard/projects/ProjectsForm/ProjectForm"; -import {notFound} from "next/navigation"; -import {getCurrentOrganizationSlug} from "@/features/dashboard/organization-cookie"; +import { PageParams } from "@/types/next"; +import { Page, PageContent, PageHeader, PageTitle } from "@/features/layout/page"; +import { ProjectForm } from "@/components/wrappers/dashboard/projects/ProjectsForm/ProjectForm"; +import { notFound } from "next/navigation"; +import { getCurrentOrganizationSlug } from "@/features/dashboard/organization-cookie"; +import { db } from "@/db"; +import { DatabaseWith, organization as drizzleOrganization } from "@/db/schema"; +import { eq } from "drizzle-orm"; -export default async function RoutePage(props: PageParams<{slug: string}>) { - const {slug: organizationSlug} = await props.params +export default async function RoutePage(props: PageParams<{ slug: string }>) { + const { slug: organizationSlug } = await props.params; - const currentOrganizationSlug = await getCurrentOrganizationSlug() - if(currentOrganizationSlug != organizationSlug) { - notFound() + const currentOrganizationSlug = await getCurrentOrganizationSlug(); + if (currentOrganizationSlug !== organizationSlug) { + notFound(); } + const availableDatabases = ( + await db.query.database.findMany({ + where: (db, { isNull }) => isNull(db.projectId), + with: { + agent: true, + project: true, + backups: true, + restorations: true, + }, + orderBy: (db, { desc }) => [desc(db.createdAt)], + }) + ).filter((db) => db.project !== null) as DatabaseWith[]; - const availableDatabases = await prisma.database.findMany({ - where: { - projectId: null - }, - include: { - agent: {} - }, - orderBy: { - createdAt: 'desc', - }, - }) + const org = await db.query.organization.findFirst({ + where: eq(drizzleOrganization.slug, currentOrganizationSlug), + }); - const organization = await prisma.organization.findFirst({ - where: { - slug: currentOrganizationSlug, - } - }) + if (!org) notFound(); return ( - - Create new project - + Create new project - + - ) -} \ No newline at end of file + ); +} diff --git a/app/(customer)/dashboard/[slug]/projects/page.tsx b/app/(customer)/dashboard/[slug]/projects/page.tsx index 3c731b14..f4b041d4 100644 --- a/app/(customer)/dashboard/[slug]/projects/page.tsx +++ b/app/(customer)/dashboard/[slug]/projects/page.tsx @@ -1,43 +1,34 @@ -import Link from 'next/link' +import Link from "next/link"; -import {PageParams} from "@/types/next"; -import {prisma} from "@/prisma"; -import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination"; -import {Button} from "@/components/ui/button"; -import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page"; -import {ProjectCard} from "@/components/wrappers/dashboard/projects/ProjectCard/ProjectCard"; -import {getCurrentOrganizationSlug} from "@/features/dashboard/organization-cookie"; -import {notFound} from "next/navigation"; +import { PageParams } from "@/types/next"; +import { CardsWithPagination } from "@/components/wrappers/common/cards-with-pagination"; +import { Button } from "@/components/ui/button"; +import { Page, PageActions, PageContent, PageHeader, PageTitle } from "@/features/layout/page"; +import { ProjectCard } from "@/components/wrappers/dashboard/projects/ProjectCard/ProjectCard"; +import { db } from "@/db"; +import { notFound } from "next/navigation"; +import { getOrganization } from "@/lib/auth/auth"; +export default async function RoutePage(props: PageParams<{ slug: string }>) { + const { slug: organizationSlug } = await props.params; -export default async function RoutePage(props: PageParams<{slug: string}>) { - const {slug: organizationSlug} = await props.params + const organization = await getOrganization(organizationSlug); + if (!organization || organization?.slug !== organizationSlug) { + notFound(); + } - const projects = await prisma.project.findMany({ - where: { - isArchived: {not: true}, - - organization: { - slug: organizationSlug, - // users: { - // some: { - // userId: user.id - // }, - // }, - } + const projects = await db.query.project.findMany({ + where: (project, { eq, and, not }) => and(eq(project.organizationId, organization.id), not(eq(project.isArchived, true))), + with: { + databases: true, }, - include: { - databases: {} - } - }) + }); return ( - - Projects - + Projects {projects.length > 0 && ( @@ -45,28 +36,20 @@ export default async function RoutePage(props: PageParams<{slug: string}>) { )} - - - {projects.length > 0 ? - - : + {projects.length > 0 ? ( + + ) : ( + className=" flex item-center justify-center border-2 border-dashed transition-colors border-primary p-8 lg:p-12 w-full rounded-md" + > Create new Project - - } + )} - ) -} \ No newline at end of file + ); +} diff --git a/app/(customer)/dashboard/[slug]/statistics/page.tsx b/app/(customer)/dashboard/[slug]/statistics/page.tsx index f459642b..6343853a 100644 --- a/app/(customer)/dashboard/[slug]/statistics/page.tsx +++ b/app/(customer)/dashboard/[slug]/statistics/page.tsx @@ -1,48 +1,57 @@ -import {PageParams} from "@/types/next"; -import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page"; -import {prisma} from "@/prisma"; -import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card"; -import {EvolutionLineChart} from "@/components/wrappers/dashboard/statistics/charts/evolution-line-chart"; -import {PercentageLineChart} from "@/components/wrappers/dashboard/statistics/charts/percentage-line-chart"; +import { PageParams } from "@/types/next"; +import { Page, PageContent, PageHeader, PageTitle } from "@/features/layout/page"; +import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"; +import { EvolutionLineChart } from "@/components/wrappers/dashboard/statistics/charts/evolution-line-chart"; +import { PercentageLineChart } from "@/components/wrappers/dashboard/statistics/charts/percentage-line-chart"; +import { getCurrentOrganizationSlug } from "@/features/dashboard/organization-cookie"; +import { notFound } from "next/navigation"; +import { db } from "@/db"; +import { asc, count, eq, inArray } from "drizzle-orm"; +import { organization as drizzleOrganization, project as drizzleProject, backup as drizzleBackup } from "@/db/schema"; -export default async function RoutePage(props: PageParams<{}>) { +export default async function RoutePage(props: PageParams<{ slug: string }>) { + const { slug: organizationSlug } = await props.params; - const projectsCount = await prisma.project.count({ - where: { - organization: { - slug: "default" - } - }, + const currentOrganizationSlug = await getCurrentOrganizationSlug(); + if (currentOrganizationSlug !== organizationSlug) { + notFound(); + } + + const org = await db.query.organization.findFirst({ + where: eq(drizzleOrganization.slug, currentOrganizationSlug), }); - const backupsEvolution = await prisma.backup.findMany({ - select: { + if (!org) notFound(); + + const projects = await db.query.project.findMany({ + where: eq(drizzleProject.organizationId, org.id), + }); + + const projectsCount = projects.length; + + const backupsEvolution = await db.query.backup.findMany({ + columns: { + id: true, createdAt: true, }, - orderBy: { - createdAt: "asc", - }, - }); - - const backupsRate = await prisma.backup.groupBy({ - by: ['createdAt', 'status'], - where: { - status: { - in: ['success', 'failed'], - }, - }, - _count: { - id: true, - }, + orderBy: [asc(drizzleBackup.id)], }); + const backupsRate = await db + .select({ + createdAt: drizzleBackup.createdAt, + status: drizzleBackup.status, + _count: count(), + }) + .from(drizzleBackup) + .where(inArray(drizzleBackup.status, ["success", "failed"])) + .groupBy(drizzleBackup.createdAt, drizzleBackup.status) + .orderBy(drizzleBackup.createdAt); return ( - - Statistics - + Statistics
@@ -50,23 +59,19 @@ export default async function RoutePage(props: PageParams<{}>) { Projects - - {projectsCount} - + {projectsCount} KPI 2 - - + KPI 3 - - +
@@ -75,7 +80,7 @@ export default async function RoutePage(props: PageParams<{}>) { Evolution of the number of backups - + @@ -83,11 +88,11 @@ export default async function RoutePage(props: PageParams<{}>) { Success rate of backups - +
- ) -} \ No newline at end of file + ); +} diff --git a/app/(customer)/dashboard/layout.tsx b/app/(customer)/dashboard/layout.tsx index 6aa80d17..080a9a09 100644 --- a/app/(customer)/dashboard/layout.tsx +++ b/app/(customer)/dashboard/layout.tsx @@ -1,37 +1,31 @@ import React from "react"; -import {redirect} from "next/navigation"; +import { redirect } from "next/navigation"; -import {SidebarInset, SidebarProvider} from "@/components/ui/sidebar" -import {AppSidebar} from "@/components/wrappers/dashboard/sideBar/app-sidebar"; -import {currentUser, requiredCurrentUser} from "@/auth/current-user"; -import {Header} from "@/features/layout/Header"; -import {prisma} from "@/prisma"; +import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"; +import { AppSidebar } from "@/components/wrappers/dashboard/sideBar/app-sidebar"; +import { Header } from "@/features/layout/Header"; +import { currentUser } from "@/lib/auth/current-user"; +export default async function Layout({ children }: { children: React.ReactNode }) { + const user = await currentUser(); -export default async function Layout({children}: { children: React.ReactNode }) { - - const user = await requiredCurrentUser() - - if (!user) redirect('/login') + if (!user) redirect("/login"); return ( // // <> -
- + -
-
- {children} -
+
+
{children}
//
//
- ) -} \ No newline at end of file + ); +} diff --git a/app/(customer)/dashboard/loading.tsx b/app/(customer)/dashboard/loading.tsx index 6bb99a62..fbc8f61e 100644 --- a/app/(customer)/dashboard/loading.tsx +++ b/app/(customer)/dashboard/loading.tsx @@ -1,9 +1,9 @@ -import {LoadingSpinner} from "@/components/wrappers/common/loading/LoadingSpinner"; +import { LoadingSpinner } from "@/components/wrappers/common/loading/LoadingSpinner"; export default function Loading() { return (
- ) -} \ No newline at end of file + ); +} diff --git a/app/(customer)/dashboard/profile/page.tsx b/app/(customer)/dashboard/profile/page.tsx index faefd99d..b105ab0e 100644 --- a/app/(customer)/dashboard/profile/page.tsx +++ b/app/(customer)/dashboard/profile/page.tsx @@ -1,44 +1,62 @@ -import {PageParams} from "@/types/next"; -import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page"; -import {notFound} from "next/navigation"; -import {requiredCurrentUser} from "@/auth/current-user"; -import {UserForm} from "@/components/wrappers/dashboard/profile/UserForm/UserForm"; -import {prisma} from "@/prisma"; -import {Badge} from "@/components/ui/badge"; -import {ButtonDeleteAccount} from "@/components/wrappers/dashboard/profile/ButtonDeleteAccount/ButtonDeleteAccount"; -import {AvatarWithUpload} from "@/components/wrappers/dashboard/profile/Avatar/AvatarWithUpload"; +import { PageParams } from "@/types/next"; +import { Page, PageActions, PageContent, PageHeader, PageTitle } from "@/features/layout/page"; +import { notFound } from "next/navigation"; +import { UserForm } from "@/components/wrappers/dashboard/profile/UserForm/UserForm"; +import { Badge } from "@/components/ui/badge"; +import { ButtonDeleteAccount } from "@/components/wrappers/dashboard/profile/ButtonDeleteAccount/ButtonDeleteAccount"; +import { AvatarWithUpload } from "@/components/wrappers/dashboard/profile/Avatar/AvatarWithUpload"; +import { currentUser } from "@/lib/auth/current-user"; +//import { getAccounts, getSessions } from "@/lib/auth/auth"; export default async function RoutePage(props: PageParams<{}>) { + const user = await currentUser(); - const user = await requiredCurrentUser() if (!user) { - notFound() + return notFound(); } - const userInfo = await prisma.user.findUnique({ - where: { - email: user.email - } - }) + + if (user.role !== "user" && user.role !== "admin" && user.role !== "superadmin") { + return notFound(); + } + + // const sessions = await getSessions(); + // const accounts = await getAccounts(); + return ( -
- - - {user.name} - {userInfo.authMethod} - {userInfo.role} - - - - -
+
+ + + {user.name} + {user.role} + + + + +
- +
-
- ) -} \ No newline at end of file + ); +} diff --git a/app/(landing)/home/page.tsx b/app/(landing)/home/page.tsx index 107c425f..a16387b1 100644 --- a/app/(landing)/home/page.tsx +++ b/app/(landing)/home/page.tsx @@ -1,7 +1,7 @@ export default function Home() { - return ( -
-

Home landing page

-
- ); + return ( +
+

Home landing page

+
+ ); } diff --git a/app/(landing)/page.tsx b/app/(landing)/page.tsx index a8742203..76ce4fa4 100644 --- a/app/(landing)/page.tsx +++ b/app/(landing)/page.tsx @@ -1,15 +1,14 @@ -import {redirect} from "next/navigation"; -import {currentUser} from "@/auth/current-user"; -import {getCurrentOrganizationSlug} from "@/features/dashboard/organization-cookie"; - +import { redirect } from "next/navigation"; +import { getCurrentOrganizationSlug } from "@/features/dashboard/organization-cookie"; +import { currentUser } from "@/lib/auth/current-user"; export default async function Index() { - const user = await currentUser() + const user = await currentUser(); if (user) { - const currentOrganizationSlug = await getCurrentOrganizationSlug() - redirect(`/dashboard/${currentOrganizationSlug}/projects`) + const currentOrganizationSlug = await getCurrentOrganizationSlug(); + redirect(`/dashboard/${currentOrganizationSlug}/projects`); } - redirect("/login") + redirect("/login"); //Do not delete // return ( // diff --git a/app/api/auth/[...all]/route.ts b/app/api/auth/[...all]/route.ts new file mode 100644 index 00000000..e64481ca --- /dev/null +++ b/app/api/auth/[...all]/route.ts @@ -0,0 +1,4 @@ +import { auth } from "@/lib/auth/auth"; +import { toNextJsHandler } from "better-auth/next-js"; + +export const { GET, POST } = toNextJsHandler(auth.handler); diff --git a/app/api/auth/[...nextauth]/route.ts b/app/api/auth/[...nextauth]/route.ts deleted file mode 100644 index 4072fadd..00000000 --- a/app/api/auth/[...nextauth]/route.ts +++ /dev/null @@ -1,4 +0,0 @@ -import {handlers} from "@/auth/auth"; - - -export const {GET, POST} = handlers; diff --git a/app/api/images/[fileName]/route.ts b/app/api/images/[fileName]/route.ts index a8196e26..191d08d4 100644 --- a/app/api/images/[fileName]/route.ts +++ b/app/api/images/[fileName]/route.ts @@ -1,21 +1,17 @@ import path from "path"; import fs from "fs/promises"; -import {NextResponse} from "next/server"; - -export async function GET( - request: Request, - {params}: { params: Promise<{ fileName: string }> } -) { +import { NextResponse } from "next/server"; +export async function GET({ params }: { params: Promise<{ fileName: string }> }) { try { - const fileName = (await params).fileName + const fileName = (await params).fileName; const filePath = path.join(process.cwd(), "private/uploads/images", fileName); // Check if the file exists try { await fs.access(filePath); // Ensures the file exists } catch { - return NextResponse.json({error: "File not found"}, {status: 404}); + return NextResponse.json({ error: "File not found" }, { status: 404 }); } // Read the file @@ -29,6 +25,6 @@ export async function GET( }); } catch (error) { console.error("Error reading file:", error); - return NextResponse.json({error: "Internal Server Error"}, {status: 500}); + return NextResponse.json({ error: "Internal Server Error" }, { status: 500 }); } } diff --git a/app/api/init/route.ts b/app/api/init/route.ts index ba557cdb..7adf2f5b 100644 --- a/app/api/init/route.ts +++ b/app/api/init/route.ts @@ -1,20 +1,14 @@ -import {NextResponse} from "next/server"; - +import { NextResponse } from "next/server"; export type BodyInit = { - initialize: boolean -} + initialize: boolean; +}; - - -export async function POST( - request: Request, -) { +export async function POST(request: Request) { try { const body: BodyInit = await request.json(); - console.log(body) - + console.log(body); return NextResponse.json( { @@ -24,9 +18,6 @@ export async function POST( ); } catch (error) { console.error("Error in POST initialization:", error); - return NextResponse.json( - { error: "Internal server error" }, - { status: 500 } - ); + return NextResponse.json({ error: "Internal server error" }, { status: 500 }); } -} \ No newline at end of file +} diff --git a/app/error/page.tsx b/app/error/page.tsx index 10e704e2..38a61506 100644 --- a/app/error/page.tsx +++ b/app/error/page.tsx @@ -24,7 +24,7 @@ //
// //
// Something went wrong @@ -63,7 +63,7 @@ function AuthErrorPageContent() {
Something went wrong diff --git a/app/globals.css b/app/globals.css index 08f0c206..e8b4b397 100644 --- a/app/globals.css +++ b/app/globals.css @@ -1,105 +1,142 @@ -@tailwind base; -@tailwind components; -@tailwind utilities; +@import "tailwindcss"; +@import "tw-animate-css"; -/*body {*/ -/* font-family: Arial, Helvetica, sans-serif;*/ -/*}*/ +@custom-variant dark (&:is(.dark *)); -@layer utilities { - /*.text-balance {*/ - /* text-wrap: balance;*/ - /*}*/ - .popover-content-width-full { - width: var(--radix-popover-trigger-width); - max-height: var(--radix-popover-content-available-height); +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --font-sans: var(--font-geist-sans); + --font-mono: var(--font-geist-mono); + --color-sidebar-ring: var(--sidebar-ring); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar: var(--sidebar); + --color-chart-5: var(--chart-5); + --color-chart-4: var(--chart-4); + --color-chart-3: var(--chart-3); + --color-chart-2: var(--chart-2); + --color-chart-1: var(--chart-1); + --color-ring: var(--ring); + --color-input: var(--input); + --color-border: var(--border); + --color-destructive: var(--destructive); + --color-accent-foreground: var(--accent-foreground); + --color-accent: var(--accent); + --color-muted-foreground: var(--muted-foreground); + --color-muted: var(--muted); + --color-secondary-foreground: var(--secondary-foreground); + --color-secondary: var(--secondary); + --color-primary-foreground: var(--primary-foreground); + --color-primary: var(--primary); + --color-popover-foreground: var(--popover-foreground); + --color-popover: var(--popover); + --color-card-foreground: var(--card-foreground); + --color-card: var(--card); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --animate-accordion-down: accordion-down 0.2s ease-out; + --animate-accordion-up: accordion-up 0.2s ease-out; + + @keyframes accordion-down { + from { + height: 0; + } + to { + height: var(--radix-accordion-content-height); + } + } + + @keyframes accordion-up { + from { + height: var(--radix-accordion-content-height); + } + to { + height: 0; + } } } - - -@layer base { - :root { - --background: 0 0% 100%; - --foreground: 240 10% 3.9%; - --card: 0 0% 100%; - --card-foreground: 240 10% 3.9%; - --popover: 0 0% 100%; - --popover-foreground: 240 10% 3.9%; - --primary: 240 5.9% 10%; - --primary-foreground: 0 0% 98%; - --secondary: 240 4.8% 95.9%; - --secondary-foreground: 240 5.9% 10%; - --muted: 240 4.8% 95.9%; - --muted-foreground: 240 3.8% 46.1%; - --accent: 240 4.8% 95.9%; - --accent-foreground: 240 5.9% 10%; - --destructive: 0 84.2% 60.2%; - --destructive-foreground: 0 0% 98%; - --border: 240 5.9% 90%; - --input: 240 5.9% 90%; - --ring: 240 5.9% 10%; - --radius: 0.5rem; - --chart-1: 12 76% 61%; - --chart-2: 173 58% 39%; - --chart-3: 197 37% 24%; - --chart-4: 43 74% 66%; - --chart-5: 27 87% 67%; - --sidebar-background: 0 0% 98%; - --sidebar-foreground: 240 5.3% 26.1%; - --sidebar-primary: 240 5.9% 10%; - --sidebar-primary-foreground: 0 0% 98%; - --sidebar-accent: 240 4.8% 95.9%; - --sidebar-accent-foreground: 240 5.9% 10%; - --sidebar-border: 220 13% 91%; - --sidebar-ring: 217.2 91.2% 59.8%; - } - - .dark { - --background: 240 10% 3.9%; - --foreground: 0 0% 98%; - --card: 240 10% 3.9%; - --card-foreground: 0 0% 98%; - --popover: 240 10% 3.9%; - --popover-foreground: 0 0% 98%; - --primary: 0 0% 98%; - --primary-foreground: 240 5.9% 10%; - --secondary: 240 3.7% 15.9%; - --secondary-foreground: 0 0% 98%; - --muted: 240 3.7% 15.9%; - --muted-foreground: 240 5% 64.9%; - --accent: 240 3.7% 15.9%; - --accent-foreground: 0 0% 98%; - --destructive: 0 62.8% 30.6%; - --destructive-foreground: 0 0% 98%; - --border: 240 3.7% 15.9%; - --input: 240 3.7% 15.9%; - --ring: 240 4.9% 83.9%; - --chart-1: 220 70% 50%; - --chart-2: 160 60% 45%; - --chart-3: 30 80% 55%; - --chart-4: 280 65% 60%; - --chart-5: 340 75% 55%; - --sidebar-background: 240 5.9% 10%; - --sidebar-foreground: 240 4.8% 95.9%; - --sidebar-primary: 224.3 76.3% 48%; - --sidebar-primary-foreground: 0 0% 100%; - --sidebar-accent: 240 3.7% 15.9%; - --sidebar-accent-foreground: 240 4.8% 95.9%; - --sidebar-border: 240 3.7% 15.9%; - --sidebar-ring: 217.2 91.2% 59.8%; - - } +:root { + --radius: 0.625rem; + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); } +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.205 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.205 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.922 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.704 0.191 22.216); + --border: oklch(1 0 0 / 10%); + --input: oklch(1 0 0 / 15%); + --ring: oklch(0.556 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(1 0 0 / 10%); + --sidebar-ring: oklch(0.556 0 0); +} @layer base { * { - @apply border-border; + @apply border-border outline-ring/50; } - body { @apply bg-background text-foreground; } } - diff --git a/app/layout.tsx b/app/layout.tsx index f562fc42..0006e7b5 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -1,13 +1,12 @@ import React from "react"; -import type {Metadata} from "next"; -import {Inter} from "next/font/google"; +import type { Metadata } from "next"; +import { Inter } from "next/font/google"; import "./globals.css"; -import {Providers} from "./providers"; -import {cn} from "@/lib/utils"; +import { Providers } from "./providers"; +import { cn } from "@/lib/utils"; - -const inter = Inter({subsets: ["latin"]}); +const inter = Inter({ subsets: ["latin"] }); export const metadata: Metadata = { title: "Portabase", @@ -15,19 +14,15 @@ export const metadata: Metadata = { }; export default function RootLayout({ - children, - }: Readonly<{ + children, +}: Readonly<{ children: React.ReactNode; }>) { return ( - - - {children} - - + + {children} + ); } diff --git a/app/not-found.tsx b/app/not-found.tsx index 1286056c..95f0b7d4 100644 --- a/app/not-found.tsx +++ b/app/not-found.tsx @@ -1,6 +1,5 @@ -import {redirect, useRouter} from "next/navigation"; +import { redirect } from "next/navigation"; export default async function NotFound() { - console.log("not found"); - redirect("/") -} \ No newline at end of file + redirect("/dashboard/profile"); +} diff --git a/app/providers.tsx b/app/providers.tsx index edd79d7b..8d933179 100644 --- a/app/providers.tsx +++ b/app/providers.tsx @@ -1,32 +1,23 @@ -"use client" +"use client"; -import {PropsWithChildren, Suspense} from "react"; -import {ThemeProvider} from "@/features/theme/theme-provider"; +import { PropsWithChildren, Suspense } from "react"; +import { ThemeProvider } from "@/features/theme/theme-provider"; -import {Toaster} from "@/components/ui/sonner"; -import {QueryClient, QueryClientProvider,} from '@tanstack/react-query' -import {SessionProvider} from "next-auth/react"; +import { Toaster } from "@/components/ui/sonner"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; export type ProviderProps = PropsWithChildren<{}>; -const queryClient = new QueryClient() +const queryClient = new QueryClient(); export const Providers = (props: ProviderProps) => { return ( - - - - - {props.children} - - - + + + + {props.children} + + - - ) -} + ); +}; diff --git a/docker-compose.yml b/docker-compose.yml index 6c149daf..388e0a05 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,68 +1,77 @@ name: portabase-dev services: + app: + build: + context: . + dockerfile: docker/dockerfile/Dockerfile + target: dev + ports: + - "8887:80" + environment: + - TIME_ZONE="Europe/Paris" + - NODE_ENV=development + depends_on: + db: + condition: service_healthy + volumes: + - .:/app + - /app/node_modules + container_name: portabase-app - app: - build: - context: . - dockerfile: docker/dockerfile/Dockerfile - target: dev - ports: - - '8887:80' - environment: - - TIME_ZONE="Europe/Paris" - - NODE_ENV=development - depends_on: - db: - condition: service_healthy - volumes: - - .:/app - - /app/node_modules - container_name: portabase-app - - develop: - watch: - - action: sync - path: . - target: /app - ignore: - - node_modules/ - - action: rebuild - path: package.json - - - db: - image: postgres:16-alpine - ports: - - "5433:5432" - volumes: - - postgres-data:/var/lib/postgresql/data - environment: - - POSTGRES_DB=devdb - - POSTGRES_USER=devuser - - POSTGRES_PASSWORD=changeme - healthcheck: - test: [ "CMD-SHELL", "pg_isready -U devuser -d devdb" ] - interval: 10s - timeout: 5s - retries: 5 - - - s3: - image: docker.io/bitnami/minio:latest - ports: - - '9000:9000' - - '9001:9001' - volumes: - - minio_data:/data - environment: - - MINIO_ROOT_USER=${S3_ACCESS_KEY} - - MINIO_ROOT_PASSWORD=${S3_SECRET_KEY} - - MINIO_DEFAULT_BUCKETS=${S3_BUCKET_NAME} + develop: + watch: + - action: sync + path: . + target: /app + - action: rebuild + path: package.json + db: + image: postgres:16-alpine + ports: + - "5433:5432" + volumes: + - postgres-data:/var/lib/postgresql/data + environment: + - POSTGRES_DB=devdb + - POSTGRES_USER=devuser + - POSTGRES_PASSWORD=changeme + healthcheck: + test: ["CMD-SHELL", "pg_isready -U devuser -d devdb"] + interval: 10s + timeout: 5s + retries: 5 + db-pgadmin: + image: dpage/pgadmin4 + environment: + PGADMIN_DEFAULT_EMAIL: "devuser@devuser.devuser" + PGADMIN_DEFAULT_PASSWORD: "changeme" + PGADMIN_CONFIG_SERVER_MODE: "False" + POSTGRES_USER: "devuser" + POSTGRES_PASSWORD: "changeme" + volumes: + - pgadmin-data:/var/lib/pgadmin + ports: + - "8080:80" + restart: unless-stopped + depends_on: + - db + s3: + image: docker.io/bitnami/minio:latest + ports: + - "9000:9000" + - "9001:9001" + volumes: + - minio_data:/data + environment: + - MINIO_ROOT_USER=${S3_ACCESS_KEY} + - MINIO_ROOT_PASSWORD=${S3_SECRET_KEY} + - MINIO_DEFAULT_BUCKETS=${S3_BUCKET_NAME} volumes: - postgres-data: - minio_data: + postgres-data: + minio_data: + pgadmin-data: diff --git a/docker/entrypoints/app-dev-entrypoint.sh b/docker/entrypoints/app-dev-entrypoint.sh index d838cbd8..d17c8d8c 100755 --- a/docker/entrypoints/app-dev-entrypoint.sh +++ b/docker/entrypoints/app-dev-entrypoint.sh @@ -3,11 +3,8 @@ npm install -g npm@11.0.0 npm -v - -current_date=$(date +"%Y-%m-%d") npx zenstack generate -npx prisma migrate dev --name $current_date - +npx drizzle-kit migrate npm run dev exec "$@" \ No newline at end of file diff --git a/docker/entrypoints/app-prod-entrypoint.sh b/docker/entrypoints/app-prod-entrypoint.sh index 255b86e9..82264aea 100644 --- a/docker/entrypoints/app-prod-entrypoint.sh +++ b/docker/entrypoints/app-prod-entrypoint.sh @@ -10,7 +10,7 @@ echo " Community Edition v1.0.0 " echo " " -npx prisma migrate deploy +npx drizzle-kit migrate node server.js exec "$@" \ No newline at end of file diff --git a/drizzle.config.ts b/drizzle.config.ts new file mode 100644 index 00000000..5f99e2a0 --- /dev/null +++ b/drizzle.config.ts @@ -0,0 +1,16 @@ +import { defineConfig } from "drizzle-kit"; + +import dotenv from "dotenv"; + +dotenv.config({ + path: ".env", +}); + +export default defineConfig({ + out: "./src/db/migrations", + schema: ["./src/db/schema", "./src/db/schema/types.ts"], + dialect: "postgresql", + dbCredentials: { + url: process.env.DATABASE_URL!, + }, +}); diff --git a/emails/Button.tsx b/emails/Button.tsx index 3f0a9988..9173a920 100644 --- a/emails/Button.tsx +++ b/emails/Button.tsx @@ -1,13 +1,6 @@ -import { - Button as ReactEmailButton -} from "@react-email/components"; -import {ComponentPropsWithoutRef} from "react"; +import { Button as ReactEmailButton } from "@react-email/components"; +import { ComponentPropsWithoutRef } from "react"; export const Button = (props: ComponentPropsWithoutRef) => { - return ( - - ) -} \ No newline at end of file + return ; +}; diff --git a/emails/EmailLayout.tsx b/emails/EmailLayout.tsx index 21a23c81..815af7af 100644 --- a/emails/EmailLayout.tsx +++ b/emails/EmailLayout.tsx @@ -1,18 +1,7 @@ -import { - Body, - Container, - Head, - Html, - Img, - Preview, - Section, - Tailwind, -} from "@react-email/components"; +import { Body, Container, Head, Html, Img, Preview, Section, Tailwind } from "@react-email/components"; import * as React from "react"; import { PropsWithChildren } from "react"; -import {getServerUrl} from "@/utils/get-server-url"; - - +import { getServerUrl } from "@/utils/get-server-url"; const baseUrl = getServerUrl(); @@ -22,19 +11,10 @@ export const EmailLayout = ({ children, preview }: PropsWithChildren<{ preview?: {preview ? {preview} : Please check your mails} - - + - Logo -
- {children} -
+ Logo +
{children}
@@ -42,6 +22,4 @@ export const EmailLayout = ({ children, preview }: PropsWithChildren<{ preview?: ); }; - - export default EmailLayout; diff --git a/emails/HelloEmail.tsx b/emails/HelloEmail.tsx index b08f2cb5..8b3c02a2 100644 --- a/emails/HelloEmail.tsx +++ b/emails/HelloEmail.tsx @@ -5,12 +5,9 @@ import * as React from "react"; export default function HelloEmail() { return ( - ); -} \ No newline at end of file +} diff --git a/emails/TestEmailSettings.tsx b/emails/TestEmailSettings.tsx index f191ed02..8b65c4d2 100644 --- a/emails/TestEmailSettings.tsx +++ b/emails/TestEmailSettings.tsx @@ -1,42 +1,16 @@ -import { - Text, -} from "@react-email/components"; +import { Text } from "@react-email/components"; import * as React from "react"; import EmailLayout from "./EmailLayout"; -import {Button} from "@react-email/button"; - +import { Button } from "@react-email/button"; export const TestEmailSettings = () => { return ( - - - - Hi, your email settings are setup ! - - - - - Best regard, - - Portabase - - + + Hi, your email settings are setup ! + Best regard,{" "} + Portabase ); }; - - - export default TestEmailSettings; diff --git a/emails/Text.tsx b/emails/Text.tsx index 1491061b..7322e396 100644 --- a/emails/Text.tsx +++ b/emails/Text.tsx @@ -1,13 +1,6 @@ -import { - Text as ReactEmailText -} from "@react-email/components"; -import {ComponentPropsWithoutRef} from "react"; +import { Text as ReactEmailText } from "@react-email/components"; +import { ComponentPropsWithoutRef } from "react"; export const Text = (props: ComponentPropsWithoutRef) => { - return ( - - ) -} \ No newline at end of file + return ; +}; diff --git a/instrumentation.ts b/instrumentation.ts index 8009caa6..63fe3b16 100644 --- a/instrumentation.ts +++ b/instrumentation.ts @@ -1,6 +1,6 @@ -import {init} from "@/utils/init"; - - -export function register() { - init(); +export async function register() { + if (process.env.NEXT_RUNTIME === "nodejs") { + const init = await import("@/utils/init"); + init.init(); + } } diff --git a/middleware.ts b/middleware.ts index 20b1619a..a1b17085 100644 --- a/middleware.ts +++ b/middleware.ts @@ -1,43 +1,54 @@ -import {NextRequest, NextResponse} from 'next/server' -import {loggingMiddleware} from "@/middleware/loggingMiddleware"; -import {errorHandler} from "@/middleware/errorHandler"; +import { NextRequest, NextResponse } from "next/server"; +import { loggingMiddleware } from "@/middleware/loggingMiddleware"; +import { errorHandler } from "@/middleware/errorHandler"; +import { auth } from "@/lib/auth/auth"; +import { headers } from "next/headers"; -export function middleware(request: NextRequest) { +export async function middleware(request: NextRequest) { const url = request.nextUrl.clone(); - // Exclude `/api/auth` and its subpaths - if (url.pathname.startsWith('/api/auth')) { + if (url.pathname.startsWith("/dashboard")) { + const session = await auth.api.getSession({ + headers: await headers(), + }); + + if (!session) { + return NextResponse.redirect(new URL("/login", request.url)); + } + return NextResponse.next(); } - if (url.pathname.startsWith('/api')) { + // Exclude `/api/auth` and its subpaths + if (url.pathname.startsWith("/api/auth")) { + return NextResponse.next(); + } + if (url.pathname.startsWith("/api")) { const routeExists = checkRouteExists(url.pathname); // If the route does not exist, return a 404 JSON response if (!routeExists) { - return new NextResponse( - JSON.stringify({ message: "This API route does not exist.", status: 404 }), - { status: 404, headers: { 'Content-Type': 'application/json' } } - ); + return new NextResponse(JSON.stringify({ message: "This API route does not exist.", status: 404 }), { + status: 404, + headers: { "Content-Type": "application/json" }, + }); } } - try{ + try { loggingMiddleware(request); - }catch(err){ - errorHandler(err) + } catch (err) { + errorHandler(err); } - - } // Function to check if the route exists (supports dynamic routes) -function checkRouteExists(pathname) { +function checkRouteExists(pathname: string) { // Define static and dynamic routes with patterns const routePatterns = [ //do not delete // /^\/api\/auth\/\d+$/, // Dynamic route with a number as a parameter (e.g., /api/dynamic/123) // /^\/api\/auth\/\w+$/, // Dynamic route with a number as a parameter (e.g., /api/dynamic/123) // /^\/api\/agent\/healthcheck\/\w+$/, // Dynamic route with an alphanumeric parameter (e.g., /api/user/username) - /^\/api\/agent\/[^/]+\/status\/?$/, // Dynamic route for /api/agent/[id]/status + /^\/api\/agent\/[^/]+\/status\/?$/, // Dynamic route for /api/agent/[id]/status /^\/api\/agent\/[^/]+\/backup\/?$/, /^\/api\/agent\/[^/]+\/restore\/?$/, /^\/api\/files\/[^/]+\/?$/, @@ -45,13 +56,15 @@ function checkRouteExists(pathname) { /^\/api\/events\/?$/, /^\/api\/init\/?$/, ]; - return routePatterns.some(pattern => pattern.test(pathname)); + return routePatterns.some((pattern) => pattern.test(pathname)); } - export const config = { + runtime: "nodejs", matcher: [ // '/api/agent/:path*', - '/api/:path*', + "/api/:path*", + "/dashboard/:path*", + "/dashboard", ], -}; \ No newline at end of file +}; diff --git a/next.config.mjs b/next.config.ts similarity index 50% rename from next.config.mjs rename to next.config.ts index 3e6142e5..0f5b8056 100644 --- a/next.config.mjs +++ b/next.config.ts @@ -1,6 +1,6 @@ +import type { NextConfig } from "next"; -/** @type {import('next').NextConfig} */ -const nextConfig = { +const nextConfig: NextConfig = { output: "standalone", typescript: { ignoreBuildErrors: true, @@ -9,12 +9,11 @@ const nextConfig = { ignoreDuringBuilds: true, }, compiler: { - removeConsole: process.env.NODE_ENV === 'production', + removeConsole: process.env.NODE_ENV === "production", + }, + experimental: { + nodeMiddleware: true, }, - }; export default nextConfig; - - - diff --git a/package.json b/package.json index 4a563806..02b17c11 100644 --- a/package.json +++ b/package.json @@ -1,106 +1,114 @@ { - "name": "portabase", - "version": "0.1.0", - "private": true, - "scripts": { - "dev": "next dev -p 80", - "build": "next build", - "start": "next start", - "lint": "next lint", - "email": "email dev" - }, - "dependencies": { - "@auth/prisma-adapter": "^2.7.4", - "@hookform/resolvers": "^3.9.1", - "@prisma/client": "^6.3.0", - "@radix-ui/react-accordion": "^1.2.2", - "@radix-ui/react-alert-dialog": "^1.1.5", - "@radix-ui/react-aspect-ratio": "^1.1.1", - "@radix-ui/react-avatar": "^1.1.2", - "@radix-ui/react-checkbox": "^1.1.3", - "@radix-ui/react-collapsible": "^1.1.2", - "@radix-ui/react-context-menu": "^2.2.5", - "@radix-ui/react-dialog": "^1.1.5", - "@radix-ui/react-dropdown-menu": "^2.1.5", - "@radix-ui/react-hover-card": "^1.1.5", - "@radix-ui/react-icons": "^1.3.2", - "@radix-ui/react-label": "^2.1.1", - "@radix-ui/react-menubar": "^1.1.5", - "@radix-ui/react-navigation-menu": "^1.2.4", - "@radix-ui/react-popover": "^1.1.5", - "@radix-ui/react-progress": "^1.1.1", - "@radix-ui/react-radio-group": "^1.2.2", - "@radix-ui/react-scroll-area": "^1.2.2", - "@radix-ui/react-select": "^2.1.5", - "@radix-ui/react-separator": "^1.1.1", - "@radix-ui/react-slider": "^1.2.2", - "@radix-ui/react-slot": "^1.1.1", - "@radix-ui/react-switch": "^1.1.2", - "@radix-ui/react-tabs": "^1.1.2", - "@radix-ui/react-toast": "^1.2.5", - "@radix-ui/react-toggle": "^1.1.1", - "@radix-ui/react-toggle-group": "^1.1.1", - "@radix-ui/react-tooltip": "^1.1.7", - "@react-email/components": "^0.0.32", - "@t3-oss/env-nextjs": "^0.12.0", - "@tanstack/react-query": "^5.66.0", - "@tanstack/react-table": "^8.20.6", - "@types/ws": "^8.5.13", - "@zenstackhq/runtime": "2.10.2", - "argon2": "^0.41.1", - "bcrypt": "^5.1.1", - "class-variance-authority": "^0.7.1", - "clsx": "^2.1.1", - "cmdk": "^1.0.4", - "date-fns": "^4.1.0", - "dockerode": "^4.0.2", - "embla-carousel-react": "^8.3.1", - "input-otp": "^1.4.2", - "lucide-react": "^0.474.0", - "minio": "^8.0.2", - "next": "15.1.7", - "next-auth": "^5.0.0-beta.25", - "next-safe-action": "^7.10.2", - "next-themes": "^0.4.4", - "nodemailer": "^6.10.0", - "npm-check-updates": "^17.1.13", - "react": "19.0.0", - "react-day-picker": "^9.5.1", - "react-dom": "19.0.0", - "react-email": "^3.0.6", - "react-hook-form": "^7.54.2", - "react-resizable-panels": "^2.1.7", - "react-twc": "^1.4.2", - "recharts": "^2.15.1", - "socket.io": "^4.8.1", - "socket.io-client": "^4.8.1", - "sonner": "^1.7.3", - "tailwind-merge": "^3.0.1", - "tailwindcss-animate": "^1.0.7", - "uuid": "^11.0.5", - "vaul": "^1.1.2", - "ws": "^8.18.0", - "zod": "^3.24.1" - }, - "devDependencies": { - "@types/eslint-plugin-tailwindcss": "^3", - "@types/node": "^22", - "@types/pg": "^8", - "@types/react": "^19", - "@types/react-dom": "^19", - "@zenstackhq/openapi": "^2.10.2", - "@zenstackhq/tanstack-query": "^2.10.2", - "eslint": "^8", - "eslint-config-next": "15.1.6", - "eslint-plugin-tailwindcss": "^3.17.5", - "postcss": "^8", - "prisma": "^6.3.0", - "tailwindcss": "^3.4.1", - "typescript": "^5", - "zenstack": "2.10.2" - }, - "overrides": { - "@types/react": "npm:types-react@19.0.0-rc.1", - "@types/react-dom": "npm:types-react-dom@19.0.0-rc.1" - } + "name": "portabase", + "version": "1.1.0", + "private": true, + "scripts": { + "dev": "next dev -p 80", + "build": "next build", + "start": "next start", + "lint": "next lint", + "email": "email dev", + "db:generate": "drizzle-kit generate", + "db:migrate": "drizzle-kit migrate", + "db:drop": "drizzle-kit drop", + "auth:generate": "npx @better-auth/cli generate --config ./src/lib/auth/auth.ts" + }, + "dependencies": { + "@hookform/resolvers": "^5.0.1", + "@radix-ui/react-accordion": "^1.2.10", + "@radix-ui/react-alert-dialog": "^1.1.13", + "@radix-ui/react-aspect-ratio": "^1.1.6", + "@radix-ui/react-avatar": "^1.1.9", + "@radix-ui/react-checkbox": "^1.3.1", + "@radix-ui/react-collapsible": "^1.1.10", + "@radix-ui/react-context-menu": "^2.2.14", + "@radix-ui/react-dialog": "^1.1.13", + "@radix-ui/react-dropdown-menu": "^2.1.14", + "@radix-ui/react-hover-card": "^1.1.13", + "@radix-ui/react-icons": "^1.3.2", + "@radix-ui/react-label": "^2.1.6", + "@radix-ui/react-menubar": "^1.1.14", + "@radix-ui/react-navigation-menu": "^1.2.12", + "@radix-ui/react-popover": "^1.1.13", + "@radix-ui/react-progress": "^1.1.6", + "@radix-ui/react-radio-group": "^1.3.6", + "@radix-ui/react-scroll-area": "^1.2.8", + "@radix-ui/react-select": "^2.2.4", + "@radix-ui/react-separator": "^1.1.6", + "@radix-ui/react-slider": "^1.3.4", + "@radix-ui/react-slot": "^1.2.2", + "@radix-ui/react-switch": "^1.2.4", + "@radix-ui/react-tabs": "^1.1.11", + "@radix-ui/react-toast": "^1.2.13", + "@radix-ui/react-toggle": "^1.1.8", + "@radix-ui/react-toggle-group": "^1.1.9", + "@radix-ui/react-tooltip": "^1.2.6", + "@react-email/components": "^0.0.41", + "@t3-oss/env-nextjs": "^0.13.4", + "@tanstack/react-query": "^5.76.1", + "@tanstack/react-table": "^8.21.3", + "@types/nodemailer": "^6.4.17", + "@types/ws": "^8.18.1", + "@zenstackhq/runtime": "2.14.2", + "argon2": "^0.43.0", + "bcrypt": "^6.0.0", + "better-auth": "^1.2.8", + "class-variance-authority": "^0.7.1", + "clsx": "^2.1.1", + "cmdk": "^1.1.1", + "date-fns": "^4.1.0", + "dockerode": "^4.0.6", + "dotenv": "^16.5.0", + "drizzle-orm": "^0.43.1", + "drizzle-zod": "^0.7.1", + "embla-carousel-react": "^8.6.0", + "input-otp": "^1.4.2", + "lucide-react": "^0.510.0", + "minio": "^8.0.5", + "next": "^15.4.0-canary.36", + "next-safe-action": "^7.10.8", + "next-themes": "^0.4.6", + "nodemailer": "^7.0.3", + "npm-check-updates": "^18.0.1", + "pg": "^8.16.0", + "react": "19.1.0", + "react-day-picker": "9.7.0", + "react-dom": "19.1.0", + "react-dropzone": "^14.3.8", + "react-email": "^4.0.13", + "react-hook-form": "^7.56.3", + "react-resizable-panels": "^3.0.2", + "react-twc": "^1.4.2", + "recharts": "^2.15.3", + "socket.io": "^4.8.1", + "socket.io-client": "^4.8.1", + "sonner": "^2.0.3", + "tailwind-merge": "^3.3.0", + "uuid": "^11.1.0", + "vaul": "^1.1.2", + "ws": "^8.18.2", + "zod": "^3.24.4" + }, + "devDependencies": { + "@iconify/react": "^6.0.0", + "@tailwindcss/postcss": "^4.1.7", + "@types/eslint-plugin-tailwindcss": "^3.17.0", + "@types/node": "^22.15.18", + "@types/pg": "^8.15.2", + "@types/react": "^19.1.4", + "@types/react-dom": "^19.1.5", + "@zenstackhq/openapi": "^2.14.2", + "@zenstackhq/tanstack-query": "^2.14.2", + "drizzle-kit": "^0.31.1", + "eslint": "^9.26.0", + "eslint-config-next": "15.3.2", + "eslint-plugin-tailwindcss": "^3.18.0", + "postcss": "^8.5.3", + "tailwindcss": "^4.1.7", + "tsx": "^4.19.4", + "tw-animate-css": "^1.2.9", + "typescript": "^5.8.3", + "zenstack": "2.14.2" + }, + "packageManager": "yarn@4.8.1+sha512.bc946f2a022d7a1a38adfc15b36a66a3807a67629789496c3714dd1703d2e6c6b1c69ff9ec3b43141ac7a1dd853b7685638eb0074300386a59c18df351ef8ff6" } diff --git a/postcss.config.mjs b/postcss.config.mjs index 1a69fd2a..5d6d8457 100644 --- a/postcss.config.mjs +++ b/postcss.config.mjs @@ -1,7 +1,7 @@ /** @type {import('postcss-load-config').Config} */ const config = { plugins: { - tailwindcss: {}, + '@tailwindcss/postcss': {}, }, }; diff --git a/prisma/migrations/20241103104432_2024_11_03/migration.sql b/prisma/migrations/20241103104432_2024_11_03/migration.sql deleted file mode 100755 index e72a552d..00000000 --- a/prisma/migrations/20241103104432_2024_11_03/migration.sql +++ /dev/null @@ -1,66 +0,0 @@ --- CreateTable -CREATE TABLE "accounts" ( - "id" TEXT NOT NULL, - "user_id" TEXT NOT NULL, - "type" TEXT NOT NULL, - "provider" TEXT NOT NULL, - "provider_account_id" TEXT NOT NULL, - "refresh_token" TEXT, - "access_token" TEXT, - "expires_at" INTEGER, - "token_type" TEXT, - "scope" TEXT, - "id_token" TEXT, - "session_state" TEXT, - - CONSTRAINT "accounts_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "sessions" ( - "id" TEXT NOT NULL, - "session_token" TEXT NOT NULL, - "user_id" TEXT NOT NULL, - "expires" TIMESTAMP(3) NOT NULL, - - CONSTRAINT "sessions_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "verificationtokens" ( - "identifier" TEXT NOT NULL, - "token" TEXT NOT NULL, - "expires" TIMESTAMP(3) NOT NULL -); - --- CreateTable -CREATE TABLE "users" ( - "id" TEXT NOT NULL, - "name" TEXT, - "email" TEXT, - "email_verified" TIMESTAMP(3), - "image" TEXT, - "role" TEXT, - "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "updated_at" TIMESTAMP(3), - - CONSTRAINT "users_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE UNIQUE INDEX "accounts_provider_provider_account_id_key" ON "accounts"("provider", "provider_account_id"); - --- CreateIndex -CREATE UNIQUE INDEX "sessions_session_token_key" ON "sessions"("session_token"); - --- CreateIndex -CREATE UNIQUE INDEX "verificationtokens_identifier_token_key" ON "verificationtokens"("identifier", "token"); - --- CreateIndex -CREATE UNIQUE INDEX "users_email_key" ON "users"("email"); - --- AddForeignKey -ALTER TABLE "accounts" ADD CONSTRAINT "accounts_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "sessions" ADD CONSTRAINT "sessions_user_id_fkey" FOREIGN KEY ("user_id") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20241103192308_2024_11_03/migration.sql b/prisma/migrations/20241103192308_2024_11_03/migration.sql deleted file mode 100755 index 58df1444..00000000 --- a/prisma/migrations/20241103192308_2024_11_03/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterTable -ALTER TABLE "users" ADD COLUMN "password" TEXT; diff --git a/prisma/migrations/20241110103733_2024_11_10/migration.sql b/prisma/migrations/20241110103733_2024_11_10/migration.sql deleted file mode 100644 index 37a8421c..00000000 --- a/prisma/migrations/20241110103733_2024_11_10/migration.sql +++ /dev/null @@ -1,46 +0,0 @@ --- CreateEnum -CREATE TYPE "Status" AS ENUM ('waiting', 'ongoing', 'failed', 'success'); - --- CreateTable -CREATE TABLE "Agent" ( - "id" TEXT NOT NULL, - "name" TEXT NOT NULL, - "description" TEXT, - "last_contact" TIMESTAMP(3), - "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT "Agent_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Database" ( - "id" TEXT NOT NULL, - "agent_id" TEXT NOT NULL, - "name" TEXT NOT NULL, - "description" TEXT, - "backup_policy" TEXT, - "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT "Database_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Backup" ( - "id" TEXT NOT NULL, - "database_id" TEXT NOT NULL, - "status" "Status" NOT NULL DEFAULT 'waiting', - "file" TEXT, - "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT "Backup_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "Restore" ( - "id" TEXT NOT NULL, - "backup_id" TEXT NOT NULL, - "status" "Status" NOT NULL DEFAULT 'waiting', - "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT "Restore_pkey" PRIMARY KEY ("id") -); diff --git a/prisma/migrations/20241110140612_2024_11_10/migration.sql b/prisma/migrations/20241110140612_2024_11_10/migration.sql deleted file mode 100755 index 50f2c768..00000000 --- a/prisma/migrations/20241110140612_2024_11_10/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterTable -ALTER TABLE "users" ADD COLUMN "authMethod" TEXT; diff --git a/prisma/migrations/20241111152326_2024_11_11/migration.sql b/prisma/migrations/20241111152326_2024_11_11/migration.sql deleted file mode 100644 index 8a710941..00000000 --- a/prisma/migrations/20241111152326_2024_11_11/migration.sql +++ /dev/null @@ -1,15 +0,0 @@ -/* - Warnings: - - - A unique constraint covering the columns `[slug]` on the table `Agent` will be added. If there are existing duplicate values, this will fail. - - Added the required column `slug` to the `Agent` table without a default value. This is not possible if the table is not empty. - -*/ --- AlterTable -ALTER TABLE "Agent" ADD COLUMN "slug" TEXT NOT NULL; - --- CreateIndex -CREATE UNIQUE INDEX "Agent_slug_key" ON "Agent"("slug"); - --- AddForeignKey -ALTER TABLE "Database" ADD CONSTRAINT "Database_agent_id_fkey" FOREIGN KEY ("agent_id") REFERENCES "Agent"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20241113175420_2024_11_13/migration.sql b/prisma/migrations/20241113175420_2024_11_13/migration.sql deleted file mode 100644 index b141408d..00000000 --- a/prisma/migrations/20241113175420_2024_11_13/migration.sql +++ /dev/null @@ -1,33 +0,0 @@ -/* - Warnings: - - - You are about to drop the column `authMethod` on the `users` table. All the data in the column will be lost. - - You are about to drop the `Restore` table. If the table is not empty, all the data it contains will be lost. - -*/ --- AlterTable -ALTER TABLE "users" DROP COLUMN "authMethod", -ADD COLUMN "auth_method" TEXT; - --- DropTable -DROP TABLE "Restore"; - --- CreateTable -CREATE TABLE "Restauration" ( - "id" TEXT NOT NULL, - "status" "Status" NOT NULL DEFAULT 'waiting', - "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "backup_id" TEXT NOT NULL, - "database_id" TEXT, - - CONSTRAINT "Restauration_pkey" PRIMARY KEY ("id") -); - --- AddForeignKey -ALTER TABLE "Backup" ADD CONSTRAINT "Backup_database_id_fkey" FOREIGN KEY ("database_id") REFERENCES "Database"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Restauration" ADD CONSTRAINT "Restauration_backup_id_fkey" FOREIGN KEY ("backup_id") REFERENCES "Backup"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "Restauration" ADD CONSTRAINT "Restauration_database_id_fkey" FOREIGN KEY ("database_id") REFERENCES "Database"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20241113182543_2024_11_13/migration.sql b/prisma/migrations/20241113182543_2024_11_13/migration.sql deleted file mode 100644 index 607ee777..00000000 --- a/prisma/migrations/20241113182543_2024_11_13/migration.sql +++ /dev/null @@ -1,13 +0,0 @@ --- CreateEnum -CREATE TYPE "TypeStorage" AS ENUM ('local', 's3'); - --- CreateTable -CREATE TABLE "Settings" ( - "id" TEXT NOT NULL, - "storage" "TypeStorage" NOT NULL DEFAULT 'local', - "s3AccessKeyId" TEXT, - "s3SecretAccessKey" TEXT, - "S3BucketName" TEXT, - - CONSTRAINT "Settings_pkey" PRIMARY KEY ("id") -); diff --git a/prisma/migrations/20241116144000_2024_11_16/migration.sql b/prisma/migrations/20241116144000_2024_11_16/migration.sql deleted file mode 100644 index c2988586..00000000 --- a/prisma/migrations/20241116144000_2024_11_16/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterTable -ALTER TABLE "Settings" ADD COLUMN "s3EndPointUrl" TEXT; diff --git a/prisma/migrations/20241116144522_2024_11_16/migration.sql b/prisma/migrations/20241116144522_2024_11_16/migration.sql deleted file mode 100644 index 6b07c705..00000000 --- a/prisma/migrations/20241116144522_2024_11_16/migration.sql +++ /dev/null @@ -1,6 +0,0 @@ --- AlterTable -ALTER TABLE "Settings" ADD COLUMN "smtpFrom" TEXT, -ADD COLUMN "smtpHost" TEXT, -ADD COLUMN "smtpPassword" TEXT, -ADD COLUMN "smtpPort" TEXT, -ADD COLUMN "smtpUser" TEXT; diff --git a/prisma/migrations/20241116201945_2024_11_16/migration.sql b/prisma/migrations/20241116201945_2024_11_16/migration.sql deleted file mode 100644 index a0c8b4fa..00000000 --- a/prisma/migrations/20241116201945_2024_11_16/migration.sql +++ /dev/null @@ -1,8 +0,0 @@ -/* - Warnings: - - - Added the required column `name` to the `Settings` table without a default value. This is not possible if the table is not empty. - -*/ --- AlterTable -ALTER TABLE "Settings" ADD COLUMN "name" TEXT NOT NULL; diff --git a/prisma/migrations/20241116203552_2024_11_16/migration.sql b/prisma/migrations/20241116203552_2024_11_16/migration.sql deleted file mode 100644 index 89d42b84..00000000 --- a/prisma/migrations/20241116203552_2024_11_16/migration.sql +++ /dev/null @@ -1,8 +0,0 @@ -/* - Warnings: - - - A unique constraint covering the columns `[name]` on the table `Settings` will be added. If there are existing duplicate values, this will fail. - -*/ --- CreateIndex -CREATE UNIQUE INDEX "Settings_name_key" ON "Settings"("name"); diff --git a/prisma/migrations/20241117143016_2024_11_17/migration.sql b/prisma/migrations/20241117143016_2024_11_17/migration.sql deleted file mode 100644 index ba810c83..00000000 --- a/prisma/migrations/20241117143016_2024_11_17/migration.sql +++ /dev/null @@ -1,118 +0,0 @@ -/* - Warnings: - - - You are about to drop the `Agent` table. If the table is not empty, all the data it contains will be lost. - - You are about to drop the `Backup` table. If the table is not empty, all the data it contains will be lost. - - You are about to drop the `Database` table. If the table is not empty, all the data it contains will be lost. - - You are about to drop the `Restauration` table. If the table is not empty, all the data it contains will be lost. - - You are about to drop the `Settings` table. If the table is not empty, all the data it contains will be lost. - -*/ --- DropForeignKey -ALTER TABLE "Backup" DROP CONSTRAINT "Backup_database_id_fkey"; - --- DropForeignKey -ALTER TABLE "Database" DROP CONSTRAINT "Database_agent_id_fkey"; - --- DropForeignKey -ALTER TABLE "Restauration" DROP CONSTRAINT "Restauration_backup_id_fkey"; - --- DropForeignKey -ALTER TABLE "Restauration" DROP CONSTRAINT "Restauration_database_id_fkey"; - --- DropTable -DROP TABLE "Agent"; - --- DropTable -DROP TABLE "Backup"; - --- DropTable -DROP TABLE "Database"; - --- DropTable -DROP TABLE "Restauration"; - --- DropTable -DROP TABLE "Settings"; - --- CreateTable -CREATE TABLE "agents" ( - "id" TEXT NOT NULL, - "slug" TEXT NOT NULL, - "name" TEXT NOT NULL, - "description" TEXT, - "last_contact" TIMESTAMP(3), - "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT "agents_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "databases" ( - "id" TEXT NOT NULL, - "name" TEXT NOT NULL, - "description" TEXT, - "backup_policy" TEXT, - "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "agent_id" TEXT NOT NULL, - - CONSTRAINT "databases_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "backups" ( - "id" TEXT NOT NULL, - "status" "Status" NOT NULL DEFAULT 'waiting', - "file" TEXT, - "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "database_id" TEXT NOT NULL, - - CONSTRAINT "backups_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "restaurations" ( - "id" TEXT NOT NULL, - "status" "Status" NOT NULL DEFAULT 'waiting', - "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "backup_id" TEXT NOT NULL, - "database_id" TEXT, - - CONSTRAINT "restaurations_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "settings" ( - "id" TEXT NOT NULL, - "storage" "TypeStorage" NOT NULL DEFAULT 'local', - "name" TEXT NOT NULL, - "s3EndPointUrl" TEXT, - "s3AccessKeyId" TEXT, - "s3SecretAccessKey" TEXT, - "S3BucketName" TEXT, - "smtpPassword" TEXT, - "smtpFrom" TEXT, - "smtpHost" TEXT, - "smtpPort" TEXT, - "smtpUser" TEXT, - - CONSTRAINT "settings_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE UNIQUE INDEX "agents_slug_key" ON "agents"("slug"); - --- CreateIndex -CREATE UNIQUE INDEX "settings_name_key" ON "settings"("name"); - --- AddForeignKey -ALTER TABLE "databases" ADD CONSTRAINT "databases_agent_id_fkey" FOREIGN KEY ("agent_id") REFERENCES "agents"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "backups" ADD CONSTRAINT "backups_database_id_fkey" FOREIGN KEY ("database_id") REFERENCES "databases"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "restaurations" ADD CONSTRAINT "restaurations_backup_id_fkey" FOREIGN KEY ("backup_id") REFERENCES "backups"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "restaurations" ADD CONSTRAINT "restaurations_database_id_fkey" FOREIGN KEY ("database_id") REFERENCES "databases"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20241117174438_2024_11_17/migration.sql b/prisma/migrations/20241117174438_2024_11_17/migration.sql deleted file mode 100644 index 682bbc22..00000000 --- a/prisma/migrations/20241117174438_2024_11_17/migration.sql +++ /dev/null @@ -1,11 +0,0 @@ -/* - Warnings: - - - Added the required column `dbms` to the `databases` table without a default value. This is not possible if the table is not empty. - -*/ --- CreateEnum -CREATE TYPE "Dbms" AS ENUM ('postgresql', 'mysql', 'mongodb'); - --- AlterTable -ALTER TABLE "databases" ADD COLUMN "dbms" "Dbms" NOT NULL; diff --git a/prisma/migrations/20241117184204_2024_11_17/migration.sql b/prisma/migrations/20241117184204_2024_11_17/migration.sql deleted file mode 100644 index 403bea2d..00000000 --- a/prisma/migrations/20241117184204_2024_11_17/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterTable -ALTER TABLE "users" ADD COLUMN "deleted" BOOLEAN; diff --git a/prisma/migrations/20241117190445_2024_11_17/migration.sql b/prisma/migrations/20241117190445_2024_11_17/migration.sql deleted file mode 100644 index d7bf72f3..00000000 --- a/prisma/migrations/20241117190445_2024_11_17/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterTable -ALTER TABLE "users" ALTER COLUMN "deleted" SET DEFAULT false; diff --git a/prisma/migrations/20241123102722_2024_11_23/migration.sql b/prisma/migrations/20241123102722_2024_11_23/migration.sql deleted file mode 100644 index 7daa2d85..00000000 --- a/prisma/migrations/20241123102722_2024_11_23/migration.sql +++ /dev/null @@ -1,12 +0,0 @@ -/* - Warnings: - - - Added the required column `role` to the `users` table without a default value. This is not possible if the table is not empty. - -*/ --- CreateEnum -CREATE TYPE "Role" AS ENUM ('pending', 'user', 'admin'); - --- AlterTable -ALTER TABLE "users" DROP COLUMN "role", -ADD COLUMN "role" "Role" NOT NULL; diff --git a/prisma/migrations/20241127175717_2024_11_27/migration.sql b/prisma/migrations/20241127175717_2024_11_27/migration.sql deleted file mode 100644 index f6a2bb2e..00000000 --- a/prisma/migrations/20241127175717_2024_11_27/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterTable -ALTER TABLE "databases" ADD COLUMN "last_contact" TIMESTAMP(3); diff --git a/prisma/migrations/20241127182048_2024_11_27/migration.sql b/prisma/migrations/20241127182048_2024_11_27/migration.sql deleted file mode 100644 index c8e7c139..00000000 --- a/prisma/migrations/20241127182048_2024_11_27/migration.sql +++ /dev/null @@ -1,8 +0,0 @@ -/* - Warnings: - - - Added the required column `generatedId` to the `databases` table without a default value. This is not possible if the table is not empty. - -*/ --- AlterTable -ALTER TABLE "databases" ADD COLUMN "generatedId" TEXT NOT NULL; diff --git a/prisma/migrations/20241129115417_2024_11_29/migration.sql b/prisma/migrations/20241129115417_2024_11_29/migration.sql deleted file mode 100644 index 8532e13a..00000000 --- a/prisma/migrations/20241129115417_2024_11_29/migration.sql +++ /dev/null @@ -1,8 +0,0 @@ -/* - Warnings: - - - A unique constraint covering the columns `[generatedId]` on the table `databases` will be added. If there are existing duplicate values, this will fail. - -*/ --- CreateIndex -CREATE UNIQUE INDEX "databases_generatedId_key" ON "databases"("generatedId"); diff --git a/prisma/migrations/20241129165221_2024_11_29/migration.sql b/prisma/migrations/20241129165221_2024_11_29/migration.sql deleted file mode 100644 index 3b9baaaa..00000000 --- a/prisma/migrations/20241129165221_2024_11_29/migration.sql +++ /dev/null @@ -1,9 +0,0 @@ -/* - Warnings: - - - Added the required column `backupToRestore` to the `databases` table without a default value. This is not possible if the table is not empty. - -*/ --- AlterTable -ALTER TABLE "databases" ADD COLUMN "backupToRestore" TEXT NOT NULL, -ADD COLUMN "isWaitingForBackup" BOOLEAN NOT NULL DEFAULT false; diff --git a/prisma/migrations/20241129173131_2024_11_29/migration.sql b/prisma/migrations/20241129173131_2024_11_29/migration.sql deleted file mode 100644 index 83eb3fae..00000000 --- a/prisma/migrations/20241129173131_2024_11_29/migration.sql +++ /dev/null @@ -1,3 +0,0 @@ --- AlterTable -ALTER TABLE "databases" ALTER COLUMN "backupToRestore" DROP NOT NULL, -ALTER COLUMN "isWaitingForBackup" DROP NOT NULL; diff --git a/prisma/migrations/20241130153218_2024_11_30/migration.sql b/prisma/migrations/20241130153218_2024_11_30/migration.sql deleted file mode 100644 index 506bda3d..00000000 --- a/prisma/migrations/20241130153218_2024_11_30/migration.sql +++ /dev/null @@ -1,35 +0,0 @@ --- AlterTable -ALTER TABLE "databases" ADD COLUMN "project_id" TEXT; - --- CreateTable -CREATE TABLE "organizations" ( - "id" TEXT NOT NULL, - "slug" TEXT NOT NULL, - "name" TEXT NOT NULL, - "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - - CONSTRAINT "organizations_pkey" PRIMARY KEY ("id") -); - --- CreateTable -CREATE TABLE "projects" ( - "id" TEXT NOT NULL, - "slug" TEXT NOT NULL, - "name" TEXT NOT NULL, - "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "organization_id" TEXT NOT NULL, - - CONSTRAINT "projects_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE UNIQUE INDEX "organizations_slug_key" ON "organizations"("slug"); - --- CreateIndex -CREATE UNIQUE INDEX "projects_slug_key" ON "projects"("slug"); - --- AddForeignKey -ALTER TABLE "projects" ADD CONSTRAINT "projects_organization_id_fkey" FOREIGN KEY ("organization_id") REFERENCES "organizations"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "databases" ADD CONSTRAINT "databases_project_id_fkey" FOREIGN KEY ("project_id") REFERENCES "projects"("id") ON DELETE SET NULL ON UPDATE CASCADE; diff --git a/prisma/migrations/20241212103214_2024_12_12/migration.sql b/prisma/migrations/20241212103214_2024_12_12/migration.sql deleted file mode 100644 index ddea0021..00000000 --- a/prisma/migrations/20241212103214_2024_12_12/migration.sql +++ /dev/null @@ -1,17 +0,0 @@ --- CreateTable -CREATE TABLE "_OrganizationToUser" ( - "A" TEXT NOT NULL, - "B" TEXT NOT NULL -); - --- CreateIndex -CREATE UNIQUE INDEX "_OrganizationToUser_AB_unique" ON "_OrganizationToUser"("A", "B"); - --- CreateIndex -CREATE INDEX "_OrganizationToUser_B_index" ON "_OrganizationToUser"("B"); - --- AddForeignKey -ALTER TABLE "_OrganizationToUser" ADD CONSTRAINT "_OrganizationToUser_A_fkey" FOREIGN KEY ("A") REFERENCES "organizations"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "_OrganizationToUser" ADD CONSTRAINT "_OrganizationToUser_B_fkey" FOREIGN KEY ("B") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20241212103831_2024_12_12/migration.sql b/prisma/migrations/20241212103831_2024_12_12/migration.sql deleted file mode 100644 index d0912c61..00000000 --- a/prisma/migrations/20241212103831_2024_12_12/migration.sql +++ /dev/null @@ -1,32 +0,0 @@ -/* - Warnings: - - - You are about to drop the `_OrganizationToUser` table. If the table is not empty, all the data it contains will be lost. - -*/ --- DropForeignKey -ALTER TABLE "_OrganizationToUser" DROP CONSTRAINT "_OrganizationToUser_A_fkey"; - --- DropForeignKey -ALTER TABLE "_OrganizationToUser" DROP CONSTRAINT "_OrganizationToUser_B_fkey"; - --- DropTable -DROP TABLE "_OrganizationToUser"; - --- CreateTable -CREATE TABLE "UserOrganization" ( - "id" TEXT NOT NULL, - "userId" TEXT NOT NULL, - "organizationId" TEXT NOT NULL, - - CONSTRAINT "UserOrganization_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE UNIQUE INDEX "UserOrganization_userId_organizationId_key" ON "UserOrganization"("userId", "organizationId"); - --- AddForeignKey -ALTER TABLE "UserOrganization" ADD CONSTRAINT "UserOrganization_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "UserOrganization" ADD CONSTRAINT "UserOrganization_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20241212110141_2024_12_12/migration.sql b/prisma/migrations/20241212110141_2024_12_12/migration.sql deleted file mode 100644 index 4521b67f..00000000 --- a/prisma/migrations/20241212110141_2024_12_12/migration.sql +++ /dev/null @@ -1,32 +0,0 @@ -/* - Warnings: - - - You are about to drop the `UserOrganization` table. If the table is not empty, all the data it contains will be lost. - -*/ --- DropForeignKey -ALTER TABLE "UserOrganization" DROP CONSTRAINT "UserOrganization_organizationId_fkey"; - --- DropForeignKey -ALTER TABLE "UserOrganization" DROP CONSTRAINT "UserOrganization_userId_fkey"; - --- DropTable -DROP TABLE "UserOrganization"; - --- CreateTable -CREATE TABLE "users_organisations" ( - "id" TEXT NOT NULL, - "userId" TEXT NOT NULL, - "organizationId" TEXT NOT NULL, - - CONSTRAINT "users_organisations_pkey" PRIMARY KEY ("id") -); - --- CreateIndex -CREATE UNIQUE INDEX "users_organisations_userId_organizationId_key" ON "users_organisations"("userId", "organizationId"); - --- AddForeignKey -ALTER TABLE "users_organisations" ADD CONSTRAINT "users_organisations_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "users_organisations" ADD CONSTRAINT "users_organisations_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "organizations"("id") ON DELETE RESTRICT ON UPDATE CASCADE; diff --git a/prisma/migrations/20241214165019_2024_12_14/migration.sql b/prisma/migrations/20241214165019_2024_12_14/migration.sql deleted file mode 100644 index 4399bb51..00000000 --- a/prisma/migrations/20241214165019_2024_12_14/migration.sql +++ /dev/null @@ -1,31 +0,0 @@ -/* - Warnings: - - - You are about to drop the `restaurations` table. If the table is not empty, all the data it contains will be lost. - -*/ --- DropForeignKey -ALTER TABLE "restaurations" DROP CONSTRAINT "restaurations_backup_id_fkey"; - --- DropForeignKey -ALTER TABLE "restaurations" DROP CONSTRAINT "restaurations_database_id_fkey"; - --- DropTable -DROP TABLE "restaurations"; - --- CreateTable -CREATE TABLE "restorations" ( - "id" TEXT NOT NULL, - "status" "Status" NOT NULL DEFAULT 'waiting', - "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, - "backup_id" TEXT NOT NULL, - "database_id" TEXT, - - CONSTRAINT "restorations_pkey" PRIMARY KEY ("id") -); - --- AddForeignKey -ALTER TABLE "restorations" ADD CONSTRAINT "restorations_backup_id_fkey" FOREIGN KEY ("backup_id") REFERENCES "backups"("id") ON DELETE CASCADE ON UPDATE CASCADE; - --- AddForeignKey -ALTER TABLE "restorations" ADD CONSTRAINT "restorations_database_id_fkey" FOREIGN KEY ("database_id") REFERENCES "databases"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/prisma/migrations/20241217174747_2024_12_17/migration.sql b/prisma/migrations/20241217174747_2024_12_17/migration.sql deleted file mode 100644 index fe0155cb..00000000 --- a/prisma/migrations/20241217174747_2024_12_17/migration.sql +++ /dev/null @@ -1,45 +0,0 @@ -/* - Warnings: - - - The required column `id` was added to the `verificationtokens` table with a prisma-level default value. This is not possible if the table is not empty. Please add this column as optional, then populate it before making it required. - -*/ --- AlterTable -ALTER TABLE "accounts" ADD COLUMN "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, -ADD COLUMN "updated_at" TIMESTAMP(3); - --- AlterTable -ALTER TABLE "agents" ADD COLUMN "updated_at" TIMESTAMP(3); - --- AlterTable -ALTER TABLE "backups" ADD COLUMN "updated_at" TIMESTAMP(3); - --- AlterTable -ALTER TABLE "databases" ADD COLUMN "updated_at" TIMESTAMP(3); - --- AlterTable -ALTER TABLE "organizations" ADD COLUMN "updated_at" TIMESTAMP(3); - --- AlterTable -ALTER TABLE "projects" ADD COLUMN "updated_at" TIMESTAMP(3); - --- AlterTable -ALTER TABLE "restorations" ADD COLUMN "updated_at" TIMESTAMP(3); - --- AlterTable -ALTER TABLE "sessions" ADD COLUMN "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, -ADD COLUMN "updated_at" TIMESTAMP(3); - --- AlterTable -ALTER TABLE "settings" ADD COLUMN "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, -ADD COLUMN "updated_at" TIMESTAMP(3); - --- AlterTable -ALTER TABLE "users_organisations" ADD COLUMN "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, -ADD COLUMN "updated_at" TIMESTAMP(3); - --- AlterTable -ALTER TABLE "verificationtokens" ADD COLUMN "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, -ADD COLUMN "id" TEXT NOT NULL, -ADD COLUMN "updated_at" TIMESTAMP(3), -ADD CONSTRAINT "verificationtokens_pkey" PRIMARY KEY ("id"); diff --git a/prisma/migrations/20241217183424_2024_12_17/migration.sql b/prisma/migrations/20241217183424_2024_12_17/migration.sql deleted file mode 100644 index a2dd6863..00000000 --- a/prisma/migrations/20241217183424_2024_12_17/migration.sql +++ /dev/null @@ -1,8 +0,0 @@ -/* - Warnings: - - - Added the required column `isArchived` to the `projects` table without a default value. This is not possible if the table is not empty. - -*/ --- AlterTable -ALTER TABLE "projects" ADD COLUMN "isArchived" BOOLEAN NOT NULL; diff --git a/prisma/migrations/20241217183746_2024_12_17/migration.sql b/prisma/migrations/20241217183746_2024_12_17/migration.sql deleted file mode 100644 index 87dae96b..00000000 --- a/prisma/migrations/20241217183746_2024_12_17/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterTable -ALTER TABLE "projects" ALTER COLUMN "isArchived" SET DEFAULT false; diff --git a/prisma/migrations/20241217192704_2024_12_17/migration.sql b/prisma/migrations/20241217192704_2024_12_17/migration.sql deleted file mode 100644 index 2c3da29f..00000000 --- a/prisma/migrations/20241217192704_2024_12_17/migration.sql +++ /dev/null @@ -1,9 +0,0 @@ -/* - Warnings: - - - You are about to drop the column `isArchived` on the `projects` table. All the data in the column will be lost. - -*/ --- AlterTable -ALTER TABLE "projects" DROP COLUMN "isArchived", -ADD COLUMN "is_archived" BOOLEAN NOT NULL DEFAULT false; diff --git a/prisma/migrations/20241229180505_2024_12_29/migration.sql b/prisma/migrations/20241229180505_2024_12_29/migration.sql deleted file mode 100644 index 3a6c00d2..00000000 --- a/prisma/migrations/20241229180505_2024_12_29/migration.sql +++ /dev/null @@ -1,2 +0,0 @@ --- AlterTable -ALTER TABLE "organizations" ADD COLUMN "deleted" BOOLEAN DEFAULT false; diff --git a/prisma/migrations/20241231084839_2024_12_31/migration.sql b/prisma/migrations/20241231084839_2024_12_31/migration.sql deleted file mode 100644 index 3b28c0f6..00000000 --- a/prisma/migrations/20241231084839_2024_12_31/migration.sql +++ /dev/null @@ -1,11 +0,0 @@ -/* - Warnings: - - - Added the required column `role` to the `users_organisations` table without a default value. This is not possible if the table is not empty. - -*/ --- CreateEnum -CREATE TYPE "OrganizationRole" AS ENUM ('member', 'admin'); - --- AlterTable -ALTER TABLE "users_organisations" ADD COLUMN "role" "OrganizationRole" NOT NULL; diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml deleted file mode 100644 index fbffa92c..00000000 --- a/prisma/migrations/migration_lock.toml +++ /dev/null @@ -1,3 +0,0 @@ -# Please do not edit this file manually -# It should be added in your version-control system (i.e. Git) -provider = "postgresql" \ No newline at end of file diff --git a/prisma/schema.prisma b/prisma/schema.prisma deleted file mode 100644 index 0d41ce8b..00000000 --- a/prisma/schema.prisma +++ /dev/null @@ -1,228 +0,0 @@ -////////////////////////////////////////////////////////////////////////////////////////////// -// DO NOT MODIFY THIS FILE // -// This file is automatically generated by ZenStack CLI and should not be manually updated. // -////////////////////////////////////////////////////////////////////////////////////////////// - -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -generator client { - provider = "prisma-client-js" - binaryTargets = ["native", "linux-musl-openssl-3.0.x", "debian-openssl-3.0.x"] -} - -enum Role { - pending - user - admin -} - -enum OrganizationRole { - member - admin -} - -enum Dbms { - postgresql - mysql - mongodb -} - -enum Status { - waiting - ongoing - failed - success -} - -enum TypeStorage { - local - s3 -} - -model Account { - id String @id() @default(cuid()) - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime? @updatedAt() @map("updated_at") - userId String @map("user_id") - type String - provider String - providerAccountId String @map("provider_account_id") - refresh_token String? @db.Text() - access_token String? @db.Text() - expires_at Int? - token_type String? - scope String? - id_token String? @db.Text() - session_state String? - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - - @@unique([provider, providerAccountId]) - @@map("accounts") -} - -model Session { - id String @id() @default(cuid()) - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime? @updatedAt() @map("updated_at") - sessionToken String @unique() @map("session_token") - userId String @map("user_id") - expires DateTime - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - - @@map("sessions") -} - -model VerificationToken { - id String @id() @default(cuid()) - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime? @updatedAt() @map("updated_at") - identifier String - token String - expires DateTime - - @@unique([identifier, token]) - @@map("verificationtokens") -} - -model User { - id String @id() @default(cuid()) - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime? @updatedAt() @map("updated_at") - name String? - email String? @unique() - emailVerified DateTime? @map("email_verified") - image String? - role Role - password String? - authMethod String? @map("auth_method") - deleted Boolean? @default(false) - accounts Account[] - sessions Session[] - organizations UserOrganization[] - - @@map("users") -} - -model Organization { - id String @id() @default(cuid()) - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime? @updatedAt() @map("updated_at") - slug String @unique() - name String - projects Project[] - users UserOrganization[] - deleted Boolean? @default(false) - - @@map("organizations") -} - -model UserOrganization { - id String @id() @default(cuid()) - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime? @updatedAt() @map("updated_at") - userId String - organizationId String - user User @relation(fields: [userId], references: [id]) - organization Organization @relation(fields: [organizationId], references: [id]) - role OrganizationRole - - @@unique([userId, organizationId]) - @@map("users_organisations") -} - -model Project { - id String @id() @default(cuid()) - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime? @updatedAt() @map("updated_at") - slug String @unique() - name String - isArchived Boolean @default(false) @map("is_archived") - organizationId String @map("organization_id") - organization Organization @relation(fields: [organizationId], references: [id]) - databases Database[] - - @@map("projects") -} - -model Agent { - id String @id() @default(cuid()) - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime? @updatedAt() @map("updated_at") - slug String @unique() - name String - description String? - lastContact DateTime? @map("last_contact") - databases Database[] - - @@map("agents") -} - -model Database { - id String @id() @default(cuid()) - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime? @updatedAt() @map("updated_at") - name String - dbms Dbms - generatedId String @unique() - description String? - backupPolicy String? @map("backup_policy") - isWaitingForBackup Boolean? @default(false) - backupToRestore String? - agentId String @map("agent_id") - agent Agent @relation(fields: [agentId], references: [id], onDelete: Cascade) - lastContact DateTime? @map("last_contact") - backups Backup[] - restorations Restoration[] - projectId String? @map("project_id") - project Project? @relation(fields: [projectId], references: [id]) - - @@map("databases") -} - -model Backup { - id String @id() @default(cuid()) - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime? @updatedAt() @map("updated_at") - status Status @default(waiting) - file String? - databaseId String @map("database_id") - database Database @relation(fields: [databaseId], references: [id], onDelete: Cascade) - restorations Restoration[] - - @@map("backups") -} - -model Restoration { - id String @id() @default(cuid()) - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime? @updatedAt() @map("updated_at") - status Status @default(waiting) - backupId String @map("backup_id") - backup Backup @relation(fields: [backupId], references: [id], onDelete: Cascade) - databaseId String? @map("database_id") - database Database? @relation(fields: [databaseId], references: [id], onDelete: Cascade) - - @@map("restorations") -} - -model Settings { - id String @id() @default(cuid()) - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime? @updatedAt() @map("updated_at") - storage TypeStorage @default(local) - name String @unique() - s3EndPointUrl String? - s3AccessKeyId String? - s3SecretAccessKey String? - S3BucketName String? - smtpPassword String? - smtpFrom String? - smtpHost String? - smtpPort String? - smtpUser String? - - @@map("settings") -} diff --git a/public/google-icon.png b/public/google-icon.png deleted file mode 100644 index 1d4d40d6c7c891ecb0f4e87d1f57071cc49f57bf..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 16277 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 AK>z>% diff --git a/schema.zmodel b/schema.zmodel deleted file mode 100644 index c950de41..00000000 --- a/schema.zmodel +++ /dev/null @@ -1,240 +0,0 @@ -// This is your Prisma schema file, -// learn more about it in the docs: https://pris.ly/d/prisma-schema - -// Looking for ways to speed up your queries, or scale easily with your serverless or edge functions? -// Try Prisma Accelerate: https://pris.ly/cli/accelerate-init - -// https://zenstack.dev/docs/guides/check-permission - -generator client { - provider = "prisma-client-js" - binaryTargets = ["native", "linux-musl-openssl-3.0.x", "debian-openssl-3.0.x"] -} - -datasource db { - provider = "postgresql" - url = env("DATABASE_URL") -} - -plugin openapi { - provider = "@zenstackhq/openapi" - output = "./src/openapi.yaml" -} - -plugin tanstack_query { - provider = "@zenstackhq/tanstack-query" - output = "./src/lib/hooks" - target = "react" -} - - -plugin enhancer { - provider = '@core/enhancer' - generatePermissionChecker = true -} - -abstract model Base { - id String @id @default(cuid()) - createdAt DateTime @default(now()) @map("created_at") - updatedAt DateTime? @updatedAt @map("updated_at") -} - -model Account extends Base { - userId String @map("user_id") - type String - provider String - providerAccountId String @map("provider_account_id") - refresh_token String? @db.Text - access_token String? @db.Text - expires_at Int? - token_type String? - scope String? - id_token String? @db.Text - session_state String? - - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - - @@unique([provider, providerAccountId]) - @@map("accounts") -} - -model Session extends Base { - sessionToken String @unique @map("session_token") - userId String @map("user_id") - expires DateTime - user User @relation(fields: [userId], references: [id], onDelete: Cascade) - - @@map("sessions") -} - -model VerificationToken extends Base { - identifier String - token String - expires DateTime - - @@unique([identifier, token]) - @@map("verificationtokens") -} - -model User extends Base { - name String? - email String? @unique @email - emailVerified DateTime? @map("email_verified") - image String? - role Role - password String? @omit @password - authMethod String? @map("auth_method") - deleted Boolean? @default(false) - accounts Account[] - sessions Session[] - organizations UserOrganization[] - - @@map("users") -} - -enum Role { - pending - user - admin -} - -model Organization extends Base { - slug String @unique - name String - - projects Project[] - users UserOrganization[] - deleted Boolean? @default(false) - - // Allow only users who are part of the organization - @@allow('all', users?[user == auth()]) - - @@map("organizations") -} - -enum OrganizationRole { - member - admin -} - - -model UserOrganization extends Base { - userId String - organizationId String - user User @relation(fields: [userId], references: [id]) - organization Organization @relation(fields: [organizationId], references: [id]) - role OrganizationRole - - @@unique([userId, organizationId]) - @@map("users_organisations") -} - -model Project extends Base { - slug String @unique - name String - isArchived Boolean @map("is_archived") @default(false) - - organizationId String @map("organization_id") - organization Organization @relation(fields: [organizationId], references: [id]) - - databases Database[] - - @@map("projects") -} - -model Agent extends Base { - slug String @unique - name String - description String? - lastContact DateTime? @map("last_contact") - - databases Database[] - - // Restrict access to users with role "admin" - @@allow('all', auth().role == "admin") - - @@map("agents") -} - -enum Dbms { - postgresql - mysql - mongodb -} - -model Database extends Base { - name String - dbms Dbms - generatedId String @unique - description String? - backupPolicy String? @map("backup_policy") - isWaitingForBackup Boolean? @default(false) - backupToRestore String? - - agentId String @map("agent_id") - agent Agent @relation(fields: [agentId], references: [id], onDelete: Cascade) - lastContact DateTime? @map("last_contact") - - backups Backup[] - restorations Restoration[] - - projectId String? @map("project_id") - project Project? @relation(fields: [projectId], references: [id]) - - @@map("databases") -} - -enum Status { - waiting - ongoing - failed - success -} - -model Backup extends Base { - status Status @default(waiting) - file String? - - databaseId String @map("database_id") - database Database @relation(fields: [databaseId], references: [id], onDelete: Cascade) - - restorations Restoration[] - - @@map("backups") -} - -model Restoration extends Base { - status Status @default(waiting) - - backupId String @map("backup_id") - backup Backup @relation(fields: [backupId], references: [id], onDelete: Cascade) - - databaseId String? @map("database_id") - database Database? @relation(fields: [databaseId], references: [id], onDelete: Cascade) - - @@map("restorations") -} - -enum TypeStorage { - local - s3 -} - -model Settings extends Base { - storage TypeStorage @default(local) - name String @unique - s3EndPointUrl String? - s3AccessKeyId String? - s3SecretAccessKey String? - S3BucketName String? - smtpPassword String? - smtpFrom String? - smtpHost String? - smtpPort String? - smtpUser String? - - // Restrict access to users with role "admin" - @@allow('all', auth().role == "admin") - - @@map("settings") -} diff --git a/src/auth/auth.ts b/src/auth/auth.ts deleted file mode 100644 index 8fbe1b43..00000000 --- a/src/auth/auth.ts +++ /dev/null @@ -1,162 +0,0 @@ -import NextAuth, {CredentialsSignin} from "next-auth"; -import {prisma} from "@/prisma"; -import {PrismaAdapter} from "@auth/prisma-adapter"; -import Credentials from "next-auth/providers/credentials"; -import {env} from "@/env.mjs"; -import GoogleProvider from "next-auth/providers/google"; - - -export const {handlers, auth: baseAuth, signIn, signOut} = NextAuth({ - adapter: PrismaAdapter(prisma), - theme: { - logo: "/logo.png" - }, - secret: env.NEXT_PUBLIC_SECRET, - debug: process.env.NODE_ENV !== "production", - providers: [ - Credentials({ - name: "Credentials", - credentials: { - email: {label: "Email", type: "email"}, - password: {label: "Password", type: "password"}, - }, - authorize: async (credentials) => { - let user = null - const argon2 = require('argon2'); - console.log(credentials); - user = await prisma.user.findFirst({ - where: { - email: credentials.email, - } - }) - if (!user || user.role === "pending") { - return null - } - const isValid = await argon2.verify(user.password, credentials.password) - if (!isValid) { - return null - } - return user; - }, - }), - GoogleProvider({ - clientId: env.AUTH_GOOGLE_ID, - clientSecret: env.AUTH_GOOGLE_SECRET, - allowDangerousEmailAccountLinking: true, - async profile(profile) { - - const users = await prisma.user.findMany({ - where: { - deleted: {not: true}, - } - }) - console.log(users.length) - const role = users.length > 0 ? "pending" : "admin" - - - return { - role: role, - name: profile.name, - email: profile.email, - image: profile.picture, - email_verified: profile.email_verified, - authMethod: "google", - } - }, - }) - ], - session: { - strategy: "jwt", - }, - pages: { - signIn: "/login", - error: "/error" - }, - callbacks: { - // session({session, user, token}) { - // // session.user.role = user.role - // // session.user.authMethod = user.authMethod; - // return session - // }, - async jwt({token, trigger, session, user}) { - if (trigger === "update" && session) { - return {...token, ...session?.user}; - } - - return {...token, ...user}; - }, - async session({session, token, user}) { - session.user = token; - return session; - }, - async signIn({account, user, profile}) { - const existingUser = await prisma.user.findFirst({ - where: {email: user.email}, - }); - - if (!existingUser) { - // Create a new user with a pending role - - - const users = await prisma.user.findMany({ - where: { - deleted: {not: true}, - } - }) - const role = users.length > 0 ? "pending" : "admin" - - const newUser = await prisma.user.create({ - data: { - email: user.email, - name: user.name, - image: user.image, - role: role, - authMethod: account.provider, - }, - }); - - const defaultOrganization = await prisma.organization.findUnique({ - where: {slug: "default"}, - include: {users: { - include:{ - user: true - }, - where:{ - user: { - deleted: {not: true}, - } - } - }} - }); - - const organizationRole = defaultOrganization.users.length > 0 ? "member" : "admin" - - await prisma.userOrganization.create({ - data: { - userId: newUser.id, - organizationId: defaultOrganization.id, - role: organizationRole - }, - }); - - return role !== "pending"; - - } - - if (existingUser.role === "pending") { - return false; // Prevent login if role is pending - } - - // Update auth method if user exists - await prisma.user.update({ - where: {id: existingUser.id}, - data: { - authMethod: account.provider, - }, - }); - - return true; // Allow login - }, - } - -}); \ No newline at end of file diff --git a/src/auth/current-user.ts b/src/auth/current-user.ts deleted file mode 100644 index 227de289..00000000 --- a/src/auth/current-user.ts +++ /dev/null @@ -1,29 +0,0 @@ -"use server" -import {baseAuth} from "@/auth/auth"; -import {User} from "@prisma/client"; -import {prisma} from "@/prisma"; - -export const currentUser = async () => { - const session = await baseAuth(); - if (!session?.user) { - return null; - } - return session.user as User; -} - - -export const requiredCurrentUser = async () => { - const user = await currentUser(); - if (!user) { - return null - } - - const userDb = await prisma.user.findFirst({ - where: {id: user.id}, - }) - if(!userDb){ - return null - } - - return user; -} diff --git a/src/components/layout.tsx b/src/components/layout.tsx index 7305d39d..afb42e29 100644 --- a/src/components/layout.tsx +++ b/src/components/layout.tsx @@ -1,16 +1,9 @@ -import {twx} from "@/lib/twx"; -import {cn} from "@/lib/utils"; +import { twx } from "@/lib/twx"; +import { cn } from "@/lib/utils"; +export const LayoutAdmin = twx.div((props) => [`w-full h-screen flex flex-col gap-4 mx-auto `]); +export const Layout = twx.div((props) => [`max-w-5xl w-full flex-col flex gap-4 mx-auto px-4 `]); -export const LayoutAdmin = twx.div((props) => [ - `w-full h-screen flex flex-col gap-4 mx-auto ` -]); -export const Layout = twx.div((props)=>[ - `max-w-5xl w-full flex-col flex gap-4 mx-auto px-4 `, -]) +export const LayoutTitle = twx.h1((props) => [cn(`text-4xl font-bold mt-5 `, props.className)]); -export const LayoutTitle = twx.h1((props)=>[ - cn(`text-4xl font-bold mt-5 `, props.className) -]) - -export const LayoutDescription = twx.p((props)=>[`text-lg text-muted-foreground`]) +export const LayoutDescription = twx.p((props) => [`text-lg text-muted-foreground`]); diff --git a/src/components/ui/accordion.tsx b/src/components/ui/accordion.tsx index 8dcf9b6f..4a8cca46 100644 --- a/src/components/ui/accordion.tsx +++ b/src/components/ui/accordion.tsx @@ -2,56 +2,65 @@ import * as React from "react" import * as AccordionPrimitive from "@radix-ui/react-accordion" -import { ChevronDownIcon } from "@radix-ui/react-icons" +import { ChevronDownIcon } from "lucide-react" import { cn } from "@/lib/utils" -const Accordion = AccordionPrimitive.Root +function Accordion({ + ...props +}: React.ComponentProps) { + return +} -const AccordionItem = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -AccordionItem.displayName = "AccordionItem" +function AccordionItem({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} -const AccordionTrigger = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - - svg]:rotate-180", - className - )} +function AccordionTrigger({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + + svg]:rotate-180", + className + )} + {...props} + > + {children} + + + + ) +} + +function AccordionContent({ + className, + children, + ...props +}: React.ComponentProps) { + return ( + - {children} - - - -)) -AccordionTrigger.displayName = AccordionPrimitive.Trigger.displayName - -const AccordionContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, children, ...props }, ref) => ( - -
{children}
-
-)) -AccordionContent.displayName = AccordionPrimitive.Content.displayName +
{children}
+ + ) +} export { Accordion, AccordionItem, AccordionTrigger, AccordionContent } diff --git a/src/components/ui/alert-dialog.tsx b/src/components/ui/alert-dialog.tsx index 57760f2e..0863e40d 100644 --- a/src/components/ui/alert-dialog.tsx +++ b/src/components/ui/alert-dialog.tsx @@ -6,125 +6,141 @@ import * as AlertDialogPrimitive from "@radix-ui/react-alert-dialog" import { cn } from "@/lib/utils" import { buttonVariants } from "@/components/ui/button" -const AlertDialog = AlertDialogPrimitive.Root +function AlertDialog({ + ...props +}: React.ComponentProps) { + return +} -const AlertDialogTrigger = AlertDialogPrimitive.Trigger +function AlertDialogTrigger({ + ...props +}: React.ComponentProps) { + return ( + + ) +} -const AlertDialogPortal = AlertDialogPrimitive.Portal +function AlertDialogPortal({ + ...props +}: React.ComponentProps) { + return ( + + ) +} -const AlertDialogOverlay = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -AlertDialogOverlay.displayName = AlertDialogPrimitive.Overlay.displayName - -const AlertDialogContent = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - - - ) { + return ( + - -)) -AlertDialogContent.displayName = AlertDialogPrimitive.Content.displayName + ) +} -const AlertDialogHeader = ({ +function AlertDialogContent({ className, ...props -}: React.HTMLAttributes) => ( -
-) -AlertDialogHeader.displayName = "AlertDialogHeader" +}: React.ComponentProps) { + return ( + + + + + ) +} -const AlertDialogFooter = ({ +function AlertDialogHeader({ className, ...props -}: React.HTMLAttributes) => ( -
-) -AlertDialogFooter.displayName = "AlertDialogFooter" +}: React.ComponentProps<"div">) { + return ( +
+ ) +} -const AlertDialogTitle = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -AlertDialogTitle.displayName = AlertDialogPrimitive.Title.displayName +function AlertDialogFooter({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} -const AlertDialogDescription = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -AlertDialogDescription.displayName = - AlertDialogPrimitive.Description.displayName +function AlertDialogTitle({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} -const AlertDialogAction = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -AlertDialogAction.displayName = AlertDialogPrimitive.Action.displayName +function AlertDialogDescription({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} -const AlertDialogCancel = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -AlertDialogCancel.displayName = AlertDialogPrimitive.Cancel.displayName +function AlertDialogAction({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} + +function AlertDialogCancel({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} export { AlertDialog, diff --git a/src/components/ui/alert.tsx b/src/components/ui/alert.tsx index 5afd41d1..14213546 100644 --- a/src/components/ui/alert.tsx +++ b/src/components/ui/alert.tsx @@ -4,13 +4,13 @@ import { cva, type VariantProps } from "class-variance-authority" import { cn } from "@/lib/utils" const alertVariants = cva( - "relative w-full rounded-lg border px-4 py-3 text-sm [&>svg+div]:translate-y-[-3px] [&>svg]:absolute [&>svg]:left-4 [&>svg]:top-4 [&>svg]:text-foreground [&>svg~*]:pl-7", + "relative w-full rounded-lg border px-4 py-3 text-sm grid has-[>svg]:grid-cols-[calc(var(--spacing)*4)_1fr] grid-cols-[0_1fr] has-[>svg]:gap-x-3 gap-y-0.5 items-start [&>svg]:size-4 [&>svg]:translate-y-0.5 [&>svg]:text-current", { variants: { variant: { - default: "bg-background text-foreground", + default: "bg-card text-card-foreground", destructive: - "border-destructive/50 text-destructive dark:border-destructive [&>svg]:text-destructive", + "text-destructive bg-card [&>svg]:text-current *:data-[slot=alert-description]:text-destructive/90", }, }, defaultVariants: { @@ -19,41 +19,48 @@ const alertVariants = cva( } ) -const Alert = React.forwardRef< - HTMLDivElement, - React.HTMLAttributes & VariantProps ->(({ className, variant, ...props }, ref) => ( -
-)) -Alert.displayName = "Alert" +function Alert({ + className, + variant, + ...props +}: React.ComponentProps<"div"> & VariantProps) { + return ( +
+ ) +} -const AlertTitle = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -AlertTitle.displayName = "AlertTitle" +function AlertTitle({ className, ...props }: React.ComponentProps<"div">) { + return ( +
+ ) +} -const AlertDescription = React.forwardRef< - HTMLParagraphElement, - React.HTMLAttributes ->(({ className, ...props }, ref) => ( -
-)) -AlertDescription.displayName = "AlertDescription" +function AlertDescription({ + className, + ...props +}: React.ComponentProps<"div">) { + return ( +
+ ) +} export { Alert, AlertTitle, AlertDescription } diff --git a/src/components/ui/aspect-ratio.tsx b/src/components/ui/aspect-ratio.tsx index d6a5226f..3df3fd02 100644 --- a/src/components/ui/aspect-ratio.tsx +++ b/src/components/ui/aspect-ratio.tsx @@ -2,6 +2,10 @@ import * as AspectRatioPrimitive from "@radix-ui/react-aspect-ratio" -const AspectRatio = AspectRatioPrimitive.Root +function AspectRatio({ + ...props +}: React.ComponentProps) { + return +} export { AspectRatio } diff --git a/src/components/ui/avatar.tsx b/src/components/ui/avatar.tsx index 51e507ba..71e428b4 100644 --- a/src/components/ui/avatar.tsx +++ b/src/components/ui/avatar.tsx @@ -5,46 +5,49 @@ import * as AvatarPrimitive from "@radix-ui/react-avatar" import { cn } from "@/lib/utils" -const Avatar = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -Avatar.displayName = AvatarPrimitive.Root.displayName +function Avatar({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} -const AvatarImage = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -AvatarImage.displayName = AvatarPrimitive.Image.displayName +function AvatarImage({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} -const AvatarFallback = React.forwardRef< - React.ElementRef, - React.ComponentPropsWithoutRef ->(({ className, ...props }, ref) => ( - -)) -AvatarFallback.displayName = AvatarPrimitive.Fallback.displayName +function AvatarFallback({ + className, + ...props +}: React.ComponentProps) { + return ( + + ) +} export { Avatar, AvatarImage, AvatarFallback } diff --git a/src/components/ui/badge.tsx b/src/components/ui/badge.tsx index e87d62bf..02054139 100644 --- a/src/components/ui/badge.tsx +++ b/src/components/ui/badge.tsx @@ -1,20 +1,22 @@ import * as React from "react" +import { Slot } from "@radix-ui/react-slot" import { cva, type VariantProps } from "class-variance-authority" import { cn } from "@/lib/utils" const badgeVariants = cva( - "inline-flex items-center rounded-md border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2", + "inline-flex items-center justify-center rounded-md border px-2 py-0.5 text-xs font-medium w-fit whitespace-nowrap shrink-0 [&>svg]:size-3 gap-1 [&>svg]:pointer-events-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px] aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive transition-[color,box-shadow] overflow-hidden", { variants: { variant: { default: - "border-transparent bg-primary text-primary-foreground shadow hover:bg-primary/80", + "border-transparent bg-primary text-primary-foreground [a&]:hover:bg-primary/90", secondary: - "border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80", + "border-transparent bg-secondary text-secondary-foreground [a&]:hover:bg-secondary/90", destructive: - "border-transparent bg-destructive text-destructive-foreground shadow hover:bg-destructive/80", - outline: "text-foreground", + "border-transparent bg-destructive text-white [a&]:hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:focus-visible:ring-destructive/40 dark:bg-destructive/60", + outline: + "text-foreground [a&]:hover:bg-accent [a&]:hover:text-accent-foreground", }, }, defaultVariants: { @@ -23,13 +25,21 @@ const badgeVariants = cva( } ) -export interface BadgeProps - extends React.HTMLAttributes, - VariantProps {} +function Badge({ + className, + variant, + asChild = false, + ...props +}: React.ComponentProps<"span"> & + VariantProps & { asChild?: boolean }) { + const Comp = asChild ? Slot : "span" -function Badge({ className, variant, ...props }: BadgeProps) { return ( -
+ ) } diff --git a/src/components/ui/breadcrumb.tsx b/src/components/ui/breadcrumb.tsx index cfdc9ac2..301887db 100644 --- a/src/components/ui/breadcrumb.tsx +++ b/src/components/ui/breadcrumb.tsx @@ -1,108 +1,101 @@ import * as React from "react" -import { ChevronRightIcon, DotsHorizontalIcon } from "@radix-ui/react-icons" import { Slot } from "@radix-ui/react-slot" - import { cn } from "@/lib/utils" +import { ChevronRightIcon, DotsHorizontalIcon } from "@radix-ui/react-icons" -const Breadcrumb = React.forwardRef< - HTMLElement, - React.ComponentPropsWithoutRef<"nav"> & { - separator?: React.ReactNode - } ->(({ ...props }, ref) =>