Compare commits

..
210 Commits
Author SHA1 Message Date
charlesgauthereau 98e40466a0 [FIX] (agent) - order by createdAt 2026-01-04 21:24:06 +01:00
charlesgauthereau 324d80a735 [FIX] (backup) - change in extension file 2026-01-04 19:40:08 +01:00
charlesgauthereau 6770be63b6 [FIX] - adding fileSize in upload-backup.action.ts 2026-01-03 01:06:13 +01:00
charlesgauthereau 89aa33ebd8 Updating version. 2026-01-02 15:40:20 +01:00
charlesgauthereau 619c32f09d [FEATURE] - Adding fileSize in backup model in order to display it. 2026-01-02 15:32:36 +01:00
Charles GTEandGitHub c471423775 Update README.md 2026-01-02 15:06:24 +01:00
charlesgauthereau c589b9b8b8 Updating version. Ready for 1.1.7-rc.1 2026-01-02 14:24:37 +01:00
charlesgauthereau bae9ba0973 Increase proxyClientMaxBodySize to 10g in order to accept large databases. (#2) 2026-01-02 13:55:53 +01:00
charlesgauthereau 7bfc5c1d9b Increase proxyClientMaxBodySize to 10g in order to accept large databases. 2026-01-02 13:51:57 +01:00
charlesgauthereau 8bd3b0d731 Updating README.md 2026-01-01 19:10:20 +01:00
charlesgauthereau 06cd4de141 Moving to Github folder CONTRIBUTING.md 2026-01-01 19:08:40 +01:00
charlesgauthereau 33eb2df023 Adding CITATION.cff 2026-01-01 19:07:23 +01:00
charlesgauthereau 540ecd4b60 Updating README.md 2026-01-01 18:54:06 +01:00
charlesgauthereau 237418f009 Updating README.md 2026-01-01 18:49:18 +01:00
charlesgauthereau 4447180e9e Refactoring charts in statistics in organization. 2025-12-30 16:26:28 +01:00
charlesgauthereau f111946592 Refactoring charts in statistics in organization. 2025-12-30 16:22:34 +01:00
charlesgauthereau 475e404fae Bug fixes and refactoring user dropdown. 2025-12-30 14:02:49 +01:00
charlesgauthereau 07ce5215d4 Changing Github url in stars component. 2025-12-30 13:46:00 +01:00
charlesgauthereau 9f4895fd63 Refactoring user Page in admin panel. 2025-12-30 13:42:23 +01:00
charlesgauthereau c18a9da0d3 Refactoring user Page in admin panel. 2025-12-30 13:38:01 +01:00
charlesgauthereau 8e3eb32631 Adding support light and dark in email layout, by changing the Portabase logo. 2025-12-30 11:07:45 +01:00
charlesgauthereau f0598b3f59 Adding support for github provider. 2025-12-30 10:42:51 +01:00
charlesgauthereau 011120fdf9 Patching bug on portabase.config.ts about social providers. 2025-12-30 10:27:47 +01:00
charlesgauthereau ff175e1edd Updating security.yml 2025-12-29 15:59:58 +01:00
Soluce TechnologiesandGitHub 3f56008c39 Update README.md, Updating the Gif 2025-12-29 15:51:12 +01:00
charlesgauthereau 5df95bf7c1 Updating the README.md 2025-12-29 15:42:25 +01:00
charlesgauthereau 23d52d0447 Updating the README.md 2025-12-26 18:58:08 +01:00
charlesgauthereau 795a127998 Ready for 1.1.6-rc.1 2025-12-26 16:09:11 +01:00
charlesgauthereau fd73d383f9 Adding uploading backup feature. 2025-12-26 16:08:36 +01:00
charlesgauthereau ee285df7b5 Adding useAutoRefresh on agent page. 2025-12-26 11:55:43 +01:00
charlesgauthereau 7f7f8e642f README.md 2025-12-24 09:26:49 +01:00
charlesgauthereau d9997ff5e9 Changing assets with new DA. 2025-12-23 22:10:00 +01:00
charlesgauthereau 36adcb323a Changing assets with new DA. 2025-12-23 22:06:14 +01:00
charlesgauthereau 5302060f44 Working and testing the profile module. 2025-12-23 21:44:52 +01:00
charlesgauthereau d19a46bee6 Working and testing the profile module. 2025-12-23 18:25:31 +01:00
charlesgauthereau d504866aad Patching hydratation error due to theme. 2025-12-23 09:06:06 +01:00
charlesgauthereau 601dae4909 Working on the profile module. 2025-12-22 20:32:18 +01:00
charlesgauthereau 08f2ad0521 Refactoring on image and component loading. 2025-12-22 14:23:52 +01:00
charlesgauthereau 3b356e33e7 Adding AuthCard for mobile first design and refactoring the Logo loading in the layout. 2025-12-22 12:50:40 +01:00
charlesgauthereau 57d9b8da1e Bug in profile. 2025-12-21 20:17:05 +01:00
charlesgauthereau 86f50e5741 Bug in profile. 2025-12-21 19:41:30 +01:00
charlesgauthereau a6e52fd03d Ready for 1.1.5-rc.4 2025-12-21 19:27:00 +01:00
charlesgauthereau 116229a29e Start working on the profile refactoring. 2025-12-21 16:55:12 +01:00
charlesgauthereau 428782e8a4 Ready for 1.1.5-rc.2. Patching an api bug. 2025-12-21 11:57:07 +01:00
charlesgauthereau b8442803d9 Ready for 1.1.5-rc.1 2025-12-21 11:25:33 +01:00
charlesgauthereau 82ab2c63d6 File refactoring and react errors. 2025-12-21 09:59:49 +01:00
charlesgauthereau e953de0e06 For release 1.1.4 in main 2025-12-09 21:31:46 +01:00
Soluce TechnologiesandGitHub f6d927f866 Merge pull request #1 from Soluce-Technologies/dependabot/npm_and_yarn/next-16.0.7
Bump next from 15.5.2 to 16.0.7
2025-12-09 21:29:58 +01:00
dependabot[bot]andGitHub 3773c4b068 Bump next from 15.5.2 to 16.0.7
Bumps [next](https://github.com/vercel/next.js) from 15.5.2 to 16.0.7.
- [Release notes](https://github.com/vercel/next.js/releases)
- [Changelog](https://github.com/vercel/next.js/blob/canary/release.js)
- [Commits](https://github.com/vercel/next.js/compare/v15.5.2...v16.0.7)

---
updated-dependencies:
- dependency-name: next
  dependency-version: 16.0.7
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-12-09 20:28:45 +00:00
charlesgauthereau bb0dbdadae For release 1.1.4 in main 2025-12-09 21:25:29 +01:00
charlesgauthereau dd8aec50eb For release 1.1.4 in main 2025-12-09 21:23:15 +01:00
charlesgauthereau 7410aaea38 Hiding notifiers if not admin or owner in organization. 2025-12-08 09:57:17 +01:00
charlesgauthereau 0f9bbbda3c Ready for release 1.1.4 2025-12-07 12:25:10 +01:00
charlesgauthereau 25f9bc6e79 Ready for release 1.1.4 2025-12-07 12:11:00 +01:00
charlesgauthereau caf00b7877 Ready for release 1.1.4 2025-12-07 11:48:14 +01:00
charlesgauthereau 9c0c462192 Ready for release 1.1.4 2025-12-07 11:22:07 +01:00
charlesgauthereau ef7570db76 Ready for release 1.1.4 2025-12-07 11:15:43 +01:00
charlesgauthereau e1bc1ec6c8 Ready for release 1.1.4 2025-12-07 10:53:08 +01:00
charlesgauthereau ca232250cc Working on notification system. 2025-12-06 23:18:59 +01:00
charlesgauthereau fde7688485 Working on notification system. 2025-12-06 23:14:59 +01:00
charlesgauthereau 9458b12d69 Refactoring the sidebar. 2025-12-06 15:50:23 +01:00
charlesgauthereau 5cf6cf075e Working on notifications channels. 2025-11-30 12:21:21 +01:00
charlesgauthereau 8980e6d4a8 Working on notifications channels. 2025-11-29 23:12:06 +01:00
charlesgauthereau ac951378e8 Some refactoring in database and projects side. 2025-11-29 22:31:32 +01:00
charlesgauthereau 934e635cdb Working on implementation of alert policies. Some refactoring in user profile and patching a bug in status endpoint api. 2025-11-28 23:10:37 +01:00
charlesgauthereau d968aa9c16 Working on implementation of alert policies. 2025-11-23 17:13:07 +01:00
charlesgauthereau eb471c7cdb Working on UX/UI of the notification system. 2025-11-22 19:36:31 +01:00
charlesgauthereau e886bdd58d Improved the OrganizationCombobox. 2025-11-22 18:45:24 +01:00
charlesgauthereau fe5d8bae1b Adding reset password feature. 2025-11-22 17:20:52 +01:00
charlesgauthereau aabdea06a9 Merge branch 'main' into feature
# Conflicts:
#	app/(auth)/login/page.tsx
2025-11-22 14:18:50 +01:00
Soluce TechnologiesandGitHub 37a3ace161 Update env.mjs 2025-11-17 19:01:21 +01:00
Soluce TechnologiesandGitHub 933ab7d68a Update Login Page 2025-11-17 18:59:33 +01:00
charlesgauthereau f42cb37d67 Bug on auth google. 2025-11-11 10:32:27 +01:00
charlesgauthereau 7dfd9f02e8 Working on notifier system. 2025-11-09 16:47:58 +01:00
charlesgauthereau 411e09f452 Working on the README.md 2025-11-08 21:43:13 +01:00
charlesgauthereau f21149f42e Working on the README.md 2025-11-08 21:04:53 +01:00
charlesgauthereau 099b6fb0e8 Working on the README.md 2025-11-08 21:03:24 +01:00
charlesgauthereau eb2e0ef468 Working on the README.md 2025-11-08 20:07:54 +01:00
charlesgauthereau 4644ed2f41 Working on the README.md 2025-11-08 20:07:49 +01:00
charlesgauthereau cf8e5b7094 Ready for 1.1.3 2025-11-07 22:04:10 +01:00
charlesgauthereau b0775e82ea Updating the README.md 2025-11-04 11:38:22 +01:00
charlesgauthereau f9b3461a44 Ready for 1.1.3-rc.3 release. 2025-11-04 11:37:36 +01:00
charlesgauthereau c4ebc876bf Ready for 1.1.3-rc.3 release. 2025-11-04 09:20:24 +01:00
charlesgauthereau a435a92d41 Ready for 1.1.3-rc.3 release. 2025-11-04 08:48:46 +01:00
charlesgauthereau bcbd6bbea1 Work on passing all env variables on server side. 2025-11-03 18:54:16 +01:00
charlesgauthereau 82559b49bd Ready for 1.1.3-rc.2 and migrating on nextjs 16 2025-11-03 14:02:50 +01:00
charlesgauthereau 8c1b869004 Ready for 1.1.3-rc.2 and migrating on nextjs 16 2025-11-03 13:31:05 +01:00
charlesgauthereau c4a0692985 Ready for 1.1.3-rc.2 and migrating on nextjs 16 2025-11-03 13:20:48 +01:00
charlesgauthereau 750f1cb145 Ready for 1.1.3-rc.2 and migrating on nextjs 16 2025-11-03 12:49:42 +01:00
charlesgauthereau b7d3487c53 Ready for 1.1.3-rc.2 and migrating on nextjs 16 2025-11-03 12:46:58 +01:00
charlesgauthereau 529f157c6e Ready for 1.1.3-rc.2 and migrating on nextjs 16 2025-11-03 12:32:00 +01:00
charlesgauthereau c62be9854a Ready for 1.1.3-rc.2 2025-11-03 11:37:54 +01:00
charlesgauthereau 9c1aa66b41 Ready for 1.1.3-rc.2 2025-11-03 11:37:14 +01:00
charlesgauthereau 9894e2582d Ready for 1.1.3-rc.2 2025-11-03 10:59:26 +01:00
charlesgauthereau 8a8ef81fce Ready for 1.1.3-rc.2 2025-11-03 10:38:10 +01:00
charlesgauthereau 2fdd9f1296 Adding mysql support. 2025-11-01 23:16:32 +01:00
charlesgauthereau 5251e01011 Refactoring. 2025-11-01 19:38:05 +01:00
charlesgauthereau 84dab35f4b Refactoring in roles. 2025-11-01 14:50:28 +01:00
charlesgauthereau ae766dab2f Refactoring in roles. 2025-11-01 14:50:08 +01:00
charlesgauthereau 027928c29f working on edge key with public server key. 2025-10-31 17:55:16 +01:00
charlesgauthereau e3239639f3 For 1.1.3-rc-2 release. 2025-10-27 18:49:24 +01:00
charlesgauthereau 7d6038e5f1 Working on Advanced Encryption Standard (AES), between agent backup and server. 2025-10-26 11:54:25 +01:00
charlesgauthereau 2a90d4933d Ready for rc-1.1.3 2025-10-12 20:33:59 +02:00
charlesgauthereau 1fbd979d40 Ready for 1.1.2 2025-09-28 12:20:03 +02:00
charlesgauthereau 4e06d2bf97 Ready for 1.1.2-rc.3 2025-09-26 09:22:11 +02:00
charlesgauthereau 8ce3988613 Ready for 1.1.2-rc.3 2025-09-26 09:15:22 +02:00
charlesgauthereau b7f161d200 Ready for 1.1.2-rc.3 2025-09-26 09:04:18 +02:00
charlesgauthereau a3f5c4e494 Ready for 1.1.2-rc.2 2025-09-23 21:19:11 +02:00
charlesgauthereau f74f378c0b Ready for 1.1.2-rc.1 2025-09-23 20:50:20 +02:00
charlesgauthereau 385836d5fe Ready for 1.1.2-rc.1 2025-09-23 20:35:48 +02:00
charlesgauthereau 461e3ff7c9 Ready for 1.1.2-rc.1 2025-09-23 19:55:07 +02:00
charlesgauthereau 54a233b207 Ready for 1.1.2-rc.1 2025-09-23 19:50:33 +02:00
charlesgauthereau 2d2453acf7 Refactoring on some files 2025-09-23 19:42:38 +02:00
charlesgauthereau 654ff6b9a5 Finish working on backups side deletion feature, to be review, test and deply in RC for tests. 2025-09-22 20:31:29 +02:00
charlesgauthereau 34ff11f377 Working on table data in detabase backups. 2025-09-19 13:28:40 +02:00
charlesgauthereau b95e145a8b Working on retention policy and cron system. 2025-09-19 09:58:54 +02:00
charlesgauthereau bce6c9d55f Working on retention policy and cron system. 2025-09-18 09:47:08 +02:00
charlesgauthereau c3e2978919 Working on retention policy and cron system. 2025-09-14 16:46:23 +02:00
charlesgauthereau 2d559e456d Adding methods to delete backups on remote storage s3 and local. Some refactoring on models db and adding common.ts model with createdAt, updatedAt, and deletedAt. 2025-08-31 18:24:07 +02:00
charlesgauthereau 45e3b31fda Adding methods to delete backups on remote storage s3 and local. Some refactoring on models db and adding common.ts model with createdAt, updatedAt, and deletedAt. 2025-08-29 15:31:23 +02:00
charlesgauthereau c8da2579e8 Working on the retention system. 2025-08-28 10:01:17 +02:00
charlesgauthereau c7e30e1659 Working on the retention system. 2025-08-28 09:59:32 +02:00
charlesgauthereau 7be23876aa Working on the retention system. 2025-08-28 09:56:10 +02:00
charlesgauthereau 98f1b329ae Working on the retention system. 2025-08-28 09:53:32 +02:00
charlesgauthereau 19ea5543ea Working on the retention system. 2025-08-28 09:47:42 +02:00
charlesgauthereau 010396ba4d Working on the retention system. 2025-08-28 09:45:47 +02:00
charlesgauthereau f055744432 Working on the retention system. 2025-08-28 09:45:32 +02:00
charlesgauthereau 1920b29a55 Working on the retention system. 2025-08-28 09:45:22 +02:00
charlesgauthereau a0c8d1fb68 Working on the retention system. 2025-08-28 09:34:02 +02:00
charlesgauthereau 59dc3b71b8 Working on the retention system. 2025-08-28 09:32:30 +02:00
charlesgauthereau 598eb44952 Working on the retention system. 2025-08-28 09:31:57 +02:00
charlesgauthereau ac49047177 Working on the retention system. 2025-08-28 09:27:47 +02:00
charlesgauthereau 4448c9e166 Working on the retention system. 2025-08-28 09:23:04 +02:00
charlesgauthereau d70778b556 Working on the retention system. 2025-08-28 09:17:57 +02:00
charlesgauthereau 66edb77113 Working on the retention system. 2025-08-28 09:16:54 +02:00
charlesgauthereau 0f7b3eae11 Working on the retention system. 2025-08-27 21:38:58 +02:00
charlesgauthereau 2f9e33f4db Patching a bug when delete an organization, add refetch. 2025-08-27 21:16:13 +02:00
charlesgauthereau dd7a65262c Patching a bug when delete an organization, add refetch. 2025-08-27 18:07:10 +02:00
charlesgauthereau 4c0d2b1f7f Adding new version of the ButtonWithConfirm component. 2025-08-27 18:01:28 +02:00
charlesgauthereau 60728885b6 Working on the retention system. 2025-08-24 20:11:34 +02:00
charlesgauthereau 7ac99a6618 Working on the organization and permissions. 2025-08-24 19:34:18 +02:00
charlesgauthereau 4ed91c1297 Working on the organization and permissions. 2025-08-24 19:33:42 +02:00
charlesgauthereau 44be8009fd Bug on stats. 2025-08-07 16:23:06 +02:00
charlesgauthereau 96630a0ef7 Merge branch 'feature' 2025-08-07 15:46:52 +02:00
charlesgauthereau 8c9d694b40 Bug on init. 2025-08-07 15:46:41 +02:00
charlesgauthereau 1f09c1507a Merge branch 'feature'
# Conflicts:
#	src/features/dashboard/backup/columns.tsx
2025-08-07 15:06:05 +02:00
Soluce TechnologiesandGitHub 141241d555 Update README.md 2025-08-05 08:22:36 +02:00
Soluce TechnologiesandGitHub c605e8173b Update README.md 2025-08-05 08:21:18 +02:00
charles-gauthereau 37eb8f319f Merge remote-tracking branch 'origin/main' 2025-08-04 08:07:38 +02:00
charles-gauthereau 0492d244b3 columns.tsx 2025-08-04 08:07:26 +02:00
charlesgauthereau baeec9f7a0 Some fix on the s3 and local methods for the files saving, we have to improve testing. Working on table actions. 2025-08-03 12:17:10 +02:00
charlesgauthereau 449c9c2cfe Working on the s3 save dump feature. 2025-08-02 20:13:29 +02:00
charlesgauthereau d13e4b7d40 Working on statistics 2025-08-02 11:14:51 +02:00
charlesgauthereau 13241811d0 Placeholder empty state component. 2025-08-02 08:48:45 +02:00
Soluce TechnologiesandGitHub 4b52dc4a1e Update README.md 2025-08-01 09:35:51 +02:00
charlesgauthereau 63cb6cb7a3 Working on some routing errors. 2025-08-01 09:21:37 +02:00
charlesgauthereau 0511ad289c Merge branch 'main' into feature
# Conflicts:
#	README.md
2025-08-01 08:35:20 +02:00
charlesgauthereau 057a51499e Release 2025-08-01 08:35:02 +02:00
charlesgauthereau 1f01d9c372 Release 2025-08-01 08:34:17 +02:00
charlesgauthereau 0684a4887a Merge branch 'main' into feature
# Conflicts:
#	app/(customer)/dashboard/[slug]/statistics/page.tsx
2025-08-01 08:33:26 +02:00
charlesgauthereau 96be14e1ba Working on the s3 feature. 2025-07-31 17:58:40 +02:00
charlesgauthereau eec3c2737b Working on the s3 feature. 2025-07-31 17:32:03 +02:00
charlesgauthereau a6b960ffb2 Working on the s3 feature. 2025-07-29 09:53:38 +02:00
charlesgauthereau d3f465b5d4 Working on the home page. 2025-07-29 08:51:50 +02:00
charlesgauthereau 2139dfd55d For release. 2025-07-28 18:55:49 +02:00
charlesgauthereau 9f53181467 For release. 2025-07-28 18:43:31 +02:00
charlesgauthereau c96e13c72f For release. 2025-07-28 18:18:23 +02:00
charlesgauthereau 58d9179152 For release. 2025-07-28 17:58:58 +02:00
charlesgauthereau 17469696ee For release. 2025-07-28 16:53:21 +02:00
charlesgauthereau 85047e7509 Working on some bugs. 2025-07-27 21:26:31 +02:00
charlesgauthereau f9dc15ba73 Routing error. 2025-07-27 10:23:56 +02:00
charlesgauthereau 7be542f676 Some bug and visuals improvements. 2025-07-26 22:23:00 +02:00
charlesgauthereau 847a567c52 Working on bugs, add delete restore. Review the auth style. working on the RBAC. 2025-07-26 16:45:43 +02:00
charlesgauthereau 1ffbd6efc8 Working on bugs, add delete restore. Review the auth style. working on the RBAC. 2025-07-26 16:34:06 +02:00
charlesgauthereau f8f599e4dd Working on the Organization side. 2025-07-26 14:43:59 +02:00
charlesgauthereau f7cd169090 Working on header and CSP. 2025-07-25 22:06:40 +02:00
charlesgauthereau bb4003b0db Working on header and CSP. 2025-07-25 21:41:28 +02:00
charlesgauthereau 159398465f But on private and public dir permissions. 2025-07-18 11:31:24 +02:00
charlesgauthereau 76e5e55f14 Ready for release soon. 2025-07-17 13:22:56 +02:00
charlesgauthereau 605946abaa Ready for release soon. 2025-07-17 12:59:38 +02:00
charlesgauthereau 2890d6ac40 Ready for release soon. 2025-07-17 12:43:00 +02:00
charlesgauthereau ac3b4e424a Ready for release soon. 2025-07-17 12:05:39 +02:00
charlesgauthereau 8154f85eb6 Ready for release soon. 2025-07-17 12:01:11 +02:00
charlesgauthereau 45312207c9 Ready for release soon. 2025-07-17 11:58:13 +02:00
charlesgauthereau 128792645b Ready for release soon. 2025-07-17 11:50:27 +02:00
charlesgauthereau 4d52419247 Ready for release soon. 2025-07-17 09:28:57 +02:00
charlesgauthereau 7abdcd57f9 Ready for release soon. 2025-07-16 20:34:28 +02:00
charlesgauthereau 5e704ec18a Testing in local validated. 2025-07-16 20:16:18 +02:00
charlesgauthereau de9fe33194 Finish migration prisma to drizzle. 2025-07-16 18:15:28 +02:00
charlesgauthereau 385777535f Working on the organization system. 2025-07-16 12:36:11 +02:00
charlesgauthereau 405de7b323 Working on the Portabase debug. 2025-05-18 20:46:59 +02:00
Théo LAGACHE 6f2523e524 migration 2025-05-16 15:54:17 +02:00
charles-gauthereau 01019fe1a3 Patch force dynamic route for dashboard/agent 2025-02-21 08:59:37 +01:00
charles-gauthereau b1ecb7ce01 Bug. 2025-02-21 08:43:01 +01:00
charles-gauthereau 0cf350cdcc Bug. 2025-02-21 08:21:11 +01:00
charles-gauthereau ed5f7a43f2 Bug. 2025-02-20 10:26:37 +01:00
charles-gauthereau 54f0e87805 Deploy new version. 2025-02-20 09:26:38 +01:00
charles-gauthereau f570a67805 Deploy new version. 2025-02-14 09:29:47 +01:00
charles-gauthereau d0ec5902b5 Correcting some bugs in statistics. 2025-02-14 09:25:45 +01:00
charles-gauthereau 8586abf7e5 Correcting some bugs in statistics. 2025-02-13 19:20:55 +01:00
charles-gauthereau 6562718635 Correcting some bugs in statistics. 2025-01-12 17:53:39 +01:00
charles-gauthereau 7a59c616eb Adding some methods in order to support the serving files images dynamically in production. 2025-01-12 14:03:15 +01:00
charles-gauthereau 2440592286 Bug on Auth. 2025-01-12 12:30:32 +01:00
charles-gauthereau dca1799aac Bug on Auth. 2025-01-03 22:02:52 +01:00
charles-gauthereau d42d73661e url image bug patched. 2025-01-03 21:47:22 +01:00
charles-gauthereau a43878688b Patching some bugs. 2025-01-03 20:51:11 +01:00
charles-gauthereau 7ca905440c Patching some bugs. 2025-01-03 20:50:51 +01:00
charles-gauthereau c733fb4fdc Patching some bugs. 2025-01-03 20:48:58 +01:00
charles-gauthereau 7193f5b71f Patching some bugs. 2025-01-03 18:10:40 +01:00
charles-gauthereau b59a010425 Patching some bugs. 2025-01-03 15:55:16 +01:00
615 changed files with 61984 additions and 49521 deletions
+23 -2
View File
@@ -1,4 +1,25 @@
# Ignore Docker-related configs
.dockerignore
docker-compose.yml
README.md
docker-compose*.yml
# Development/Editor config
.idea
.next
.vscode
# Node-related
node_modules
npm-debug.log
yarn-debug.log
yarn-error.log
pnpm-debug.log
# Next.js build output
.next
out
# Docs and markdowns
README.md
*.md
+37
View File
@@ -0,0 +1,37 @@
# Environnement
NODE_ENV=production
# Base de données
DATABASE_URL=postgresql://devuser:changeme@db:5432/devdb?schema=public
# Projet
PROJECT_NAME="Portabase"
PROJECT_DESCRIPTION="Portabase is a powerful database manager"
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=
AUTH_GOOGLE_METHOD=
# 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
# Retention
RETENTION_CRON="* * * * *"
+1 -1
View File
@@ -26,7 +26,7 @@ Please take a moment to review this guide. It will help you understand how to co
2. **Clone the repository**
```bash
git clone https://github.com/Soluce-Technologies/portabase
git clone https://github.com/Portabase/portabase
```
3. **Set up the development environment**
+3 -3
View File
@@ -7,9 +7,9 @@
We take security seriously and aim to support the following versions of the project with security updates:
| Version | Supported |
|-----------|--------------------|
| 1.x | ✅ Fully Supported |
| Version | Supported |
|---------|--------------------|
| Latest | ✅ Fully Supported |
---
+11 -10
View File
@@ -29,6 +29,16 @@ jobs:
with:
images: solucetechnologies/portabase
- name: Set tags
id: set-tags
run: |
echo "RELEASE_TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
TAGS="solucetechnologies/portabase:${GITHUB_REF#refs/tags/}"
if [ "${{ github.event.release.target_commitish }}" = "main" ]; then
TAGS="$TAGS,solucetechnologies/portabase:latest"
fi
echo "tags=$TAGS" >> $GITHUB_OUTPUT
- name: Build and push Docker image
id: push
uses: docker/build-push-action@v6
@@ -36,14 +46,5 @@ jobs:
context: .
file: ./docker/dockerfile/Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
tags: ${{ steps.set-tags.outputs.tags }}
target: prod
# - name: Generate artifact attestation
# uses: actions/attest-build-provenance@v2
# with:
# subject-name: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME}}
# subject-digest: ${{ steps.push.outputs.digest }}
# push-to-registry: true
+31
View File
@@ -0,0 +1,31 @@
name: Security Checks
on:
pull_request:
push:
branches: [ main ]
jobs:
sca-deps: # Dependency & container scan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: aquasecurity/trivy-action@0.20.0
with:
scan-type: 'fs'
format: 'table'
severity: 'CRITICAL,HIGH'
ignore-unfixed: true
secrets-gitleaks: # Secrets exposure
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch full history for gitleaks
- uses: gitleaks/gitleaks-action@v2
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
GITLEAKS_LICENSE: ${{ secrets.GITLEAKS_LICENSE }}
with:
config-path: .gitleaks.toml # Optional, if you have a custom config
+2 -2
View File
@@ -1,10 +1,10 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
.idea
# dependencies
/node_modules
/.pnp
.pnp.js
.yarn/install-state.gz
#.yarn/install-state.gz
# testing
/coverage
+10
View File
@@ -0,0 +1,10 @@
title = "Custom gitleaks config"
[allowlist]
# Global allowlist patterns (wont be flagged)
regexes = [
"s3SecretAccessKey: env.S3_SECRET_KEY ?? null",
]
paths = [
"src/utils/init.ts"
]
+5
View File
@@ -0,0 +1,5 @@
repos:
- repo: https://github.com/gitleaks/gitleaks
rev: v8.24.2
hooks:
- id: gitleaks
+3
View File
@@ -0,0 +1,3 @@
{
"WillLuke.nextjs.hasPrompted": true
}
Binary file not shown.
-934
View File
File diff suppressed because one or more lines are too long
+2 -2
View File
@@ -1,2 +1,2 @@
nodeLinker: "node-modules"
yarnPath: .yarn/releases/yarn-4.5.3.cjs
nodeLinker: node-modules
+30
View File
@@ -0,0 +1,30 @@
cff-version: 1.2.0
title: Portabase
message: "If you use this software, please cite it as below."
type: software
authors:
- family-names: Gauthereau
given-names: Charles
- family-names: Larcher
given-names: Killian
repository-code: https://github.com/Portabase/portabase
url: https://portabase.io
abstract: "Portabase is a free, open-source, self-hosted solution for database administration, providing backup and restore capabilities, scheduling, retention policies, notifications, and support for multiple storage backends. Its headless agent architecture enables connection to multiple database instances securely and efficiently."
keywords:
- docker
- kubernetes
- backups
- postgresql
- mysql
- mariadb
- devops
- database
- monitoring
- s3
- self-hosted
- system-administration
- web-ui
- agent
license: Apache-2.0
version: 1.1.6
date-released: "2026-01-01"
+4 -2
View File
@@ -1,2 +1,4 @@
migrate:
docker compose run --rm app sh -c "npx prisma migrate dev"
CLUSTER_SCRIPT=docker/entrypoints/app-dev-entrypoint.sh
up:
@bash $(CLUSTER_SCRIPT)
+286 -146
View File
@@ -1,176 +1,316 @@
<!-- PROJECT LOGO -->
[//]: # (<br />)
[//]: # (<div align="center">)
[//]: # ( <a href="https://github.com/othneildrew/Best-README-Template">)
[//]: # ( <img src="images/logo.png" alt="Logo" width="80" height="80">)
[//]: # ( </a>)
<br />
<div align="center">
<a href="https://portabase.io">
<img src="/public/images/logo.png" alt="Logo" width="80" height="80">
</a>
<h3 align="center">Portabase</h3>
<p align="center">
Backup / Restore your databases instances
<br />
<a href="https://portabase.io"><strong>Explore the docs »</strong></a>
<br />
<br />
<a href="https://portabase.io">View Demo</a>
·
<a href="https://github.com/Soluce-Technologies/portabase/issues/new?labels=bug&template=bug-report---.md">Report Bug</a>
·
<a href="https://github.com/Soluce-Technologies/portabase/issues/new?labels=enhancement&template=feature-request---.md">Request Feature</a>
<p>
Take full control of your databases with Portabase — the self-hosted, open-source platform
for automated backup, restoration, and operational management. Powered by the <strong>Portabase Agent</strong>,
every database in your infrastructure can be monitored, backed up, and managed in real time,
with zero reliance on third-party services.
</p>
<p>
Secure, lightweight, and deployable anywhere — on Docker, Kubernetes, or directly on your servers.
Designed for teams, DevOps, and enterprises who demand control, reliability, and automation at scale.
</p>
[![License: Apache](https://img.shields.io/badge/License-apache-yellow.svg)](LICENSE)
[![Docker Pulls](https://img.shields.io/docker/pulls/solucetechnologies/portabase?color=brightgreen)](https://hub.docker.com/r/solucetechnologies/portabase)
[![Platform](https://img.shields.io/badge/platform-linux%20%7C%20macos%20%7C%20windows-lightgrey)](https://github.com/Portabase/portabase)
[![PostgreSQL](https://img.shields.io/badge/PostgreSQL-336791?logo=postgresql&logoColor=white)](https://www.postgresql.org/)
[![MySQL](https://img.shields.io/badge/MySQL-4479A1?logo=mysql&logoColor=white)](https://www.mysql.com/)
[![MariaDB](https://img.shields.io/badge/MariaDB-003545?logo=mariadb&logoColor=white)](https://mariadb.org/)
[![Self Hosted](https://img.shields.io/badge/self--hosted-yes-brightgreen)](https://github.com/Portabase/portabase)
[![Open Source](https://img.shields.io/badge/open%20source-❤️-red)](https://github.com/Portabase/portabase)
<!-- TABLE OF CONTENTS -->
<details>
<summary>Table of Contents</summary>
<ol>
<li>
<a href="#about-the-project">About The Project</a>
<ul>
<li><a href="#built-with">Built With</a></li>
</ul>
</li>
<li>
<a href="#getting-started">Getting Started</a>
<ul>
<li><a href="#prerequisites">Prerequisites</a></li>
<li><a href="#installation">Installation</a></li>
</ul>
</li>
<li><a href="#usage">Usage</a></li>
<li><a href="#roadmap">Roadmap</a></li>
<li><a href="#contributing">Contributing</a></li>
<li><a href="#license">License</a></li>
<li><a href="#contact">Contact</a></li>
<li><a href="#acknowledgments">Acknowledgments</a></li>
</ol>
</details>
<p>
<strong>
<a href="https://portabase.io">Documentation</a> •
<a href="https://www.youtube.com/watch?v=hvLbX5LN1UE">Demo</a> •
<a href="#installation">Installation</a> •
<a href="https://github.com/Portabase/portabase/issues/new?labels=bug&template=bug-report---.md">Report Bug</a>
<a href="https://github.com/Portabase/portabase/issues/new?labels=enhancement&template=feature-request---.md">Request Feature</a>
</strong>
</p>
![portabase-dashboard](https://github.com/user-attachments/assets/f5ec7204-79c0-43ac-ba17-f2ed682c39d6)
<!-- ABOUT THE PROJECT -->
## About The Project
</div>
Portabase is the server dashboard service working with portabase agents.
<a href="https://github.com/Soluce-Technologies/portabase">see Portabase</a>
---
<p align="right">(<a href="#readme-top">back to top</a>)</p>
## ✨ About The Project
**Portabase** is a server dashboard tool designed to simplify the backup and restoration of your database instances. It
integrates seamlessly with [Portabase agents](https://github.com/Portabase/agent-portabase) for managing operations
securely and efficiently.
### 🔧 Built With
### Built With
- [![NextJS][NextJS]][NextJS-url]
- [![Drizzle][Drizzle]][Drizzle-url]
- [![ShadcnUI][ShadcnUI]][ShadcnUI-url]
- [![BetterAuth][BetterAuth]][BetterAuth-url]
- [![Docker][Docker]][Docker-url]
* [![NextJS][NextJS]][NextJS-url]
* [![Prisma][Prisma]][Prisma-url]
---
## 📦 Features
<p align="right">(<a href="#readme-top">back to top</a>)</p>
### 🗄️ Supported databases
- PostgreSQL
- MySQL
- MariaDB
### ⏱️ Scheduled backups
<!-- GETTING STARTED -->
## Getting Started
- Cron-based scheduling for full control
- Manual trigger support for on-demand backups
### Prerequisites
### 💾 Storage backends
Install docker on your machine
- On-premise storage: Backups are stored directly on your VPS or server
- Cloud storage: S3-compatible backends
supported ([documentation](https://portabase.io/docs/dashboard/advanced/storage/s3))
- Full data ownership: No third-party access — your data stays under your control
### 🔔 Smart notifications
- Multi-channel delivery: Email, Slack, Discord, webhooks
- Real-time alerts: Immediate feedback on success and failure
- Custom alert policies: Database-level notification rules
- Team-ready: Designed for DevOps, on-call, and incident workflows
### 👥 Built for team environments
- Workspaces: Organize databases, notification channels, and storage backends by organization and project
- Access control: Fine-grained, role-based permissions on all resources
- Role management: Member, admin, and owner roles at both system and organization levels
### 🐳 Self-hosted & secure
- Containerized deployment: Docker-based setup for predictable installation and operations
- Privacy by design: All data remains within your own infrastructure
- Open source: Apache 2.0 licensed — fully auditable codebase
### 🤖 Portabase Agent ([details](https://github.com/Portabase/agent-portabase))
- Headless architecture: Runs locally on your infrastructure to manage backups and database operations
- Multi-target support: Single agent can connect to multiple databases across different servers
- Lightweight & efficient: Minimal resource footprint while providing full operational control
- Secure communication: Encrypted channels between agent and central dashboard
---
## 🚀 Getting Started
### Installation
Installation steps in order to use it locally and work on some features improvements
You have 3 ways to install **Portabase**:
1. Clone the repo
```sh
git clone https://github.com/Soluce-Technologies/portabase
```
3. Start the project
```sh
docker compose up
```
- Automated CLI (recommended) - [details](https://portabase.io/docs/cli)
- Docker Compose setup - [details](https://portabase.io/docs/dashboard/setup)
- Kubernetes with Helm (soon)
<p align="right">(<a href="#readme-top">back to top</a>)</p>
Ensure Docker is installed on your machine before getting started.
### Docker Compose Setup
Create a `docker-compose.yml` file with the following configuration:
```yaml
name: portabase
services:
portabase:
image: solucetechnologies/portabase:latest
env_file:
- .env
ports:
- '8887:80'
environment:
- TZ="Europe/Paris"
volumes:
- portabase-private:/app/private
depends_on:
db:
condition: service_healthy
container_name: portabase-app
db:
image: postgres:17-alpine
ports:
- "5433:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=<your_database>
- POSTGRES_USER=<database_user>
- POSTGRES_PASSWORD=<database_password>
healthcheck:
test: [ "CMD-SHELL", "pg_isready -U <database_user> -d <your_database>" ]
interval: 10s
timeout: 5s
retries: 5
volumes:
postgres-data:
portabase-private:
```
Then run:
```bash
docker compose up -d
```
If you use reverse proxy like
Traefik : [Check this link](https://portabase.io/docs/dashboard/advanced/reverse-proxy)
#### Environment Variables
```yml
# Environment
NODE_ENV=production
# Database
DATABASE_URL=postgresql://devuser:changeme@db:5432/devdb?schema=public
# Project Info
PROJECT_NAME="Portabase"
PROJECT_DESCRIPTION="Portabase is a powerful database manager"
PROJECT_URL=http://app.portabase.io
PROJECT_SECRET=
# SMTP (Email)
SMTP_HOST=
SMTP_PORT=587
SMTP_USER=
SMTP_PASSWORD=
SMTP_FROM=
# Google OAuth
AUTH_GOOGLE_ID=
AUTH_GOOGLE_SECRET=
AUTH_GOOGLE_METHOD=
# 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
# Storage Backend: 'local' or 's3'
STORAGE_TYPE=local
# Retention
RETENTION_CRON="* * * * *"
```
To get more information about env variables, check
that [link](https://portabase.io/docs/dashboard/advanced/environment)
### Locally (Development)
1. Clone the repository:
```bash
git clone https://github.com/Portabase/portabase
cd portabase
```
2. Start the development database for Portabase service:
```bash
docker compose up
```
3. Start the Next.js app:
```bash
make up
```
---
## 🛠️ Usage
Once the installation process is done, follow the steps to configure your instance.
### Dashboard configuration process
1. **Access the dashboard** Open `http://localhost:8887` in your browser.
2. **Sign up** Register the first user, who will automatically have the **Admin** role in the default workspace.
3. **Add your first agent** Follow [this guide](https://github.com/Portabase/agent-portabase) for setup
instructions.
4. **Create organizations and projects** Link your databases to projects to enable backups and restores.
5. **Configure backup policies** Define schedules (hourly, daily, weekly, or monthly) and retention rules.
6. **Choose a storage provider** Select where backups will be stored (local, S3, etc.).
7. **Save and start** Portabase validates your configuration and starts automated backups based on your defined
policies.
---
## 🤝 Contributing
Contributions are welcome and appreciated! Here's how to get started:
1. Fork the repository
2. Create a new branch:
```bash
git checkout -b features/your-feature
```
3. Commit your changes:
```bash
git commit -m "Add YourFeature"
```
4. Push to the branch:
```bash
git push origin features/your-feature
```
5. Open a pull request
### Top Contributors
[![Contributors](https://contrib.rocks/image?repo=Portabase/portabase)](https://github.com/Portabase/portabase/graphs/contributors)
---
## 📄 License
Distributed under the Apache License. See `LICENSE.txt` for more details.
---
## 🙏 Acknowledgments
Thanks to all contributors and the open-source community!
Give the project a ⭐ if you like it!
<!-- ROADMAP -->
## Roadmap
- [ ] Add Changelog
- [ ] Improve documentation and repositories
- [ ] Add tests procedure
- [ ] Add Release File
- [ ] Security improvements
- [ ] Multi locale support
- [ ] Multi db 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).
<p align="right">(<a href="#readme-top">back to top</a>)</p>
<!-- CONTRIBUTING -->
## Contributing
Contributions are what make the open source community such an amazing place to learn, inspire, and create. Any contributions you make are **greatly appreciated**.
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!
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
### Top contributors:
<a href="https://github.com/Soluce-Technologies/portabase/graphs/contributors">
<img src="https://contrib.rocks/image?repo=Soluce-Technologies/portabase" alt="contrib.rocks image" />
</a>
<p align="right">(<a href="#readme-top">back to top</a>)</p>
## Memo
If you're using Semantic Versioning (SemVer) to version your Docker images, you can use the following format:
```major.minor.patch-rc.release (e.g. 1.0.0-rc.1)```
```major.minor.patch-rc.release-tag (e.g. 1.0.0-rc.1-dev)```
<!-- LICENSE -->
## License
Distributed under the Apache License. See `LICENSE.txt` for more information.
<p align="right">(<a href="#readme-top">back to top</a>)</p>
<!-- CONTACT -->
## Contact
Killian Larcher - killian.larcher@soluce-technologies.com
Charles Gauthereau - charles.gauthereau@soluce-technologies.com
Project Link: [https://github.com/Soluce-Technologies/portabase](https://github.com/Soluce-Technologies/agent-portabase)
<p align="right">(<a href="#readme-top">back to top</a>)</p>
<!-- MARKDOWN LINKS & IMAGES -->
<!-- https://www.markdownguide.org/basic-syntax/#reference-style-links -->
[Docker]: https://img.shields.io/badge/Docker-2496ED?logo=docker&logoColor=fff&style=for-the-badge
[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/
[BetterAuth]: https://img.shields.io/badge/Better%20Auth-FFF?logo=betterauth&logoColor=000&style=for-the-badge
[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/
[BetterAuth-url]: https://www.better-auth.com/
[Drizzle-url]: https://orm.drizzle.team/
[ShadcnUI-url]: https://ui.shadcn.com/
[Docker-url]: https://www.docker.com/
+25
View File
@@ -0,0 +1,25 @@
import {CardContent, CardHeader} from "@/components/ui/card";
import {TooltipProvider} from "@/components/ui/tooltip";
import {ForgotPasswordForm} from "@/components/wrappers/auth/login/forgot-password-form/forgot-password-form";
import {CardAuth} from "@/features/layout/card-auth";
export default async function RoutePage(props: { searchParams: Promise<{ callbackUrl: string | undefined }> }) {
return (
<TooltipProvider>
<CardAuth className="w-full">
<CardHeader>
<div className="grid gap-2 text-center mb-2">
<h1 className="text-3xl font-bold">Reset password</h1>
<p className="text-balance text-muted-foreground">Enter your email address and we'll send you a
link to reset your password.</p>
</div>
</CardHeader>
<CardContent>
<ForgotPasswordForm/>
</CardContent>
</CardAuth>
</TooltipProvider>
);
}
+33
View File
@@ -0,0 +1,33 @@
import {CardContent, CardHeader} from "@/components/ui/card";
import {TooltipProvider} from "@/components/ui/tooltip";
import {GuardForm} from "@/components/wrappers/auth/guard/guard-form";
import {cookies} from "next/headers";
import {redirect} from "next/navigation";
import {CardAuth} from "@/features/layout/card-auth";
export default async function GuardPage() {
const cookieStore = await cookies();
const token = cookieStore.get("better-auth.two_factor")?.value;
if (!token) {
redirect("/login");
}
return (
<TooltipProvider>
<CardAuth className="w-full max-w-md">
<CardHeader>
<div className="grid gap-2 text-center mb-2">
<h1 className="text-3xl font-bold">Two-factor verification</h1>
<p className="text-balance text-muted-foreground">Please enter the verification code generated
by your authentication app.</p>
</div>
</CardHeader>
<CardContent>
<GuardForm/>
</CardContent>
</CardAuth>
</TooltipProvider>
);
}
+14 -23
View File
@@ -1,34 +1,25 @@
import React from "react";
import {LayoutAdmin} from "@/components/layout";
import {currentUser} from "@/auth/current-user";
import {redirect} from "next/navigation";
import {prisma} from "@/prisma";
import {currentUser} from "@/lib/auth/current-user";
import {AuthLogoSection} from "@/components/wrappers/auth/auth-logo-section";
export default async function Layout({children}: { children: React.ReactNode }) {
const user = await currentUser()
if(user){
const userInfo = await prisma.user.findUnique({
where: {
email: user?.email
}
})
if(userInfo && userInfo.authMethod){
redirect('/')
// redirect('/dashboard')
}
const user = await currentUser();
if (user && !user.banned && user.role !== "pending") {
redirect("/dashboard/home");
}
return (
<LayoutAdmin>
<div
className="w-full h-full grid "
>
<div className="flex items-center justify-center py-12">
{children}
</div>
<div className="flex min-h-full flex-1 flex-col justify-center py-12 sm:px-6 lg:px-8 ">
<div className="mx-auto w-full max-w-md">
<AuthLogoSection/>
<div>{children}</div>
</div>
</LayoutAdmin>
<footer className="py-4 text-center text-xs justify-items-end text-muted-foreground">
Powered by <span className="font-medium">Soluce Technologies</span>
</footer>
</div>
)
}
+47 -24
View File
@@ -1,29 +1,52 @@
"use client"
import {LoginForm} from "@/components/wrappers/auth/login/login-form/login-form";
import {Metadata} from "next";
import {SUPPORTED_PROVIDERS} from "../../../portabase.config";
import {SocialAuthButtons} from "@/components/wrappers/auth/social-buttons";
import {TooltipProvider} from "@/components/ui/tooltip";
import {CardContent, CardHeader} from "@/components/ui/card";
import Link from "next/link";
import {Separator} from "@/components/ui/separator";
import {CardAuth} from "@/features/layout/card-auth";
import {useEffect, useRef} from "react";
import {useSearchParams} from "next/navigation";
export const metadata: Metadata = {
title: "Login",
};
import {LoginForm} from "@/components/wrappers/auth/login/loginForm/LoginForm";
import {toast} from "sonner";
export default function SignInPage(props: {
searchParams: Promise<{ callbackUrl: string | undefined }>
}) {
const searchParams = useSearchParams();
const error = searchParams.get("error");
const isFirstRender = useRef(true);
useEffect(() => {
if (isFirstRender.current && error) {
toast.error("Error occurred.");
isFirstRender.current = false;
}
}, [error]);
export default async function SignInPage() {
return (
<div className="mx-auto grid w-[350px] gap-6">
<LoginForm/>
</div>
<TooltipProvider>
<CardAuth className="w-full">
<CardHeader>
<div className="grid gap-2 text-center mb-2">
<h1 className="text-3xl font-bold">Login</h1>
<p className="text-balance text-muted-foreground">Fill your login informations</p>
</div>
</CardHeader>
<CardContent className="space-y-4">
<LoginForm/>
{SUPPORTED_PROVIDERS.filter((p) => !p.isManual && p.isActive).length > 0 && (
<>
<div className="relative my-4 flex items-center justify-center overflow-hidden">
<Separator/>
<div className="px-2 text-center text-sm">OR</div>
<Separator/>
</div>
<SocialAuthButtons providers={SUPPORTED_PROVIDERS}/>
</>
)}
<div className="mt-4 text-center text-sm">
Don&apos;t have an account ?{" "}
<Link href="/register" className="underline">
Sign up
</Link>
</div>
</CardContent>
</CardAuth>
</TooltipProvider>
)
}
}
+7 -2
View File
@@ -1,9 +1,14 @@
import {PageParams} from "@/types/next";
import {RegisterForm} from "@/components/wrappers/auth/Register/register-form/RegisterForm";
import {RegisterForm} from "@/components/wrappers/auth/register/register-form/register-form";
import {Metadata} from "next";
export const metadata: Metadata = {
title: "Register",
};
export default async function RoutePage(props: PageParams<{}>) {
return (
<div className="mx-auto grid w-[350px] gap-6">
<div className="mx-auto grid w-full gap-6">
<RegisterForm/>
</div>
)
+59
View File
@@ -0,0 +1,59 @@
import { Card, CardContent, CardHeader } from "@/components/ui/card";
import { TooltipProvider } from "@/components/ui/tooltip";
import { ResetPasswordForm } from "@/components/wrappers/auth/login/reset-password-form/reset-password-form";
import { redirect } from "next/navigation";
import { auth } from "@/lib/auth/auth";
import { Avatar, AvatarImage, AvatarFallback } from "@radix-ui/react-avatar";
export default async function RoutePage(props: { searchParams: Promise<{ token: string | undefined }> }) {
const { token } = await props.searchParams;
if (!token) {
return redirect(`/login?error=invalid_or_expired_token`);
}
const verification = await (await auth.$context).internalAdapter.findVerificationValue(`reset-password:${token}`);
if (!verification || verification.expiresAt < new Date()) {
return redirect(`/login?error=invalid_or_expired_token`);
}
const user = await (await auth.$context).internalAdapter.findUserById(verification.value);
return (
<TooltipProvider>
<Card className="w-full max-w-md shadow-lg">
<CardHeader className="space-y-4">
<div className="space-y-1 text-center">
<h1 className="text-2xl font-bold tracking-tight">Set a new password</h1>
<p className="text-sm text-muted-foreground text-balance">Please enter your new password below.</p>
</div>
<div className="flex flex-col items-center space-y-2 text-center">
<Avatar className="relative flex h-16 w-16 shrink-0 overflow-hidden rounded-full border">
<AvatarImage src={user!.image ?? ""} alt={user!.name} className="aspect-square h-full w-full object-cover" />
<AvatarFallback className="flex h-full w-full items-center justify-center rounded-full bg-muted text-2xl font-medium text-muted-foreground">
{user!.name
.split(" ")
.map((n) => n[0])
.join("")
.toUpperCase()
.slice(0, 2)}
</AvatarFallback>
</Avatar>
<div className="flex flex-col items-center">
<div className="font-semibold text-lg">{user!.name}</div>
<div className="text-sm text-muted-foreground">{user!.email}</div>
</div>
</div>
</CardHeader>
<CardContent>
<ResetPasswordForm />
</CardContent>
</Card>
</TooltipProvider>
);
}
@@ -0,0 +1,47 @@
import {notFound} from "next/navigation";
import {eq} from "drizzle-orm";
import {db} from "@/db";
import * as drizzleDb from "@/db";
import {PageParams} from "@/types/next";
import {Page} from "@/features/layout/page";
import {OrganizationManagement} from "@/components/wrappers/dashboard/admin/organizations/organization/organization-management";
import {buildOrganizationWithMembers} from "@/utils/common";
import {isUUID} from "@/utils/text";
import {user} from "@/db/schema/02_user";
import {invitation} from "@/db/schema/05_invitation";
import {member} from "@/db/schema/04_member";
import {organization} from "@/db/schema/03_organization";
import {user as drizzleUser} from "@/db/schema/02_user";
export default async function RoutePage(props: PageParams<{ organizationId: string }>) {
const {organizationId} = await props.params;
if (!organizationId) {
return notFound();
}
if (!isUUID(organizationId)) {
return notFound();
}
const users = await db.select().from(drizzleUser);
const organizationData = await db
.select({organization, member, user, invitation})
.from(organization)
.leftJoin(member, eq(drizzleDb.schemas.organization.id, member.organizationId))
.leftJoin(invitation, eq(drizzleDb.schemas.invitation.id, invitation.organizationId))
.leftJoin(user, eq(drizzleDb.schemas.member.userId, user.id))
.where(eq(organization.id, organizationId));
const formattedData = buildOrganizationWithMembers(organizationData);
if (!formattedData) return notFound();
return (
<Page>
<OrganizationManagement organization={formattedData} users={users}/>
</Page>
);
}
@@ -0,0 +1,35 @@
import {PageParams} from "@/types/next";
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {db} from "@/db";
import {
AdminOrganizationAddModal
} from "@/components/wrappers/dashboard/admin/organizations/organization/admin-organization-add-modal";
import {AdminOrganizationList} from "@/components/wrappers/dashboard/admin/organizations/organization/admin-orgnization-list";
import {isNull} from "drizzle-orm";
export default async function RoutePage(props: PageParams<{}>) {
const organizations = await db.query.organization.findMany({
where: (fields) => isNull(fields.deletedAt),
with: {
members: true,
},
});
return (
<Page>
<PageHeader className="flex flex-col">
<div className="flex justify-between">
<PageTitle className="mb-3">Active organizations</PageTitle>
<PageActions>
<AdminOrganizationAddModal/>
</PageActions>
</div>
</PageHeader>
<PageContent className="flex flex-col gap-5">
<AdminOrganizationList organizations={organizations}/>
</PageContent>
</Page>
);
}
@@ -1,42 +0,0 @@
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";
export default async function RoutePage(props: PageParams<{}>) {
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"
}
})
return (
<Page>
<PageHeader>
<PageTitle>
Administration Panel
</PageTitle>
</PageHeader>
<PageContent className="mt-10">
<AdminTabs settings={settings} currentUser={user} users={users}/>
</PageContent>
</Page>
)
}
@@ -0,0 +1,30 @@
import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {db} from "@/db";
import {SettingsEmailSection} from "@/components/wrappers/dashboard/admin/settings/email/settings-email-section";
import {notFound} from "next/navigation";
export default async function RoutePage(props: PageParams<{}>) {
const settings = await db.query.setting.findFirst({
where: (fields, {eq}) => eq(fields.name, "system"),
});
if (!settings) {
notFound()
}
return (
<Page>
<PageHeader className="flex flex-col">
<div className="flex justify-between">
<PageTitle className="mb-3">System email</PageTitle>
</div>
</PageHeader>
<PageContent className="flex flex-col gap-5">
<SettingsEmailSection settings={settings}/>
</PageContent>
</Page>
);
}
@@ -0,0 +1,29 @@
import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {SettingsStorageSection} from "@/components/wrappers/dashboard/admin/settings/storage/settings-storage-section";
import {db} from "@/db";
import {notFound} from "next/navigation";
export default async function RoutePage(props: PageParams<{}>) {
const settings = await db.query.setting.findFirst({
where: (fields, {eq}) => eq(fields.name, "system"),
});
if (!settings) {
notFound()
}
return (
<Page>
<PageHeader className="flex flex-col">
<div className="flex justify-between">
<PageTitle className="mb-3">System storage</PageTitle>
</div>
</PageHeader>
<PageContent className="flex flex-col gap-5">
<SettingsStorageSection settings={settings}/>
</PageContent>
</Page>
);
}
@@ -0,0 +1,41 @@
import {PageParams} from "@/types/next";
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {db} from "@/db";
import {desc, isNull} from "drizzle-orm";
import {AdminUserList} from "@/components/wrappers/dashboard/admin/users/admin-user-list";
import {AdminUserAddModal} from "@/components/wrappers/dashboard/admin/users/admin-user-add-modal";
export default async function RoutePage(props: PageParams<{}>) {
const users = await db.query.user.findMany({
where: (fields) => isNull(fields.deletedAt),
with: {
accounts: true
},
orderBy: (fields) => desc(fields.createdAt),
});
const organizations = await db.query.organization.findMany({
with: {
members: true,
},
});
return (
<Page>
<PageHeader className="flex flex-col">
<div className="flex justify-between">
<PageTitle className="mb-3">Active users</PageTitle>
<PageActions>
<AdminUserAddModal organizations={organizations}/>
</PageActions>
</div>
</PageHeader>
<PageContent className="flex flex-col gap-5">
<AdminUserList users={users}/>
</PageContent>
</Page>
);
}
@@ -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/agent-form/agent-form";
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 (
<Page>
<PageHeader>
<PageTitle>
Edit {agent.name}
</PageTitle>
<PageTitle>Edit {agent.name}</PageTitle>
</PageHeader>
<PageContent>
<AgentForm defaultValues={agent} agentId={agent.id}/>
<AgentForm defaultValues={agent} agentId={agent.id} />
</PageContent>
</Page>
)
}
);
}
@@ -1,100 +1,58 @@
import Link from "next/link";
import {GearIcon} from "@radix-ui/react-icons";
import {prisma} from "@/prisma";
import {PageParams} from "@/types/next";
import {Page, PageContent, PageDescription, PageTitle} from "@/features/layout/page";
import {formatDateLastContact} from "@/utils/date-formatting";
import {Page, PageActions, PageContent, PageDescription, PageTitle} from "@/features/layout/page";
import {buttonVariants} from "@/components/ui/button";
import {Card, CardContent, CardHeader} from "@/components/ui/card";
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
import {DatabaseCard} from "@/components/wrappers/dashboard/projects/ProjectCard/ProjectDatabaseCard";
import {AgentCardKey} from "@/components/wrappers/dashboard/agent/AgentCardKey/AgentCardKey";
import {db} from "@/db";
import * as drizzleDb from "@/db";
import {eq} from "drizzle-orm";
import {notFound} from "next/navigation";
import {ButtonDeleteAgent} from "@/components/wrappers/dashboard/agent/button-delete-agent/button-delete-agent";
import {capitalizeFirstLetter} from "@/utils/text";
import {generateEdgeKey} from "@/utils/edge_key";
import {getServerUrl} from "@/utils/get-server-url";
import {AgentContentPage} from "@/components/wrappers/dashboard/agent/agent-content";
export default async function RoutePage(props: PageParams<{ agentId: string }>) {
const {agentId} = await props.params
const agent = await prisma.agent.findUnique({
where: {
id: agentId,
},
include: {
databases: {}
const agent = await db.query.agent.findFirst({
where: eq(drizzleDb.schemas.agent.id, agentId),
with: {
databases: true
}
})
const databaseId = 'db-123';
const totalBackups = await prisma.backup.count({
where: {
databaseId: databaseId,
},
});
const successfulBackups = await prisma.backup.count({
where: {
databaseId: databaseId,
status: 'success',
},
});
const successRate = totalBackups > 0 ? (successfulBackups / totalBackups) * 100 : null
if (!agent) {
notFound()
}
const edgeKey = await generateEdgeKey(getServerUrl(), agent.id);
return (
<Page>
<div className="justify-between gap-2 sm:flex">
<PageTitle className="flex items-center">
{agent.name}
{capitalizeFirstLetter(agent.name)}
<Link className={buttonVariants({variant: "outline"})}
href={`/dashboard/agents/${agent.id}/edit`}>
<GearIcon className="w-7 h-7"/>
</Link>
{/*<AgentModalKey agent={agent}>*/}
{/* <Button variant="outline">*/}
{/* <KeyRound/>*/}
{/* </Button>*/}
{/*</AgentModalKey>*/}
</PageTitle>
<PageActions className="justify-between">
<ButtonDeleteAgent agentId={agentId} text={"Delete Agent"}/>
</PageActions>
</div>
<PageDescription className="mt-5 sm:mt-0">{agent.description}</PageDescription>
<PageContent className="flex flex-col w-full h-full">
<div className="flex flex-col sm:flex-row sm:justify-between gap-8 mb-6">
<Card className="w-full sm:w-auto flex-1">
<CardHeader className="font-bold text-xl">
Backups
</CardHeader>
<CardContent></CardContent>
</Card>
<Card className="w-full sm:w-auto flex-1">
<CardHeader className="font-bold text-xl">
Success rate
</CardHeader>
<CardContent>
{successRate ?? "Unavailable for now."}
</CardContent>
</Card>
<Card className="w-full sm:w-auto flex-1">
<CardHeader className="font-bold text-xl">
Last contact
</CardHeader>
<CardContent>
{formatDateLastContact(agent.lastContact)}
</CardContent>
</Card>
</div>
<Card className="w-full sm:w-auto flex-1">
<CardHeader className="font-bold text-xl">
Edge Key
</CardHeader>
<CardContent>
<AgentCardKey agent={agent}/>
</CardContent>
</Card>
<CardsWithPagination data={agent.databases} cardItem={DatabaseCard}/>
{agent.description && (
<PageDescription className="mt-5 sm:mt-0">{agent.description}</PageDescription>
)}
<PageContent className="flex flex-col w-full h-full justify-between gap-6">
<AgentContentPage
agent={agent}
edgeKey={edgeKey}
/>
</PageContent>
</Page>
)
@@ -1,28 +1,21 @@
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 {AgentForm} from "@/components/wrappers/dashboard/agent/agent-form/agent-form";
import {Metadata} from "next";
export const metadata: Metadata = {
title: "Create Agent",
};
export default async function RoutePage(props: PageParams<{}>) {
const user = await currentUser();
if(user.role != "admin"){
notFound()
}
return (
<Page>
<PageHeader>
<PageTitle>
Create new agent
</PageTitle>
<PageTitle>Create new agent</PageTitle>
</PageHeader>
<PageContent>
<AgentForm/>
</PageContent>
</Page>
)
}
);
}
@@ -1,22 +1,41 @@
import {PageParams} from "@/types/next";
import {AgentCard} from "@/components/wrappers/dashboard/agent/AgentCard/AgentCard";
import {AgentCard} from "@/components/wrappers/dashboard/agent/agent-card/agent-card";
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
import {Button} from "@/components/ui/button";
import Link from 'next/link'
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 {db} from "@/db";
import * as drizzleDb from "@/db";
import {desc, eq, not} from "drizzle-orm";
import {EmptyStatePlaceholder} from "@/components/wrappers/common/empty-state-placeholder";
import {Metadata} from "next";
export const metadata: Metadata = {
title: "Agents",
};
export default async function RoutePage(props: PageParams<{}>) {
const agents = await prisma.agent.findMany()
const agents = await db.query.agent.findMany({
where: not(eq(drizzleDb.schemas.agent.isArchived, true)),
with: {
databases: true
},
orderBy: (fields) => desc(fields.createdAt),
});
console.log(agents);
if (!agents) {
notFound();
}
return (
<Page>
<PageHeader>
<PageTitle>
Agents
</PageTitle>
<PageTitle>Agents</PageTitle>
{agents.length > 0 && (
<PageActions>
<Link href={"/dashboard/agents/new"}>
@@ -25,22 +44,16 @@ export default async function RoutePage(props: PageParams<{}>) {
</PageActions>
)}
</PageHeader>
<PageContent className="mt-10">
{agents.length > 0 ?
<CardsWithPagination
data={agents}
cardItem={AgentCard}
cardsPerPage={4}
numberOfColumns={1}
<PageContent>
{agents.length > 0 ? (
<CardsWithPagination data={agents} cardItem={AgentCard} cardsPerPage={4} numberOfColumns={1}/>
) : (
<EmptyStatePlaceholder
url={"/dashboard/agents/new"}
text={"Create new Agent"}
/>
:
<Link
href={"/dashboard/agents/new"}
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
</Link>
}
)}
</PageContent>
</Page>
)
}
);
}
+9 -12
View File
@@ -1,16 +1,13 @@
import React from "react";
import {currentUser} 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 currentUser()
if(user.role != "admin"){
notFound()
if (user!.role !== "superadmin" && user!.role !== "admin") {
notFound();
}
return (
<>
{children}
</>
)
}
return <>{children}</>;
}
@@ -0,0 +1,46 @@
import {PageParams} from "@/types/next";
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {Metadata} from "next";
import {
NotificationChannelsSection
} from "@/components/wrappers/dashboard/admin/notifications/channels/notification-channels-section";
import {db} from "@/db";
import {notificationChannel, NotificationChannel, NotificationChannelWith} from "@/db/schema/09_notification-channel";
import {NotifierAddEditModal} from "@/components/wrappers/dashboard/common/notifier/notifier-add-edit-modal";
import {desc, isNull} from "drizzle-orm";
export const metadata: Metadata = {
title: "Notification Channels",
};
export default async function RoutePage(props: PageParams<{}>) {
const notificationChannels = await db.query.notificationChannel.findMany({
with: {
organizations: true
},
orderBy: desc(notificationChannel.createdAt)
}) as NotificationChannelWith[]
const organizations = await db.query.organization.findMany({
where: (fields) => isNull(fields.deletedAt),
with: {
members: true,
},
});
return (
<Page>
<PageHeader>
<PageTitle>Notification channels</PageTitle>
<PageActions>
<NotifierAddEditModal adminView={false}/>
</PageActions>
</PageHeader>
<PageContent>
<NotificationChannelsSection organizations={organizations} notificationChannels={notificationChannels}/>
</PageContent>
</Page>
);
}
@@ -0,0 +1,30 @@
import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {Metadata} from "next";
import {NotificationLogsList} from "@/components/wrappers/dashboard/admin/notifications/logs/notification-logs-list";
import {notFound} from "next/navigation";
import {getNotificationHistory} from "@/db/services/notification-log";
export const metadata: Metadata = {
title: "Activity logs",
};
export default async function RoutePage(props: PageParams<{}>) {
const notificationLogs = await getNotificationHistory()
if (!notificationLogs) {
notFound();
}
return (
<Page>
<PageHeader>
<PageTitle>Activity logs</PageTitle>
</PageHeader>
<PageContent>
<NotificationLogsList notificationLogs={notificationLogs} />
</PageContent>
</Page>
);
}
@@ -0,0 +1,36 @@
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/database-form/database-form";
import { db } from "@/db";
import * as drizzleDb from "@/db";
import { eq } from "drizzle-orm";
export default async function RoutePage(props: PageParams<{ databaseId: string }>) {
const { databaseId } = await props.params;
const dbItem = await db.query.database.findFirst({
where: eq(drizzleDb.schemas.database.id, databaseId),
});
if (!dbItem) {
notFound();
}
return (
<Page>
<PageHeader>
<PageTitle>Edit {dbItem.name}</PageTitle>
</PageHeader>
<PageContent>
<DatabaseForm
databaseId={databaseId}
// @ts-ignore
defaultValues={{ ...dbItem, dbms: dbItem.dbms ?? "inactive", description: dbItem.description ?? undefined }}
/>
</PageContent>
</Page>
);
}
@@ -0,0 +1,136 @@
import {PageParams} from "@/types/next";
import {notFound, redirect} from "next/navigation";
import {Page, 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/database-tabs";
import {DatabaseKpi} from "@/components/wrappers/dashboard/projects/database/database-kpi";
import {CronButton} from "@/components/wrappers/dashboard/database/cron-button/cron-button";
import {db} from "@/db";
import {eq, and, inArray} from "drizzle-orm";
import * as drizzleDb from "@/db";
import {getOrganizationProjectDatabases} from "@/lib/services";
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
import {RetentionPolicySheet} from "@/components/wrappers/dashboard/database/retention-policy/retention-policy-sheet";
import {capitalizeFirstLetter} from "@/utils/text";
import {AlertPolicyModal} from "@/components/wrappers/dashboard/database/alert-policy/alert-policy-modal";
import {getOrganizationChannels} from "@/db/services/notification-channel";
import {ImportModal} from "@/components/wrappers/dashboard/database/import/import-modal";
export default async function RoutePage(props: PageParams<{
projectId: string;
databaseId: string
}>) {
const {projectId, databaseId} = await props.params;
const organization = await getOrganization({});
const activeMember = await getActiveMember()
if (!organization || !activeMember) {
notFound();
}
const databasesProject = await getOrganizationProjectDatabases({
organizationSlug: organization.slug,
projectId: projectId
})
const dbItem = await db.query.database.findFirst({
where: and(inArray(drizzleDb.schemas.backup.id, databasesProject.ids ?? []), eq(drizzleDb.schemas.database.id, databaseId), eq(drizzleDb.schemas.database.projectId, projectId)),
with: {
project: true,
retentionPolicy: true,
alertPolicies: true
}
});
if (!dbItem) {
redirect("/dashboard/projects");
}
const backups = await db.query.backup.findMany({
where: eq(drizzleDb.schemas.backup.databaseId, dbItem.id),
with: {
restorations: true,
},
orderBy: (b, {desc}) => [desc(b.createdAt)],
});
const restorations = await db.query.restoration.findMany({
where: eq(drizzleDb.schemas.restoration.databaseId, dbItem.id),
orderBy: (r, {desc}) => [desc(r.createdAt)],
});
const isAlreadyBackup = backups.some((b) => b.status === "waiting");
const isAlreadyRestore = restorations.some((r) => r.status === "waiting");
const totalBackups = await db.select({count: drizzleDb.schemas.backup.id})
.from(drizzleDb.schemas.backup)
.where(eq(drizzleDb.schemas.backup.databaseId, dbItem.id))
.then(rows => rows.length);
const availableBackups = backups.filter(b => !b.deletedAt).length;
const successfulBackups = await db.select({count: drizzleDb.schemas.backup.id})
.from(drizzleDb.schemas.backup)
.where(and(
eq(drizzleDb.schemas.backup.databaseId, dbItem.id),
eq(drizzleDb.schemas.backup.status, "success")
))
.then(rows => rows.length);
const [settings] = await db.select().from(drizzleDb.schemas.setting).where(eq(drizzleDb.schemas.setting.name, "system")).limit(1);
if (!settings) {
notFound();
}
const organizationChannels = await getOrganizationChannels(organization.id);
const activeOrganizationChannels = organizationChannels.filter(channel => channel.enabled);
const successRate = totalBackups > 0 ? (successfulBackups / totalBackups) * 100 : null;
const isMember = activeMember?.role === "member";
return (
<Page>
<div className="justify-between gap-2 sm:flex">
<PageTitle className="flex flex-col md:flex-row items-center justify-between w-full ">
<div className="min-w-full md:min-w-fit ">
{capitalizeFirstLetter(dbItem.name)}
</div>
{!isMember && (
<div className="flex items-center gap-2 md:justify-between w-full ">
<div className="flex items-center gap-2">
{/* Do not delete*/}
{/*<EditButton/>*/}
<RetentionPolicySheet database={dbItem}/>
<CronButton database={dbItem}/>
<AlertPolicyModal database={dbItem} notificationChannels={activeOrganizationChannels}
organizationId={organization.id}/>
<ImportModal database={dbItem}/>
</div>
<div className="flex items-center gap-2">
<BackupButton disable={isAlreadyBackup} databaseId={databaseId}/>
</div>
</div>
)}
</PageTitle>
</div>
{dbItem.description && (
<PageDescription className="mt-5 sm:mt-0">{dbItem.description}</PageDescription>
)}
<PageContent className="flex flex-col w-full h-full">
<DatabaseKpi successRate={successRate} database={dbItem} availableBackups={availableBackups}
totalBackups={totalBackups}/>
<DatabaseTabs activeMember={activeMember} settings={settings} database={dbItem}
isAlreadyRestore={isAlreadyRestore}
backups={backups}
restorations={restorations}/>
</PageContent>
</Page>
);
}
@@ -0,0 +1,65 @@
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/project-form/project-form";
import * as drizzleDb from "@/db";
import {db} from "@/db";
import {eq} from "drizzle-orm";
import {DatabaseWith} from "@/db/schema/07_database";
export default async function RoutePage(props: PageParams<{ projectId: string }>) {
const {projectId} = await props.params;
const proj = await db.query.project.findFirst({
where: eq(drizzleDb.schemas.project.id, projectId),
with: {
databases: true,
},
});
if (!proj) {
notFound();
}
const org = await db.query.organization.findFirst({
where: eq(drizzleDb.schemas.organization.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.project !== null) as DatabaseWith[];
return (
<Page>
<PageHeader>
<PageTitle>Edit {proj.name}</PageTitle>
</PageHeader>
<PageContent>
<ProjectForm
organization={org}
databases={availableDatabases as DatabaseWith[]}
defaultValues={{...proj, databases: proj.databases.map((db) => db.id)}}
projectId={proj.id}
/>
</PageContent>
</Page>
);
}
@@ -0,0 +1,94 @@
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/button-delete-project/button-delete-project";
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
import {ProjectDatabaseCard} from "@/components/wrappers/dashboard/projects/project-card/project-database-card";
import {notFound, redirect} from "next/navigation";
import {db} from "@/db";
import {eq} from "drizzle-orm";
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
import * as drizzleDb from "@/db";
import {capitalizeFirstLetter} from "@/utils/text";
export default async function RoutePage(props: PageParams<{
projectId: string
}>) {
const {
projectId
} = await props.params;
const organization = await getOrganization({});
const activeMember = await getActiveMember()
if (!organization) {
notFound();
}
const org = await db.query.organization.findFirst({
where: eq(drizzleDb.schemas.organization.slug, organization.slug),
});
if (!org) notFound();
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,
},
});
if (!proj) {
redirect("/dashboard/projects");
}
const isMember = activeMember?.role === "member";
return (
<Page>
<div className="justify-between gap-2 sm:flex">
<PageTitle className="flex items-center">
{capitalizeFirstLetter(proj.name)}
{!isMember && (
<Link className={buttonVariants({variant: "outline"})}
href={`/dashboard/projects/${proj.id}/edit`}>
<GearIcon className="w-7 h-7"/>
</Link>
)}
</PageTitle>
{!isMember && (
<PageActions className="justify-between">
<ButtonDeleteProject projectId={projectId} text={"Delete Project"}/>
</PageActions>
)}
</div>
<PageDescription>The list of associated databases</PageDescription>
<PageContent className="flex flex-col w-full h-full">
{proj.databases.length > 0 ? (
<CardsWithPagination
data={proj.databases}
organizationSlug={organization.slug}
// @ts-ignore
cardItem={ProjectDatabaseCard}
cardsPerPage={4}
numberOfColumns={1}
extendedProps={proj}
/>
) : (
<div className="flex flex-col items-center justify-center h-full text-muted-foreground py-20">
<p className="text-lg font-medium">No databases found</p>
<p className="text-sm mt-2">You havent added any databases to this project yet.</p>
</div>
)}
</PageContent>
</Page>
);
}
@@ -0,0 +1,50 @@
import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {ProjectForm} from "@/components/wrappers/dashboard/projects/project-form/project-form";
import {notFound} from "next/navigation";
import {db} from "@/db";
import {eq} from "drizzle-orm";
import {getOrganization} from "@/lib/auth/auth";
import * as drizzleDb from "@/db";
import {DatabaseWith} from "@/db/schema/07_database";
export default async function RoutePage(props: PageParams<{}>) {
const organization = await getOrganization({});
if (!organization) {
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 org = await db.query.organization.findFirst({
where: eq(drizzleDb.schemas.organization.slug, organization.slug),
});
if (!org) notFound();
return (
<Page>
<PageHeader>
<PageTitle>Create new project</PageTitle>
</PageHeader>
<PageContent>
<ProjectForm databases={availableDatabases} organization={org}/>
</PageContent>
</Page>
);
}
@@ -0,0 +1,70 @@
import Link from "next/link";
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/project-card/project-card";
import {db} from "@/db";
import {notFound} from "next/navigation";
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
import {EmptyStatePlaceholder} from "@/components/wrappers/common/empty-state-placeholder";
import {Metadata} from "next";
export const metadata: Metadata = {
title: "Projects",
};
export default async function RoutePage(props: PageParams<{}>) {
const organization = await getOrganization({});
const activeMember = await getActiveMember()
if (!organization) {
notFound();
}
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,
},
});
const isMember = activeMember?.role === "member";
return (
<Page>
<PageHeader>
<PageTitle>Projects</PageTitle>
{(projects.length > 0 && !isMember) && (
<PageActions>
<Link href={`/dashboard/projects/new`}>
<Button>+ Create Project</Button>
</Link>
</PageActions>
)}
</PageHeader>
<PageContent>
{projects.length > 0 ? (
<CardsWithPagination
organizationSlug={organization.slug}
data={projects}
cardItem={ProjectCard}
cardsPerPage={4}
numberOfColumns={1}
/>
) : isMember ? (
<EmptyStatePlaceholder text="No project available"/>
) : (
<EmptyStatePlaceholder url="/dashboard/projects/new" text="Create new Project"/>
)}
</PageContent>
</Page>
);
}
@@ -0,0 +1,37 @@
import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {notFound} from "next/navigation";
import {OrganizationForm} from "@/components/wrappers/dashboard/organization/organization-form/organization-form";
import {getOrganization} from "@/lib/auth/auth";
import {db} from "@/db";
import {isNull} from "drizzle-orm";
import {currentUser} from "@/lib/auth/current-user";
export default async function RoutePage(props: PageParams<{
}>) {
const user = await currentUser();
const organization = await getOrganization({});
const users = await db.query.user.findMany({
where: (fields) => isNull(fields.deletedAt)
});
if (!user || !users || !organization || organization.slug == "default") {
notFound();
}
return(
<Page>
<PageHeader>
<PageTitle>
Edit {organization.name}
</PageTitle>
</PageHeader>
<PageContent>
<OrganizationForm currentUser={user} users={users} defaultValues={organization}/>
</PageContent>
</Page>
)
}
@@ -0,0 +1,58 @@
import {PageParams} from "@/types/next";
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {currentUser} from "@/lib/auth/current-user";
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
import {notFound} from "next/navigation";
import {
DeleteOrganizationButton
} from "@/components/wrappers/dashboard/organization/delete-organization/delete-organization-button";
import {EditButtonSettings} from "@/components/wrappers/dashboard/settings/edit-button-settings/edit-button-settings";
import {Metadata} from "next";
import {OrganizationTabs} from "@/components/wrappers/dashboard/organization/tabs/organization-tabs";
import {getOrganizationChannels} from "@/db/services/notification-channel";
import {computeOrganizationPermissions} from "@/lib/acl/organization-acl";
export const metadata: Metadata = {
title: "Settings",
};
export default async function RoutePage(props: PageParams<{ slug: string }>) {
const organization = await getOrganization({});
const user = await currentUser();
const activeMember = await getActiveMember()
if (!organization || !activeMember) {
notFound();
}
const notificationChannels = await getOrganizationChannels(organization.id)
const permissions = computeOrganizationPermissions(activeMember);
return (
<Page>
<PageHeader>
<PageTitle className="flex items-center">
Organization settings
{permissions.canManageSettings && organization.slug !== "default" && (
<EditButtonSettings/>
)}
</PageTitle>
<PageActions>
{permissions.canManageDangerZone && organization.slug !== "default" && (
<DeleteOrganizationButton organizationSlug={organization.slug}/>
)}
</PageActions>
</PageHeader>
<PageContent>
<OrganizationTabs
activeMember={activeMember}
organization={organization}
notificationChannels={notificationChannels}
/>
</PageContent>
</Page>
)
}
@@ -0,0 +1,135 @@
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 {notFound} from "next/navigation";
import {db} from "@/db";
import {and, asc, count, eq, inArray} from "drizzle-orm";
import * as drizzleDb from "@/db";
import {getOrganization} from "@/lib/auth/auth";
import {Building2, DatabaseBackup, Folder, RefreshCcw} from "lucide-react";
import {Metadata} from "next";
export const metadata: Metadata = {
title: "Statistics",
};
export default async function RoutePage(props: PageParams<{}>) {
const organization = await getOrganization({});
if (!organization) {
notFound();
}
const org = await db.query.organization.findFirst({
where: eq(drizzleDb.schemas.organization.slug, organization.slug),
});
if (!org) notFound();
const projects = await db.query.project.findMany({
where: eq(drizzleDb.schemas.project.organizationId, org.id),
});
const projectIds = projects.map(project => project.id);
const databasesOfAllProjects = await db.query.database.findMany({
where: inArray(drizzleDb.schemas.database.projectId, projectIds),
})
const databaseIds = databasesOfAllProjects.map((database) => database.id);
const backupsEvolution = await db.query.backup.findMany({
columns: {
id: true,
createdAt: true,
},
orderBy: [asc(drizzleDb.schemas.backup.id)],
where: inArray(drizzleDb.schemas.backup.databaseId, databaseIds),
});
const backupsRate = await db
.select({
createdAt: drizzleDb.schemas.backup.createdAt,
status: drizzleDb.schemas.backup.status,
_count: count(),
})
.from(drizzleDb.schemas.backup)
.where(and(inArray(drizzleDb.schemas.backup.status, ["success", "failed"]), inArray(drizzleDb.schemas.backup.databaseId, databaseIds)))
.groupBy(drizzleDb.schemas.backup.createdAt, drizzleDb.schemas.backup.status)
.orderBy(drizzleDb.schemas.backup.createdAt);
const restorationsCountResult = await db
.select({
count: count(),
})
.from(drizzleDb.schemas.restoration)
.where(inArray(drizzleDb.schemas.restoration.databaseId, databaseIds));
const restorationsCount = restorationsCountResult[0]?.count ?? 0;
const projectsCount = projects.length;
const backupsEvolutionCount = backupsEvolution.length;
const sortedBackupsEvolution = backupsEvolution.sort(
(a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime()
);
return (
<Page>
<PageHeader>
<PageTitle>Statistics Overview</PageTitle>
</PageHeader>
<PageContent className="flex flex-col gap-y-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Card className="w-full">
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium">Projects</CardTitle>
<Building2 className="h-4 w-4 text-muted-foreground"/>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{projectsCount}</div>
<p className="text-xs text-muted-foreground">Active projects in this organization</p>
</CardContent>
</Card>
<Card className="w-full">
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium">Backups</CardTitle>
<DatabaseBackup className="h-4 w-4 text-muted-foreground"/>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{backupsEvolutionCount}</div>
<p className="text-xs text-muted-foreground">Total backups executed across all databases</p>
</CardContent>
</Card>
<Card className="w-full">
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium">Restorations</CardTitle>
<RefreshCcw className="h-4 w-4 text-muted-foreground"/>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{restorationsCount}</div>
<p className="text-xs text-muted-foreground">Total restoration operations performed</p>
</CardContent>
</Card>
</div>
<div className="flex flex-col md:flex-row gap-4">
<EvolutionLineChart
data={sortedBackupsEvolution}
/>
<PercentageLineChart
data={backupsRate}/>
</div>
</PageContent>
</Page>
);
}
@@ -1,55 +0,0 @@
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";
export default async function RoutePage(props: PageParams<{
databaseId: string;
}>) {
const {databaseId} = await props.params
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) {
notFound();
}
return (
<Page>
<PageHeader>
<PageTitle>
Restore {database.name}
</PageTitle>
</PageHeader>
<PageContent>
<RestoreForm databaseToRestore={database} databases={databases} backups={backups}/>
</PageContent>
</Page>
)
}
@@ -1,17 +0,0 @@
import {PageParams} from "@/types/next";
import {redirect} from "next/navigation";
export default async function RoutePage(props: PageParams<{}>) {
// return (
// <div className="flex flex-1 flex-col gap-4 p-4">
// <div className="grid auto-rows-min gap-4 md:grid-cols-3">
// <div className="aspect-video rounded-xl bg-muted/50"/>
// <div className="aspect-video rounded-xl bg-muted/50"/>
// <div className="aspect-video rounded-xl bg-muted/50"/>
// </div>
// <div className="min-h-[100vh] flex-1 rounded-xl bg-muted/50 md:min-h-min"/>
// </div>
// )
}
@@ -1,39 +0,0 @@
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";
export default async function RoutePage(props: PageParams<{
databaseId: string;
}>) {
const {databaseId} = await props.params
const user = await requiredCurrentUser()
const database = await prisma.database.findUnique({
where: {
id: databaseId,
}
});
if (!database) {
notFound();
}
return (
<Page>
<PageHeader>
<PageTitle>
Edit {database.name}
</PageTitle>
</PageHeader>
<PageContent>
<DatabaseForm databaseId={databaseId} defaultValues={database}/>
</PageContent>
</Page>
)
}
@@ -1,91 +0,0 @@
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";
export default async function RoutePage(props: PageParams<{ databaseId: string }>) {
const {databaseId} = await props.params
const database = await prisma.database.findUnique({
where: {
id: databaseId,
},
})
if (!database) {
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 successfulBackups = await prisma.backup.count({
where: {
databaseId: databaseId,
status: 'success',
},
});
const successRate = totalBackups > 0 ? (successfulBackups / totalBackups) * 100 : null
return (
<Page>
<div className="justify-between gap-2 sm:flex">
<PageTitle className="flex items-center">
{database.name}
<EditButton/>
<CronButton database={database}/>
</PageTitle>
<PageActions className="justify-between">
<BackupButton disable={isAlreadyBackup} databaseId={databaseId}/>
</PageActions>
</div>
<PageDescription className="mt-5 sm:mt-0">{database.description}</PageDescription>
<PageContent className="flex flex-col w-full h-full">
<DatabaseKpi
successRate={successRate}
database={database}
totalBackups={totalBackups}
/>
<DatabaseTabs
database={database}
isAlreadyRestore={isAlreadyRestore}
backups={backups}
restorations={restorations}
/>
</PageContent>
</Page>
)
}
@@ -1,61 +0,0 @@
import {notFound} from "next/navigation";
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";
export default async function RoutePage(props: PageParams<{
projectId: string;
}>) {
const {projectId} = await props.params
const project = await prisma.project.findUnique({
where: {
id: projectId,
},
include: {
databases: {}
}
});
if (!project) {
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',
},
})
return (
<Page>
<PageHeader>
<PageTitle>
Edit {project.name}
</PageTitle>
</PageHeader>
<PageContent>
<ProjectForm organization={organization} databases={availableDatabases} defaultValues={project}
projectId={project.id}/>
</PageContent>
</Page>
)
}
@@ -1,76 +0,0 @@
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 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";
export default async function RoutePage(props: PageParams<{ slug:string, projectId: string }>) {
const {slug: organizationSlug, projectId} = await props.params
const currentOrganizationSlug = await getCurrentOrganizationSlug()
if(currentOrganizationSlug != organizationSlug) {
notFound()
}
const project = await prisma.project.findUnique({
where: {
id: projectId,
},
include:{
databases: {}
}
})
if(!project){
notFound()
}
return (
<Page>
<div className="justify-between gap-2 sm:flex">
<PageTitle className="flex items-center">
{project.name}
<Link className={buttonVariants({variant: "outline"})}
href={`/dashboard/${currentOrganizationSlug}/projects/${project.id}/edit`}>
<GearIcon className="w-7 h-7"/>
</Link>
</PageTitle>
<PageActions className="justify-between">
<ButtonDeleteProject
projectId={projectId}
text={"Delete Project"}
/>
</PageActions>
</div>
<PageDescription>
The list of associated databases
</PageDescription>
<PageContent className="flex flex-col w-full h-full">
{project.databases.length > 0 ?
<CardsWithPagination
data={project.databases}
organizationSlug={organizationSlug}
cardItem={ProjectDatabaseCard}
cardsPerPage={4}
numberOfColumns={1}
extendedProps={project}
/>
:
null
}
</PageContent>
</Page>
)
}
@@ -1,47 +0,0 @@
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";
export default async function RoutePage(props: PageParams<{slug: string}>) {
const {slug: organizationSlug} = await props.params
const currentOrganizationSlug = await getCurrentOrganizationSlug()
if(currentOrganizationSlug != organizationSlug) {
notFound()
}
const availableDatabases = await prisma.database.findMany({
where: {
projectId: null
},
include: {
agent: {}
},
orderBy: {
createdAt: 'desc',
},
})
const organization = await prisma.organization.findFirst({
where: {
slug: currentOrganizationSlug,
}
})
return (
<Page>
<PageHeader>
<PageTitle>
Create new project
</PageTitle>
</PageHeader>
<PageContent>
<ProjectForm databases={availableDatabases} organization={organization}/>
</PageContent>
</Page>
)
}
@@ -1,79 +0,0 @@
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 {requiredCurrentUser} from "@/auth/current-user";
import {getCurrentOrganizationSlug} from "@/features/dashboard/organization-cookie";
import {currentOrganization} from "@/auth/current-organization";
import {notFound} from "next/navigation";
export default async function RoutePage(props: PageParams<{slug: string}>) {
const {slug: organizationSlug} = await props.params
const user = await requiredCurrentUser()
const currentOrganizationSlug = await getCurrentOrganizationSlug()
if(currentOrganizationSlug != organizationSlug) {
notFound()
}
const projects = await prisma.project.findMany({
where: {
organization: {
slug: organizationSlug,
// users: {
// some: {
// userId: user.id
// },
// },
}
},
include: {
databases: {}
}
})
return (
<Page>
<PageHeader>
<PageTitle>
Projects
</PageTitle>
{projects.length > 0 && (
<PageActions>
<Link href={`/dashboard/${currentOrganizationSlug}/projects/new`}>
<Button>+ Create Project</Button>
</Link>
</PageActions>
)}
</PageHeader>
<PageContent className="mt-10">
{projects.length > 0 ?
<CardsWithPagination
organizationSlug={currentOrganizationSlug}
data={projects}
cardItem={ProjectCard}
cardsPerPage={4}
numberOfColumns={1}
/>
:
<Link
href={`/dashboard/${currentOrganizationSlug}/projects/new`}
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
</Link>
}
</PageContent>
</Page>
)
}
@@ -1,62 +0,0 @@
import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {getCurrentOrganizationSlug} from "@/features/dashboard/organization-cookie";
import {prisma} from "@/prisma";
import {requiredCurrentUser} from "@/auth/current-user";
import {notFound} from "next/navigation";
import {OrganizationForm} from "@/components/wrappers/dashboard/organization/OrganizationForm/OrganizationForm";
export default async function RoutePage(props: PageParams<{
slug: string;
}>) {
const {slug: organizationSlug} = await props.params
const currentOrganizationSlug = await getCurrentOrganizationSlug()
const user = await requiredCurrentUser()
if(currentOrganizationSlug != organizationSlug) {
notFound()
}
const organization = await prisma.organization.findUnique({
where:{
slug: currentOrganizationSlug,
},
include:{
users:{
include:{
user:{
}
},
where: {
user: {
id: {
not: user.id, // Exclude user with id: 1
},
},
},
}
}
})
const users = await prisma.user.findMany({
where: {
deleted: {not: true},
id: {not: user.id}
}
})
return(
<Page>
<PageHeader>
<PageTitle>
Edit {organization.name}
</PageTitle>
</PageHeader>
<PageContent>
<OrganizationForm users={users} defaultValues={organization}/>
</PageContent>
</Page>
)
}
@@ -1,74 +0,0 @@
import {prisma} from "@/prisma";
import {PageParams} from "@/types/next";
import {Page, PageActions, PageContent, PageDescription, PageHeader, PageTitle} from "@/features/layout/page";
import {requiredCurrentUser} from "@/auth/current-user";
import {SettingsTabs} from "@/components/wrappers/dashboard/settings/SettingsTabs/SettingsTabs";
import {getCurrentOrganizationSlug} from "@/features/dashboard/organization-cookie";
import {
DeleteOrganizationButton
} from "@/components/wrappers/dashboard/organization/DeleteOrganization/DeleteOrganizationButton";
import {EditButtonSettings} from "@/components/wrappers/dashboard/settings/EditButtonSettings/EditButtonSettings";
import {notFound} from "next/navigation";
export default async function RoutePage(props: PageParams<{}>) {
const user = await requiredCurrentUser()
const currentOrganizationSlug = await getCurrentOrganizationSlug()
const organization = await prisma.organization.findUnique({
where: {
slug: currentOrganizationSlug,
},
include: {
users:{
include:{
user: {}
}
}
}
})
const currentOrganizationUser = await prisma.userOrganization.findFirst({
where:{
userId: user.id,
organization:{
slug: currentOrganizationSlug != "" ? currentOrganizationSlug : "default",
}
}
})
if (currentOrganizationUser.role != "admin") {
notFound()
}
const settings = await prisma.settings.findUnique({
where: {
name: "system"
}
})
return (
<Page>
<PageHeader>
<PageTitle className="flex items-center">
Settings
{organization.slug != "default" ?
<EditButtonSettings/>
: null}
</PageTitle>
<PageActions>
{organization.slug != "default" ?
<DeleteOrganizationButton organizationSlug={currentOrganizationSlug}/>
: null}
</PageActions>
</PageHeader>
<PageDescription>
Manage your organization settings.
</PageDescription>
<PageContent>
<SettingsTabs settings={settings} currentUser={user} users={organization.users}/>
</PageContent>
</Page>
)
}
@@ -1,93 +0,0 @@
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";
export default async function RoutePage(props: PageParams<{}>) {
const projectsCount = await prisma.project.count({
where: {
organization: {
slug: "default"
}
},
});
const backupsEvolution = await prisma.backup.findMany({
select: {
createdAt: true,
},
orderBy: {
createdAt: "asc",
},
});
const backupsRate = await prisma.backup.groupBy({
by: ['createdAt', 'status'],
where: {
status: {
in: ['success', 'failed'],
},
},
_count: {
id: true,
},
});
return (
<Page>
<PageHeader>
<PageTitle>
Statistics
</PageTitle>
</PageHeader>
<PageContent className="flex flex-col gap-y-4">
<div className="flex flex-col md:flex-row gap-4">
<Card className="w-full">
<CardHeader>
<CardTitle>Projects</CardTitle>
</CardHeader>
<CardContent>
{projectsCount}
</CardContent>
</Card>
<Card className="w-full">
<CardHeader>
<CardTitle>KPI 2</CardTitle>
</CardHeader>
<CardContent>
</CardContent>
</Card>
<Card className="w-full">
<CardHeader>
<CardTitle>KPI 3</CardTitle>
</CardHeader>
<CardContent>
</CardContent>
</Card>
</div>
<div className="flex flex-col md:flex-row gap-4">
<Card className="w-full">
<CardHeader>
<CardTitle>Evolution of the number of backups</CardTitle>
</CardHeader>
<CardContent>
<EvolutionLineChart data={backupsEvolution}/>
</CardContent>
</Card>
<Card className="w-full">
<CardHeader>
<CardTitle>Success rate of backups</CardTitle>
</CardHeader>
<CardContent>
<PercentageLineChart data={backupsRate}/>
</CardContent>
</Card>
</div>
</PageContent>
</Page>
)
}
+139
View File
@@ -0,0 +1,139 @@
import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
import {Building2, Database, DatabaseBackup, Folder, RefreshCcw, Server, Workflow} from "lucide-react";
import {currentUser} from "@/lib/auth/current-user";
import {notFound} from "next/navigation";
import {db} from "@/db";
import {asc, inArray} from "drizzle-orm";
import * as drizzleDb from "@/db";
import {listOrganizations} from "@/lib/auth/auth";
import {Metadata} from "next";
export const metadata: Metadata = {
title: "Home",
};
export default async function RoutePage(props: PageParams<{}>) {
const user = await currentUser();
const organizations = await listOrganizations()
if (!user || !organizations) notFound();
const organizationIds = organizations.map(project => project.id);
const agents = await db.query.agent.findMany({});
const projects = await db.query.project.findMany({
where: inArray(drizzleDb.schemas.project.organizationId, organizationIds),
});
const projectIds = projects.map(project => project.id);
const databasesOfAllProjects = await db.query.database.findMany({
where: inArray(drizzleDb.schemas.database.projectId, projectIds),
})
const databaseIds = databasesOfAllProjects.map((database) => database.id);
const backupsEvolution = await db.query.backup.findMany({
columns: {
id: true,
createdAt: true,
deletedAt: true,
},
orderBy: [asc(drizzleDb.schemas.backup.id)],
where: inArray(drizzleDb.schemas.backup.databaseId, databaseIds),
});
const availableBackups = backupsEvolution.filter(backup => backup.deletedAt == null);
return (
<Page>
<PageHeader>
<PageTitle>Dashboard</PageTitle>
</PageHeader>
<PageContent className="flex flex-col gap-y-4">
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<Card className="w-full">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Organizations</CardTitle>
<Building2 className="h-4 w-4 text-muted-foreground"/>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{organizations.length}</div>
<p className="text-xs text-muted-foreground">Number of organizations</p>
</CardContent>
</Card>
<Card className="w-full">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Projects</CardTitle>
<Folder className="h-4 w-4 text-muted-foreground"/>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{projects.length}</div>
<p className="text-xs text-muted-foreground">Number of projects</p>
</CardContent>
</Card>
<Card className="w-full">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Databases</CardTitle>
<Database className="h-4 w-4 text-muted-foreground"/>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{databasesOfAllProjects.length}</div>
<p className="text-xs text-muted-foreground">Databases across all projects</p>
</CardContent>
</Card>
<Card className="w-full">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Agents</CardTitle>
<Workflow className="h-4 w-4 text-muted-foreground"/>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{agents.length}</div>
<p className="text-xs text-muted-foreground">Registered agents</p>
</CardContent>
</Card>
<Card className="w-full">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Total Backups</CardTitle>
<DatabaseBackup className="h-4 w-4 text-muted-foreground"/>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{backupsEvolution.length}</div>
<p className="text-xs text-muted-foreground">All backups recorded</p>
</CardContent>
</Card>
<Card className="w-full">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Available Backups</CardTitle>
<RefreshCcw className="h-4 w-4 text-muted-foreground"/>
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{availableBackups.length}</div>
<p className="text-xs text-muted-foreground">Currently active backups</p>
</CardContent>
</Card>
</div>
{/*Do not delete*/}
{/*<div className="flex flex-1 flex-col gap-4">*/}
{/* <div className="grid auto-rows-min gap-4 md:grid-cols-3">*/}
{/* <div className="aspect-video rounded-xl bg-muted/50"/>*/}
{/* <div className="aspect-video rounded-xl bg-muted/50"/>*/}
{/* <div className="aspect-video rounded-xl bg-muted/50"/>*/}
{/* </div>*/}
{/*</div>*/}
</PageContent>
</Page>
);
}
+20 -39
View File
@@ -1,45 +1,26 @@
import React from "react";
import React, {ReactNode} from "react";
import {redirect} from "next/navigation";
import {SidebarInset, SidebarProvider} from "@/components/ui/sidebar"
import {AppSidebar} from "@/components/wrappers/dashboard/sideBar/app-sidebar";
import {currentUser} from "@/auth/current-user";
import {SidebarInset, SidebarProvider} from "@/components/ui/sidebar";
import {AppSidebar} from "@/components/wrappers/dashboard/common/sidebar/app-sidebar";
import {Header} from "@/features/layout/Header";
import {prisma} from "@/prisma";
import {GlobalStoreProvider} from "@/state-management/provider";
import {currentUser} from "@/lib/auth/current-user";
import {ThemeMetaUpdater} from "@/features/browser/theme-meta-updater";
export default async function Layout({children}: { children: React.ReactNode }) {
const user = await currentUser()
if (user) {
const userInfo = await prisma.user.findUnique({
where: {
email: user?.email
}
})
if (!userInfo) redirect('/login')
}
if (!user) redirect('/login')
export default async function Layout({children}: { children: ReactNode }) {
const user = await currentUser();
if (!user) redirect("/login");
return (
// <HydrationZustand>
// <GlobalStoreProvider>
<>
<SidebarProvider>
<div className="flex flex-col lg:flex-row w-full">
<AppSidebar/>
<SidebarInset>
<Header/>
<main className="h-full">
{children}
</main>
</SidebarInset>
</div>
</SidebarProvider>
</>
// </GlobalStoreProvider>
// </HydrationZustand>
)
}
<SidebarProvider>
<div className="flex flex-col lg:flex-row w-full">
<ThemeMetaUpdater/>
<AppSidebar/>
<SidebarInset>
<Header/>
<main className="h-full">{children}</main>
</SidebarInset>
</div>
</SidebarProvider>
);
}
+3 -3
View File
@@ -1,9 +1,9 @@
import {LoadingSpinner} from "@/components/wrappers/common/loading/LoadingSpinner";
import { LoadingSpinner } from "@/components/wrappers/common/loading/loading-spinner";
export default function Loading() {
return (
<div className="w-full h-full flex flex-col items-center justify-center">
<LoadingSpinner size={50} />
</div>
)
}
);
}
-44
View File
@@ -1,44 +0,0 @@
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";
export default async function RoutePage(props: PageParams<{}>) {
const user = await requiredCurrentUser()
if (!user) {
notFound()
}
const userInfo = await prisma.user.findUnique({
where: {
email: user.email
}
})
return (
<Page>
<div className="justify-between gap-2 sm:flex">
<PageTitle className="flex items-center">
<AvatarWithUpload user={userInfo}/>
{user.name}
<Badge className="ml-3 hidden lg:block">{userInfo.authMethod}</Badge>
<Badge className="ml-3 hidden lg:block">{userInfo.role}</Badge>
</PageTitle>
<PageActions className="mt-2 hidden sm:block">
<ButtonDeleteAccount text="Delete my account"/>
</PageActions>
</div>
<PageContent>
<UserForm userId={userInfo.id} defaultValues={userInfo}/>
<div className="mt-4 sm:hidden">
<ButtonDeleteAccount text="Delete my account" />
</div>
</PageContent>
</Page>
)
}
+5 -5
View File
@@ -1,7 +1,7 @@
export default function Home() {
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<h1 className="text-3xl font-bold text-4xl leading-tight">Home landing page</h1>
</div>
);
return (
<div className="grid grid-rows-[20px_1fr_20px] items-center justify-items-center min-h-screen p-8 pb-20 gap-16 sm:p-20 font-[family-name:var(--font-geist-sans)]">
<h1 className="font-bold text-4xl leading-tight">Home landing page</h1>
</div>
);
}
+7 -9
View File
@@ -1,16 +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/home`);
}
redirect("/login")
redirect("/login");
//Do not delete
// return (
// <Home/>
+54
View File
@@ -1,2 +1,56 @@
import fs from "node:fs";
import forge from "node-forge";
import {EventPayload} from "@/features/notifications/types";
import {dispatchNotification} from "@/features/notifications/dispatch";
import {Database, DatabaseWith} from "@/db/schema/07_database";
export async function decryptedDump(file: File, aesKeyHex: string, ivHex: string, fileExtension: string): Promise<File> {
const privateKeyPem = fs.readFileSync("private/keys/server_private.pem", "utf8");
const privateKey = forge.pki.privateKeyFromPem(privateKeyPem);
// Decrypt AES key with RSA-OAEP
const encryptedAesKey = forge.util.hexToBytes(aesKeyHex);
const aesKey = privateKey.decrypt(encryptedAesKey, "RSA-OAEP", {
md: forge.md.sha256.create(),
mgf1: {md: forge.md.sha256.create()},
});
// Read encrypted file content
const encryptedBuffer = Buffer.from(await file.arrayBuffer());
const iv = forge.util.hexToBytes(ivHex);
// AES decryption
const decipher = forge.cipher.createDecipher("AES-CBC", aesKey);
decipher.start({iv});
decipher.update(forge.util.createBuffer(encryptedBuffer.toString("binary")));
const success = decipher.finish();
if (!success) {
throw new Error("Decryption failed");
}
const decryptedBytes = decipher.output.getBytes();
const decryptedBuffer = Buffer.from(decryptedBytes, "binary");
// Return a File so you can use file.arrayBuffer() later
return new File(
[decryptedBuffer],
file.name.replace(/\.enc$/, fileExtension),
{type: "application/octet-stream"}
);
}
export function getFileExtension(dbType: string) {
switch (dbType) {
case "postgresql":
return ".dump";
case "mysql":
return ".sql";
default:
return ".dump";
}
}
+101 -57
View File
@@ -1,88 +1,103 @@
import {NextResponse} from "next/server";
import {prisma} from "@/prisma";
import {isUuidv4} from "@/utils/verify-uuid";
import {uploadLocalPrivate} from "@/features/upload/private/upload.action";
import {uploadLocalPrivate, uploadS3Private} from "@/features/upload/private/upload.action";
import {v4 as uuidv4} from "uuid";
import {Backup, Database} from "@prisma/client";
import {eventEmitter} from "../../../events/route";
import * as drizzleDb from "@/db";
import {db} from "@/db";
import {Backup} from "@/db/schema/07_database";
import {and, eq} from "drizzle-orm";
import {env} from "@/env.mjs";
import {withUpdatedAt} from "@/db/utils";
import {decryptedDump, getFileExtension} from "./helpers";
import {sendNotificationsBackupRestore} from "@/features/notifications/helpers";
export async function POST(
request: Request,
{ params }: { params: Promise<{ agentId: string }> }
{params}: { params: Promise<{ agentId: string }> }
) {
try {
const contentType = request.headers.get("Content-Type");
if (!contentType || !contentType.includes("multipart/form-data")) {
return NextResponse.json(
{ error: "Unsupported or missing Content-Type" },
{ status: 400 }
{error: "Unsupported or missing Content-Type"},
{status: 400}
);
}
eventEmitter.emit('modification', { update: true });
eventEmitter.emit('modification', {update: true});
const agentId = (await params).agentId;
const formData = await request.formData();
const aesKeyHex = formData.get("aes_key") as string;
const ivHex = formData.get("iv") as string;
const generatedId = formData.get("generatedId") as string | null;
const method = formData.get("method") as string | null;
if (!generatedId || !isUuidv4(generatedId)) {
return NextResponse.json(
{ error: "generatedId is not a valid UUID" },
{ status: 400 }
{error: "generatedId is not a valid UUID"},
{status: 400}
);
}
const agent = await prisma.agent.findFirst({
where: { id: agentId },
const agent = await db.query.agent.findFirst({
where: eq(drizzleDb.schemas.agent.id, agentId),
});
if (!agent) {
return NextResponse.json(
{ error: "Agent not found" },
{ status: 404 }
{error: "Agent not found"},
{status: 404}
);
}
const database = await prisma.database.findFirst({
where: { generatedId },
const database = await db.query.database.findFirst({
where: eq(drizzleDb.schemas.database.agentDatabaseId, generatedId),
with: {
project: true,
alertPolicies: true
}
});
if (!database) {
return NextResponse.json(
{ error: "Database associated with generatedId not found" },
{ status: 404 }
{error: "Database associated with generatedId not found"},
{status: 404}
);
}
let backup: Backup | null = null;
let backup: Backup | null | undefined = null;
if (method === "automatic") {
backup = await prisma.backup.create({
data: {
status: "ongoing",
[backup] = await db
.insert(drizzleDb.schemas.backup)
.values({
status: 'ongoing',
databaseId: database.id,
},
});
})
.returning();
if (!backup) {
return NextResponse.json(
{ error: "Unable to create an automatic backup" },
{ status: 500 }
{error: "Unable to create an automatic backup"},
{status: 500}
);
}
} else {
backup = await prisma.backup.findFirst({
where: {
status: "ongoing",
databaseId: database.id,
},
backup = await db.query.backup.findFirst({
where: and(
eq(drizzleDb.schemas.backup.status, 'ongoing'),
eq(drizzleDb.schemas.backup.databaseId, database.id),
),
});
if (!backup) {
return NextResponse.json(
{ error: "Unable to find the corresponding backup" },
{ status: 404 }
{error: "Unable to find the corresponding backup"},
{status: 404}
);
}
}
@@ -91,61 +106,90 @@ export async function POST(
if (status === "success") {
const file = formData.get("file") as File | null;
const extension = formData.get("extension") as string | null;
if (!aesKeyHex || !ivHex) {
return NextResponse.json({error: "Missing fields"}, {status: 400});
}
if (!file) {
return NextResponse.json(
{ error: "File is required for successful backup" },
{ status: 400 }
{error: "File is required for successful backup"},
{status: 400}
);
}
const fileSizeBytes = file.size;
// const fileExtension = '.' + (file.name.split('.').pop()?.toLowerCase() || '');
const fileExtension = extension ? extension : getFileExtension(database.dbms)
const decryptedFile = await decryptedDump(file, aesKeyHex, ivHex, fileExtension);
const uuid = uuidv4();
const fileName = `${uuid}.dump`;
const buffer = Buffer.from(await file.arrayBuffer());
const fileName = `${uuid}${fileExtension}`;
const buffer = Buffer.from(await decryptedFile.arrayBuffer());
const [settings] = await db.select().from(drizzleDb.schemas.setting).where(eq(drizzleDb.schemas.setting.name, "system")).limit(1);
if (!settings) {
throw new Error("System settings not found.");
}
const { success, message, filePath } = await uploadLocalPrivate(fileName, buffer);
let success: boolean, message: string, filePath: string;
const result =
settings.storage === "local"
? await uploadLocalPrivate(fileName, buffer)
: await uploadS3Private(`${database.project?.slug}/${fileName}`, buffer, env.S3_BUCKET_NAME!);
({success, message, filePath} = result);
if (!success) {
return NextResponse.json(
{ error: message },
{ status: 500 }
{error: message},
{status: 500}
);
}
await prisma.backup.update({
where: { id: backup.id },
data: {
await db
.update(drizzleDb.schemas.backup)
.set(withUpdatedAt({
file: fileName,
status: "success",
},
});
eventEmitter.emit('modification', { update: true });
fileSize: fileSizeBytes,
status: 'success',
}))
.where(eq(drizzleDb.schemas.backup.id, backup.id));
eventEmitter.emit('modification', {update: true});
await sendNotificationsBackupRestore(database, "success_backup");
return NextResponse.json(
{
message: "Backup successfully uploaded",
},
{ status: 200 }
{status: 200}
);
} else {
await prisma.backup.update({
where: { id: backup.id },
data: { status: "failed" },
});
eventEmitter.emit('modification', { update: true });
await db
.update(drizzleDb.schemas.backup)
.set(withUpdatedAt({status: 'failed'}))
.where(eq(drizzleDb.schemas.backup.id, backup.id));
eventEmitter.emit('modification', {update: true});
await sendNotificationsBackupRestore(database, "error_backup");
return NextResponse.json(
{
message: "Backup successfully updated with status failed",
},
{ status: 200 }
{status: 200}
);
}
} catch (error) {
console.error("Error in POST handler:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
{error: "Internal server error"},
{status: 500}
);
}
}
}
+25 -31
View File
@@ -1,14 +1,16 @@
import {NextResponse} from "next/server";
import {isUuidv4} from "@/utils/verify-uuid";
import {prisma} from "@/prisma";
import {eventEmitter} from "../../../events/route";
import * as drizzleDb from "@/db";
import {db} from "@/db";
import {and, eq} from "drizzle-orm";
import {sendNotificationsBackupRestore} from "@/features/notifications/helpers";
export type BodyResultRestore = {
generatedId: string
status: string
}
type RestorationStatus = 'waiting' | 'ongoing' | 'failed' | 'success';
export async function POST(
@@ -17,14 +19,12 @@ export async function POST(
) {
try {
eventEmitter.emit('modification', { update: true });
eventEmitter.emit('modification', {update: true});
const agentId = (await params).agentId
const body: BodyResultRestore = await request.json();
console.log(body)
console.log(agentId)
if (!isUuidv4(body.generatedId)) {
return NextResponse.json(
@@ -33,18 +33,17 @@ export async function POST(
);
}
const agent = await prisma.agent.findFirst({
where: {
id: agentId
}
const agent = await db.query.agent.findFirst({
where: eq(drizzleDb.schemas.agent.id, agentId)
})
if (!agent) {
return NextResponse.json({error: "Agent not found"}, {status: 404})
}
const database = await prisma.database.findFirst({
where: {
generatedId: body.generatedId
const database = await db.query.database.findFirst({
where: eq(drizzleDb.schemas.database.agentDatabaseId, body.generatedId),
with: {
alertPolicies: true
}
})
@@ -52,36 +51,31 @@ export async function POST(
return NextResponse.json({error: "Database associated with generatedId provided not found"}, {status: 404})
}
const restoration = await prisma.restoration.findFirst({
where: {
status : "ongoing",
databaseId: database.id
}
const restoration = await db.query.restoration.findFirst({
where: and(eq(drizzleDb.schemas.restoration.status, "ongoing"), eq(drizzleDb.schemas.restoration.databaseId, database.id),)
})
if (!restoration) {
return NextResponse.json({error: "Unable to fin the corresponding restoration"}, {status: 404})
}
await prisma.restoration.update({
where:{
id : restoration.id
},
data: {
status: body.status
}
})
await db
.update(drizzleDb.schemas.restoration)
.set({status: body.status as RestorationStatus})
.where(eq(drizzleDb.schemas.restoration.id, restoration.id));
await sendNotificationsBackupRestore(database, body.status == "error" ? "error_restore" : "success_restore");
const response = {
message: true,
details: "Restoration successfully updated"
}
eventEmitter.emit('modification', { update: true });
return Response.json(response , {status: 200})
eventEmitter.emit('modification', {update: true});
return Response.json(response, {status: 200})
} catch (error) {
console.error('Error in POST handler:', error);
return NextResponse.json(
+113 -65
View File
@@ -1,16 +1,23 @@
import {Agent, Database} from "@prisma/client";
import {prisma} from "@/prisma";
import {NextResponse} from "next/server";
import {Body} from "./route";
import {isUuidv4} from "@/utils/verify-uuid";
import {getFileUrlPresignedLocal} from "@/features/upload/private/upload.action";
import {getFileUrlPresignedLocal, getFileUrlPreSignedS3Action} from "@/features/upload/private/upload.action";
import {Agent} from "@/db/schema/08_agent";
import {Database} from "@/db/schema/07_database";
import * as drizzleDb from "@/db";
import {db as dbClient} from "@/db";
import {and, eq} from "drizzle-orm";
import {EDbmsSchema} from "@/db/schema/types";
import {ServerActionResult} from "@/types/action-type";
import {SafeActionResult} from "next-safe-action";
import {ZodString} from "zod";
import {withUpdatedAt} from "@/db/utils";
export async function handleDatabases(body: Body, agent: Agent, lastContact: Date) {
const databasesResponse = [];
const formatDatabase = (database: Database, backupAction: boolean, restoreAction: boolean, UrlBackup: string) => ({
generatedId: database.generatedId,
generatedId: database.agentDatabaseId,
dbms: database.dbms,
data: {
backup: {
@@ -25,97 +32,138 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
});
for (const db of body.databases) {
const existingDatabase = await prisma.database.findFirst({
where: {
generatedId: db.generatedId,
},
const existingDatabase = await dbClient.query.database.findFirst({
where: eq(drizzleDb.schemas.database.agentDatabaseId, db.generatedId)
});
let backupAction: boolean = false
let restoreAction: boolean = false
let UrlBackup: string = null
let urlBackup: string = ""
if (!existingDatabase) {
if (!isUuidv4(db.generatedId)) {
return NextResponse.json(
{ error: "generatedId is not a valid uuid" },
{ status: 500 }
{error: "generatedId is not a valid uuid"},
{status: 500}
);
}
const databaseCreated = await prisma.database.create({
data: {
const [databaseCreated] = await dbClient
.insert(drizzleDb.schemas.database)
.values({
agentId: agent.id,
name: db.name,
dbms: db.dbms,
generatedId: db.generatedId,
dbms: db.dbms as EDbmsSchema,
agentDatabaseId: db.generatedId,
lastContact: lastContact,
},
});
})
.returning();
if (databaseCreated) {
databasesResponse.push(formatDatabase(databaseCreated, backupAction,restoreAction, UrlBackup));
databasesResponse.push(formatDatabase(databaseCreated, backupAction, restoreAction, urlBackup));
}
} else {
const databaseUpdated = await prisma.database.update({
where: {
id: existingDatabase.id,
},
data: {
lastContact: lastContact,
},
});
const backup = await prisma.backup.findFirst({
where: {
databaseId: databaseUpdated.id,
status: "waiting"
}
})
const [databaseUpdated] = await dbClient
.update(drizzleDb.schemas.database)
.set(withUpdatedAt({
name: db.name,
agentId: agent.id,
lastContact: lastContact
}))
.where(eq(drizzleDb.schemas.database.id, existingDatabase.id))
.returning();
const restoration = await prisma.restoration.findFirst({
where:{
databaseId: databaseUpdated.id,
status: "waiting"
}
const backup = await dbClient.query.backup.findFirst({
where: and(eq(drizzleDb.schemas.backup.databaseId, databaseUpdated.id), eq(drizzleDb.schemas.backup.status, "waiting"))
})
if(backup){
const restoration = await dbClient.query.restoration.findFirst({
where: and(eq(drizzleDb.schemas.restoration.databaseId, databaseUpdated.id), eq(drizzleDb.schemas.restoration.status, "waiting"))
})
if (backup) {
backupAction = true
await prisma.backup.update({
where:{
id: backup.id
},
data: {
status: "ongoing"
}
})
await dbClient
.update(drizzleDb.schemas.backup)
.set(withUpdatedAt({status: "ongoing"}))
.where(eq(drizzleDb.schemas.backup.id, backup.id));
}
if(restoration){
if (restoration) {
restoreAction = true
const backupToRestore = await prisma.backup.findFirst({
where:{
id: restoration.backupId
const backupToRestore = await dbClient.query.backup.findFirst({
where: eq(drizzleDb.schemas.backup.id, restoration.backupId),
with: {
database: {
with: {
project: true
}
}
}
})
const fileName = backupToRestore.file
UrlBackup = await getFileUrlPresignedLocal(fileName)
await prisma.restoration.update({
where: {
id: restoration.id
},
data:{
status: "ongoing"
const [settings] = await dbClient.select().from(drizzleDb.schemas.setting).where(eq(drizzleDb.schemas.setting.name, "system")).limit(1);
if (!settings) {
return NextResponse.json(
{error: "Unable to find settings"},
{status: 500}
);
}
const fileName = backupToRestore?.file
let data: SafeActionResult<string, ZodString, readonly [], {
_errors?: string[] | undefined;
}, readonly [], ServerActionResult<string>, object> | undefined
try {
if (settings.storage == "local") {
data = await getFileUrlPresignedLocal({fileName: fileName!})
} else if (settings.storage == "s3") {
data = await getFileUrlPreSignedS3Action(`backups/${backupToRestore?.database.project?.slug}/${fileName}`);
}
})
if (data?.data?.success) {
urlBackup = data.data.value ?? "";
} else {
await dbClient
.update(drizzleDb.schemas.restoration)
.set({status: "failed"})
.where(eq(drizzleDb.schemas.restoration.id, restoration.id));
// @ts-ignore
const errorMessage = data?.data?.actionError?.message || "Failed to get presigned URL";
console.error("Restoration failed: ", errorMessage);
continue;
}
} catch (err) {
console.error("Restoration crashed unexpectedly:", err);
await dbClient
.update(drizzleDb.schemas.restoration)
.set({status: "failed"})
.where(eq(drizzleDb.schemas.restoration.id, restoration.id));
continue;
}
await dbClient
.update(drizzleDb.schemas.restoration)
.set({status: "ongoing"})
.where(eq(drizzleDb.schemas.restoration.id, restoration.id));
}
databasesResponse.push(formatDatabase(databaseUpdated, backupAction, restoreAction, UrlBackup));
databasesResponse.push(formatDatabase(databaseUpdated, backupAction, restoreAction, urlBackup));
}
}
return databasesResponse;
}
}
+38 -20
View File
@@ -1,24 +1,28 @@
import {prisma} from "@/prisma";
import {NextResponse} from "next/server";
import {Dbms} from "@prisma/client";
import {getFileUrlPresignedLocal} from "@/features/upload/private/upload.action";
import {handleDatabases} from "./helpers";
import {eventEmitter} from "../../../events/route";
import * as drizzleDb from "@/db";
import {db} from "@/db";
import {EDbmsSchema} from "@/db/schema/types";
import {eq} from "drizzle-orm";
import {isUuidv4} from "@/utils/verify-uuid";
import {withUpdatedAt} from "@/db/utils";
export type databaseAgent = {
name: string,
dbms: Dbms,
dbms: EDbmsSchema,
generatedId: string
}
export type Body = {
version: string,
databases: databaseAgent[]
}
// Function to test the get file url presigned local
export async function GET(request: Request) {
const url = await getFileUrlPresignedLocal("d4a7fa35-2506-4d01-a612-a8ef2e2cc1c5.dump")
const url = await getFileUrlPresignedLocal({fileName: "d4a7fa35-2506-4d01-a612-a8ef2e2cc1c5.dump"})
return Response.json({
message: url
})
@@ -32,26 +36,39 @@ export async function POST(
const agentId = (await params).agentId
const body: Body = await request.json();
const lastContact = new Date();
let message: string
const agent = await prisma.agent.findFirst({
where: {
id: agentId
}
if (!isUuidv4(agentId)) {
message = "agentId is not a valid uuid"
console.log(message)
return NextResponse.json(
{error: "agentId is not a valid uuid"},
{status: 500}
);
}
const agent = await db.query.agent.findFirst({
where: eq(drizzleDb.schemas.agent.id, agentId),
})
if (!agent) {
return NextResponse.json({error: "Agent not found"}, {status: 404})
message = "Agent not found"
console.log(message)
return NextResponse.json({error: message}, {status: 404})
}
const databasesResponse = await handleDatabases(body, agent, lastContact)
await prisma.agent.update({
where:{
id: agentId,
},
data:{
lastContact: lastContact,
}
})
eventEmitter.emit('modification', { update: true });
await db
.update(drizzleDb.schemas.agent)
.set(withUpdatedAt({
version: body.version,
lastContact: lastContact
}))
.where(eq(drizzleDb.schemas.agent.id, agentId));
eventEmitter.emit('modification', {update: true});
const response = {
agent: {
@@ -61,6 +78,7 @@ export async function POST(
databases: databasesResponse
}
console.log(response)
return Response.json(response)
} catch (error) {
console.error('Error in POST handler:', error);
+4
View File
@@ -0,0 +1,4 @@
import { auth } from "@/lib/auth/auth";
import { toNextJsHandler } from "better-auth/next-js";
export const { GET, POST } = toNextJsHandler(auth.handler);
-4
View File
@@ -1,4 +0,0 @@
import {handlers} from "@/auth/auth";
export const {GET, POST} = handlers;
+9
View File
@@ -0,0 +1,9 @@
import {NextResponse} from "next/server";
export async function GET() {
return NextResponse.json({
PROJECT_URL: process.env.PROJECT_URL,
PROJECT_NAME: process.env.PROJECT_NAME,
PROJECT_DESCRIPTION: process.env.PROJECT_DESCRIPTION,
});
}
+22 -12
View File
@@ -1,9 +1,19 @@
import { EventEmitter } from 'events';
import {EventEmitter} from 'events';
import {auth} from "@/lib/auth/auth";
import {headers} from "next/headers";
import {NextResponse} from "next/server";
export const eventEmitter = new EventEmitter();
export async function GET(request: Request) {
console.log('GET request received');
const session = await auth.api.getSession({
headers: await headers(),
});
if (!session) {
return NextResponse.json({error: "Unauthorized"}, {status: 403});
}
return new Response(
new ReadableStream({
@@ -37,13 +47,13 @@ export async function GET(request: Request) {
);
}
export async function POST(request: Request) {
console.log('POST request received');
const data = await request.json();
console.log('Data received:', data);
// Emit the event to all connected clients
eventEmitter.emit('modification', data);
return new Response('Event sent', { status: 200 });
}
// export async function POST(request: Request) {
// console.log('POST request received');
// const data = await request.json();
// console.log('Data received:', data);
//
// // Emit the event to all connected clients
// eventEmitter.emit('modification', data);
//
// return new Response('Event sent', {status: 200});
// }
+8 -8
View File
@@ -12,16 +12,16 @@ export async function GET(
const expires = searchParams.get('expires');
const fileName = (await params).fileName
const privateLocalDir = "private/uploads/";
const filePath = path.join(privateLocalDir, fileName);
const uploadsDir = "private/uploads/files/";
const uploadPath = path.join(uploadsDir, fileName);
const crypto = require('crypto');
if (!fs.existsSync(filePath)) {
return NextResponse.json(
{error: 'File not found'},
{status: 404}
);
let filePath = null;
if (fs.existsSync(uploadPath)) {
filePath = uploadPath;
} else {
return NextResponse.json({error: "File not found"}, {status: 404})
}
const expectedToken = crypto.createHash('sha256').update(`${fileName}${expires}`).digest('hex');
@@ -32,7 +32,7 @@ export async function GET(
);
}
const expiresAt = parseInt(expires, 10);
const expiresAt = parseInt(expires!, 10);
if (Date.now() > expiresAt) {
return NextResponse.json(
{error: 'Signed token expired'},
+103
View File
@@ -0,0 +1,103 @@
import {NextResponse} from "next/server";
import {auth} from "@/lib/auth/auth";
import {headers} from "next/headers";
import {checkFileExistsInBucket, getObjectFromClient} from "@/utils/s3-file-management";
import {env} from "@/env.mjs";
import * as stream from "node:stream";
import path from "path";
import {db} from "@/db";
import * as drizzleDb from "@/db";
import {eq} from "drizzle-orm";
import fs from "fs/promises";
function nodeStreamToWebStream(nodeStream: stream.Readable) {
return new ReadableStream({
start(controller) {
nodeStream.on("data", chunk => controller.enqueue(chunk));
nodeStream.on("end", () => controller.close());
nodeStream.on("error", err => controller.error(err));
},
cancel() {
nodeStream.destroy();
},
});
}
const privateS3ImageDir = "images/";
export async function GET(
req: Request,
{params}: { params: Promise<{ fileName: string }> }
) {
const fileName = (await params).fileName;
if (!fileName) return NextResponse.json({error: "Missing file parameter"}, {status: 400});
const session = await auth.api.getSession({headers: await headers()});
if (!session) return NextResponse.json({error: "Unauthorized"}, {status: 403});
const [settings] = await db
.select()
.from(drizzleDb.schemas.setting)
.where(eq(drizzleDb.schemas.setting.name, "system"))
.limit(1);
if (!settings) throw new Error("System settings not found.");
const storageType = settings.storage; // "local" or "s3"
const ext = fileName.split(".").pop()?.toLowerCase();
const contentType =
ext === "png"
? "image/png"
: ext === "jpg" || ext === "jpeg"
? "image/jpeg"
: ext === "gif"
? "image/gif"
: ext === "webp"
? "image/webp"
: "application/octet-stream";
try {
if (storageType === "local") {
const filePath = path.join(process.cwd(), "private/uploads/images", fileName);
try {
await fs.access(filePath);
const file = await fs.readFile(filePath);
return new NextResponse(file, {
headers: {
"Content-Type": contentType,
"Cache-Control": "no-store",
"Content-Disposition": `inline; filename="${fileName}"`,
},
});
} catch {
// if not found locally, fallback to S3
}
}
const exists = await checkFileExistsInBucket({
bucketName: env.S3_BUCKET_NAME!,
fileName: `${privateS3ImageDir}${fileName}`,
});
if (!exists) return NextResponse.json({error: "File not found"}, {status: 404});
const nodeStream = await getObjectFromClient({
bucketName: env.S3_BUCKET_NAME!,
fileName: `${privateS3ImageDir}${fileName}`,
});
const webStream = nodeStreamToWebStream(nodeStream);
return new NextResponse(webStream, {
headers: {
"Content-Type": contentType,
"Cache-Control": "no-store",
"Content-Disposition": `inline; filename="${fileName}"`,
},
});
} catch (err) {
console.error("Error streaming image:", err);
return NextResponse.json({error: "Error fetching file"}, {status: 500});
}
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

+2 -2
View File
@@ -24,7 +24,7 @@
// <div className="flex h-screen w-full flex-col items-center justify-center">
// <a
// href="#"
// className="block max-w-sm rounded-lg border border-gray-200 bg-white p-6 text-center shadow hover:bg-gray-100 dark:border-gray-700 dark:bg-gray-800 dark:hover:bg-gray-700"
// className="block max-w-sm rounded-lg border border-gray-200 bg-white p-6 text-center shadow-sm hover:bg-gray-100 dark:border-gray-700 dark:bg-gray-800 dark:hover:bg-gray-700"
// >
// <h5 className="mb-2 flex flex-row items-center justify-center gap-2 text-xl font-bold tracking-tight text-gray-900 dark:text-white">
// Something went wrong
@@ -63,7 +63,7 @@ function AuthErrorPageContent() {
<div className="flex h-screen w-full flex-col items-center justify-center">
<a
href="#"
className="block max-w-sm rounded-lg border border-gray-200 bg-white p-6 text-center shadow hover:bg-gray-100 dark:border-gray-700 dark:bg-gray-800 dark:hover:bg-gray-700"
className="block max-w-sm rounded-lg border border-gray-200 bg-white p-6 text-center shadow-sm hover:bg-gray-100 dark:border-gray-700 dark:bg-gray-800 dark:hover:bg-gray-700"
>
<h5 className="mb-2 flex flex-row items-center justify-center gap-2 text-xl font-bold tracking-tight text-gray-900 dark:text-white">
Something went wrong
Regular → Executable
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

+127 -90
View File
@@ -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;
}
}
Executable
+1
View File
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 20 KiB

Executable
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

+16 -9
View File
@@ -1,27 +1,34 @@
import React from "react";
import type {Metadata} from "next";
import {Inter} from "next/font/google";
import "./globals.css";
import {Providers} from "./providers";
import {cn} from "@/lib/utils";
import {ConsoleSilencer} from "@/components/wrappers/common/console-silencer";
import {inter} from "@/fonts/fonts";
const inter = Inter({subsets: ["latin"]});
const title = process.env.PROJECT_NAME ?? "App Title";
export const metadata: Metadata = {
title: "Portabase",
description: "Manage all your database instances from one place !",
title: {
default: title,
template: `%s - ${title}`
},
description: process.env.PROJECT_DESCRIPTION ?? undefined,
};
export default function RootLayout({
children,
}: Readonly<{
export default async function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en" suppressHydrationWarning>
<head>
<meta name="apple-mobile-web-app-title" content={title}/>
</head>
<body className={cn(inter.className, "h-full")}>
<ConsoleSilencer/>
<Providers>
{children}
</Providers>
+21
View File
@@ -0,0 +1,21 @@
{
"name": "Portabase",
"short_name": "Portabase",
"icons": [
{
"src": "/web-app-manifest-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable"
},
{
"src": "/web-app-manifest-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable"
}
],
"theme_color": "#ffffff",
"background_color": "#ffffff",
"display": "standalone"
}
+14 -13
View File
@@ -1,14 +1,15 @@
"use client"
import {useRouter} from "next/navigation";
import {useEffect} from "react";
import BackButton from "@/components/wrappers/common/button/back-button";
export default function NotFound() {
const router = useRouter();
useEffect(() => {
router.push('/');
}, [router]);
return null; // You can return null or a loading spinner while the redirect happens
}
export default async function NotFound() {
return(
<div className="flex items-center min-h-screen px-4 py-12 sm:px-6 md:px-8 lg:px-12 xl:px-16">
<div className="w-full space-y-6 text-center">
<div className="space-y-3">
<h1 className="scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl">Not found</h1>
<p className="leading-7 [&:not(:first-child)]:mt-6">The content you are trying to view is not available.</p>
</div>
<BackButton>Go home</BackButton>
</div>
</div>
)
}
+11 -10
View File
@@ -1,29 +1,30 @@
"use client"
"use client";
import {PropsWithChildren} from "react";
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 {QueryClient, QueryClientProvider} from "@tanstack/react-query";
import {ThemeMetaUpdaterRoot} from "@/features/browser/theme-meta-updater-root";
export type ProviderProps = PropsWithChildren<{}>;
const queryClient = new QueryClient()
const queryClient = new QueryClient();
export const Providers = (props: ProviderProps) => {
return (
<SessionProvider>
<Suspense fallback={null}>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
// disableTransitionOnChange
>
<ThemeMetaUpdaterRoot/>
<QueryClientProvider client={queryClient}>
<Toaster/>
{props.children}
</QueryClientProvider>
</ThemeProvider>
</SessionProvider>
)
}
</Suspense>
);
};
+5 -1
View File
@@ -10,11 +10,15 @@
"cssVariables": true,
"prefix": ""
},
"iconLibrary": "radix",
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"registries": {
"@reui": "https://reui.io/r/{name}.json"
}
}
}
+7 -23
View File
@@ -3,23 +3,22 @@ name: portabase-prod
services:
app:
build:
context: .
dockerfile: docker/dockerfile/Dockerfile
target: prod
# build:
# context: .
# dockerfile: docker/dockerfile/Dockerfile
# target: prod
image: solucetechnologies/portabase:1.1.4-rc.2
ports:
- '8887:80'
environment:
TZ: "Europe/Paris"
env_file:
- .env
depends_on:
db:
condition: service_healthy
container_name: portabase-app-prod
db:
image: postgres:16-alpine
ports:
@@ -36,20 +35,5 @@ services:
timeout: 5s
retries: 5
s3:
image: docker.io/bitnami/minio:latest
ports:
- '9000:9000'
- '9001:9001'
volumes:
- minio_data:/data
environment:
- MINIO_ROOT_USER=SoluceTechnologies
- MINIO_ROOT_PASSWORD=p18avkwHPDh9mk
- MINIO_DEFAULT_BUCKETS=statics
volumes:
postgres-data:
minio_data:
-47
View File
@@ -1,36 +1,6 @@
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
develop:
watch:
- action: sync
path: .
target: /app
ignore:
- node_modules/
- action: rebuild
path: package.json
db:
image: postgres:16-alpine
ports:
@@ -47,22 +17,5 @@ services:
timeout: 5s
retries: 5
s3:
image: docker.io/bitnami/minio:latest
ports:
- '9000:9000'
- '9001:9001'
volumes:
- minio_data:/data
environment:
- MINIO_ROOT_USER=SoluceTechnologies
- MINIO_ROOT_PASSWORD=p18avkwHPDh9mk
- MINIO_DEFAULT_BUCKETS=statics
volumes:
postgres-data:
minio_data:
+28 -15
View File
@@ -1,12 +1,23 @@
FROM node:23.5.0-alpine AS base
FROM node:22-alpine AS base
#ENV YARN_VERSION=4.2.2
ENV YARN_VERSION=4.5.3
ENV YARN_VERSION=4.9.1
RUN apk add --no-cache libc6-compat
RUN apk add --no-cache openssl
RUN apk add --update --no-cache \
tzdata \
build-base \
libc6-compat \
openssl \
make \
g++ \
jpeg-dev \
cairo-dev \
giflib-dev \
pango-dev \
libtool \
autoconf \
automake \
libpng
# install and use yarn 4.x
RUN corepack enable && corepack prepare yarn@${YARN_VERSION}
RUN yarn set version ${YARN_VERSION}
@@ -19,6 +30,7 @@ COPY .yarn ./.yarn
COPY .yarnrc.yml ./
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
RUN \
if [ -f yarn.lock ]; then yarn; \
elif [ -f package-lock.json ]; then npm ci; \
@@ -49,10 +61,6 @@ COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
RUN npx prisma generate
RUN npx zenstack generate
RUN \
if [ -f yarn.lock ]; then yarn run build; \
elif [ -f package-lock.json ]; then npm run build; \
@@ -75,14 +83,17 @@ COPY --from=builder /app/public ./public
RUN mkdir .next
RUN chown nextjs:nodejs .next
RUN mkdir -p /app/private/uploads
RUN chown -R nextjs:nodejs /app/private
RUN chown -R nextjs:nodejs /app/public
COPY --from=builder /app/next.config.mjs ./
COPY --from=builder /app/schema.zmodel ./
#COPY --from=builder /app/src/lib/hooks ./src/lib/hooks
COPY --from=builder /app/src/openapi.yaml ./src/
COPY --from=builder /app/next.config.ts ./
COPY --from=builder /app/portabase.config.ts ./
COPY --from=builder /app/drizzle.config.ts ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/standalone ./
COPY --from=builder --chown=nextjs:nodejs /app/.next/static ./.next/static
COPY --chown=nextjs:nodejs prisma ./prisma/
COPY --chown=nextjs:nodejs src/db ./src/db
USER root
@@ -98,3 +109,5 @@ ENV HOSTNAME="0.0.0.0"
USER nextjs
ENTRYPOINT ["sh","/app/app-prod-entrypoint.sh"]
+7 -8
View File
@@ -1,13 +1,12 @@
#!/bin/bash
npm install -g npm@11.0.0
npm -v
set -euo pipefail
echo "▶ Running Drizzle codegen..."
npx drizzle-kit generate
current_date=$(date +"%Y-%m-%d")
npx zenstack generate
npx prisma migrate dev --name $current_date
echo "▶ Applying migrations..."
npx drizzle-kit migrate
npm run dev
exec "$@"
echo "▶ Starting Next.js dev server..."
exec npm run dev
+8 -4
View File
@@ -1,10 +1,14 @@
#!/bin/bash
npx install --save-dev @zenstackhq/openapi
if [ -n "$TZ" ]; then
echo "[INFO] Application timezone set to $TZ (environment only)"
export TZ="$TZ"
else
echo "[WARN] No TZ provided, using default container timezone"
fi
npx zenstack generate
npx prisma migrate deploy
node server.js
exec "$@"
exec "$@"
+16
View File
@@ -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!,
},
});
-13
View File
@@ -1,13 +0,0 @@
import {
Button as ReactEmailButton
} from "@react-email/components";
import {ComponentPropsWithoutRef} from "react";
export const Button = (props: ComponentPropsWithoutRef<typeof ReactEmailButton>) => {
return (
<ReactEmailButton
className="block w-52 rounded bg-blue-600 py-3.5 text-center text-sm font-normal text-white no-underline "
{...props}
/>
)
}
-47
View File
@@ -1,47 +0,0 @@
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";
const baseUrl = getServerUrl();
export const EmailLayout = ({ children, preview }: PropsWithChildren<{ preview?: string }>) => {
return (
<Tailwind>
<Html>
<Head />
{preview ? <Preview>{preview}</Preview> : <Preview>Please check your mails</Preview>}
<Body className="bg-gray-100 py-4"
style={{fontFamily: "Arial, sans-serif"}}>
<Container className="bg-white border border-gray-200 p-12">
<Img
src={`${baseUrl}/logo-title-black.png`}
width="200"
height="auto"
alt="Logo"
/>
<Section>
{children}
</Section>
</Container>
</Body>
</Html>
</Tailwind>
);
};
export default EmailLayout;
-16
View File
@@ -1,16 +0,0 @@
import { Button } from "@react-email/button";
import { Html } from "@react-email/html";
import * as React from "react";
export default function HelloEmail() {
return (
<Html>
<Button
href="https://example.com"
style={{ background: "#000", color: "#fff" }}
>
Hello Charles
</Button>
</Html>
);
}
-42
View File
@@ -1,42 +0,0 @@
import {
Text,
} from "@react-email/components";
import * as React from "react";
import EmailLayout from "./EmailLayout";
import {Button} from "@react-email/button";
export const TestEmailSettings = () => {
return (
<EmailLayout
preview="Email Setup"
>
<Text
className="text-base font-light leading-8 text-green-800 "
>
Hi, your email settings are setup !
</Text>
<Text
className="text-base font-light leading-8 text-green-800 "
>
Best regard,
</Text> <Text
className="text-base font-light leading-8 text-green-800 "
>
Portabase
</Text>
</EmailLayout>
);
};
export default TestEmailSettings;
-13
View File
@@ -1,13 +0,0 @@
import {
Text as ReactEmailText
} from "@react-email/components";
import {ComponentPropsWithoutRef} from "react";
export const Text = (props: ComponentPropsWithoutRef<typeof ReactEmailText>) => {
return (
<ReactEmailText
className="text-base font-light leading-8 text-green-800 "
{...props}
/>
)
}
+14
View File
@@ -0,0 +1,14 @@
import {defineConfig, globalIgnores} from 'eslint/config'
import nextVitals from 'eslint-config-next/core-web-vitals'
const eslintConfig = defineConfig([
...nextVitals,
globalIgnores([
'.next/**',
'out/**',
'build/**',
'next-env.d.ts',
]),
])
export default eslintConfig
+4 -4
View File
@@ -1,6 +1,6 @@
import {init} from "@/utils/init";
export async function register() {
init()
if (process.env.NEXT_RUNTIME === "nodejs") {
const init = await import("@/utils/init");
await init.init();
}
}
-55
View File
@@ -1,55 +0,0 @@
import {NextRequest, NextResponse} from 'next/server'
import {loggingMiddleware} from "@/middleware/loggingMiddleware";
import {errorHandler} from "@/middleware/errorHandler";
export function middleware(request: NextRequest) {
const url = request.nextUrl.clone();
// 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' } }
);
}
}
try{
loggingMiddleware(request);
}catch(err){
errorHandler(err)
}
}
// Function to check if the route exists (supports dynamic routes)
function checkRouteExists(pathname) {
// 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\/[^/]+\/backup\/?$/,
/^\/api\/agent\/[^/]+\/restore\/?$/,
/^\/api\/files\/[^/]+\/?$/,
/^\/api\/events\/?$/,
];
return routePatterns.some(pattern => pattern.test(pathname));
}
export const config = {
matcher: [
// '/api/agent/:path*',
'/api/:path*',
],
};
-20
View File
@@ -1,20 +0,0 @@
// next-auth.d.ts
import NextAuth from "next-auth";
declare module "next-auth" {
interface Session {
user: {
id: string;
email: string;
name: string;
authMethod: string; // Add authMethod to the session user type
};
}
interface User {
id: string;
email: string;
name: string;
authMethod: string; // Add authMethod to the user type
}
}
+1
View File
@@ -1,5 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.

Some files were not shown because too many files have changed in this diff Show More