Compare commits

..
202 Commits
Author SHA1 Message Date
Théo LAGACHE 977f9344c7 chore(release): 1.1.9-rc4 2026-01-06 16:23:09 +01:00
Théo LAGACHE 2adb2f3b68 chore(release): 1.1.9-rc3 2026-01-06 16:21:06 +01:00
Théo LAGACHE 40a6e9e69f fix 2026-01-06 16:12:04 +01:00
Théo LAGACHE e1d1a3fb9a fix(ci): grant necessary permissions for docker publish workflow 2026-01-06 15:37:19 +01:00
Théo LAGACHE 75120c0a56 fix 2026-01-06 15:33:06 +01:00
Théo LAGACHE 33f1ef3bb3 ci: restore release candidate workflow and use reusable actions 2026-01-06 15:10:28 +01:00
Théo LAGACHE b6e742333c update: README.md 2026-01-06 13:26:52 +01:00
Théo LAGACHE 9f8c66fc20 update readme.md 2026-01-06 12:11:45 +01:00
Théo LAGACHE 3e950652e8 update readme.md 2026-01-05 15:04:28 +01:00
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 6562718635 Correcting some bugs in statistics. 2025-01-12 17:53:39 +01:00
622 changed files with 68049 additions and 57114 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 |
---
Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

+63
View File
@@ -0,0 +1,63 @@
name: Docker Publish
on:
workflow_call:
inputs:
image_name:
required: false
type: string
default: 'solucetechnologies/portabase'
add_latest:
required: false
type: boolean
default: false
target:
required: false
type: string
default: 'prod'
dockerfile:
required: false
type: string
default: './docker/dockerfile/Dockerfile'
secrets:
DOCKER_USERNAME:
required: true
DOCKER_PASSWORD:
required: true
jobs:
build-and-push:
runs-on: ubuntu-latest
permissions:
packages: write
contents: read
steps:
- name: Check out the repo
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Log in to Docker Hub
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Set tags
id: set-tags
run: |
REF_NAME=${GITHUB_REF#refs/tags/}
TAGS="${{ inputs.image_name }}:$REF_NAME"
if [[ "${{ inputs.add_latest }}" == "true" ]]; then
TAGS="$TAGS,${{ inputs.image_name }}:latest"
fi
echo "tags=$TAGS" >> $GITHUB_OUTPUT
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
file: ${{ inputs.dockerfile }}
push: true
tags: ${{ steps.set-tags.outputs.tags }}
target: ${{ inputs.target }}
+128
View File
@@ -0,0 +1,128 @@
name: GitHub Release
on:
workflow_call:
inputs:
prerelease:
required: false
type: boolean
default: false
make_latest:
required: false
type: boolean
default: false
discord_title:
required: true
type: string
discord_color:
required: true
type: number
discord_footer:
required: true
type: string
secrets:
DISCORD_WEBHOOK:
required: true
GH_TOKEN:
required: true
jobs:
create-release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Check out the repo
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Build Changelog
id: build_changelog
uses: mikepenz/release-changelog-builder-action@v5
with:
mode: "COMMIT"
configurationJson: |
{
"template": "#{{CHANGELOG}}",
"categories": [
{
"title": "## Feature",
"labels": ["feat", "feature"]
},
{
"title": "## Fix",
"labels": ["fix", "bug"]
},
{
"title": "## Other",
"labels": []
}
],
"label_extractor": [
{
"pattern": "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test){1}(\([\w\-\.]+\))?(!)?: ([\w ])+([\s\S]*)",
"on_property": "title",
"target": "$1"
}
]
}
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
generate_release_notes: false
body: ${{ steps.build_changelog.outputs.changelog }}
prerelease: ${{ inputs.prerelease }}
make_latest: ${{ inputs.make_latest }}
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
- name: Send Discord Notification
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
run: |
RELEASE_INFO=$(gh release view "${{ github.ref_name }}" -R ${{ github.repository }} --json name,url,body,author)
RELEASE_TITLE=$(echo "$RELEASE_INFO" | jq -r .name)
if [ -z "$RELEASE_TITLE" ] || [ "$RELEASE_TITLE" = "null" ]; then RELEASE_TITLE="${{ github.ref_name }}"; fi
RELEASE_URL=$(echo "$RELEASE_INFO" | jq -r .url)
RELEASE_BODY=$(echo "$RELEASE_INFO" | jq -r .body)
AUTHOR_NAME="Portabase"
AUTHOR_ICON="https://github.com/Portabase.png"
PAYLOAD=$(jq -n \
--arg title "$RELEASE_TITLE" \
--arg description "$RELEASE_BODY" \
--arg url "$RELEASE_URL" \
--arg author "$AUTHOR_NAME" \
--arg icon "$AUTHOR_ICON" \
--arg discord_title "${{ inputs.discord_title }}" \
--arg discord_footer "${{ inputs.discord_footer }}" \
--argjson discord_color ${{ inputs.discord_color }} \
'{
content: $discord_title,
embeds: [{
title: $title,
url: $url,
description: $description,
color: $discord_color,
author: {
name: $author,
icon_url: $icon
},
footer: {
text: $discord_footer
}
}]
}'
)
curl -H "Content-Type: application/json" \
-d "$PAYLOAD" \
"$DISCORD_WEBHOOK"
+32
View File
@@ -0,0 +1,32 @@
name: Publish Docker image for release candidate
on:
push:
tags:
- '*-rc*'
permissions:
contents: write
packages: write
jobs:
docker_publish:
uses: ./.github/workflows/docker.yml
with:
add_latest: false
secrets:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
github_release:
needs: docker_publish
uses: ./.github/workflows/github.yml
with:
prerelease: true
make_latest: false
discord_title: "||@release-dashboard|| New release candidate published"
discord_color: 16776960
discord_footer: "Portabase"
secrets:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+27 -42
View File
@@ -1,48 +1,33 @@
name: Publish Docker image for release
on:
release:
types: [published]
push:
tags:
- '*.*.*'
- '!*-*'
permissions:
contents: write
packages: write
jobs:
push_to_registry:
name: Push Docker image to Docker Hub
runs-on: ubuntu-latest
permissions:
packages: write
contents: read
attestations: write
id-token: write
steps:
- name: Check out the repo
uses: actions/checkout@v4
docker_publish:
uses: ./.github/workflows/docker.yml
with:
add_latest: true
secrets:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
- name: Log in to Docker Hub
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
with:
images: solucetechnologies/portabase
- name: Build and push Docker image
id: push
uses: docker/build-push-action@v6
with:
context: .
file: ./docker/dockerfile/Dockerfile
push: true
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
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
github_release:
needs: docker_publish
uses: ./.github/workflows/github.yml
with:
prerelease: false
make_latest: true
discord_title: "||@release-dashboard|| New release published"
discord_color: 5814783
discord_footer: "Portabase"
secrets:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+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
}
-934
View File
File diff suppressed because one or more lines are too long
-3
View File
@@ -1,3 +0,0 @@
nodeLinker: node-modules
yarnPath: .yarn/releases/yarn-4.6.0.cjs
+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.9-rc4
date-released: "2026-01-06"
+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)
+66 -159
View File
@@ -1,182 +1,89 @@
<!-- 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="/.github/assets/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 align="center" style="margin-top: 20px; font-style: italic;">
<i>Portabase is a tool designed to simplify the backup and restoration of your database instances. It integrates seamlessly with <a href="https://github.com/Portabase/agent">Portabase agents</a> for managing operations securely and efficiently.</i>
</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)
<!-- 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>
[![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)
<!-- ABOUT THE PROJECT -->
## About The Project
[![NextJS][NextJS]][NextJS-url]
[![BetterAuth][BetterAuth]][BetterAuth-url]
[![Drizzle][Drizzle]][Drizzle-url]
[![ShadcnUI][ShadcnUI]][ShadcnUI-url]
[![Docker][Docker]][Docker-url]
Portabase is the server dashboard service working with portabase agents.
<a href="https://github.com/Soluce-Technologies/portabase">see Portabase</a>
<p>
<strong>
<a href="https://portabase.io">Website</a> •
<a href="https://portabase.io/docs">Documentation</a> •
<a href="https://www.youtube.com/watch?v=hvLbX5LN1UE">Demo</a> •
<a href="https://portabase.io/docs/dashboard/setup">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>
<p align="right">(<a href="#readme-top">back to top</a>)</p>
![portabase-dashboard](https://github.com/user-attachments/assets/f5ec7204-79c0-43ac-ba17-f2ed682c39d6)
</div>
## Installation
You have 4 ways to install Portabase:
- Automated CLI (recommended) - [details](https://portabase.io/docs/dashboard/setup#cli)
- Docker Compose setup - [details](https://portabase.io/docs/dashboard/setup#docker)
- Kubernetes with Helm (soon)
- Development setup - [details](https://portabase.io/docs/dashboard/setup#development)
**Ensure Docker is installed on your machine before getting started.**
### Built With
## Contributors
* [![NextJS][NextJS]][NextJS-url]
* [![Prisma][Prisma]][Prisma-url]
[![Contributors](https://contrib.rocks/image?repo=Portabase/portabase)](https://github.com/Portabase/portabase/graphs/contributors)
<p align="right">(<a href="#readme-top">back to top</a>)</p>
<!-- GETTING STARTED -->
## Getting Started
### Prerequisites
Install docker on your machine
### Installation
Installation steps in order to use it locally and work on some features improvements
1. Clone the repo
```sh
git clone https://github.com/Soluce-Technologies/portabase
```
3. Start the project
```sh
docker compose up
```
<p align="right">(<a href="#readme-top">back to top</a>)</p>
<!-- ROADMAP -->
## Roadmap
- [ ] Add Changelog
- [ ] Improve documentation and repositories
- [ ] Add tests procedure
- [ ] Add Release File
- [ ] Security improvements
- [ ] Improve UX/UI
- [ ] Improve Workspace system
- [ ] Migrate to Drizzle ORM
- [ ] Multi locale support
- [ ] Multi db support
- [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)```
For zenstask
```zenstack generate```
<!-- 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>
Distributed under the Apache License. See `LICENSE.txt` for more details.
<!-- 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>
);
}
+16 -10
View File
@@ -1,19 +1,25 @@
import React from "react";
import {LayoutAdmin} from "@/components/layout";
import {redirect} from "next/navigation";
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 && !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,29 +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";
export const dynamic = "force-dynamic";
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),
});
if(!agents){
notFound()
console.log(agents);
if (!agents) {
notFound();
}
return (
<Page>
<PageHeader>
<PageTitle>
Agents
</PageTitle>
<PageTitle>Agents</PageTitle>
{agents.length > 0 && (
<PageActions>
<Link href={"/dashboard/agents/new"}>
@@ -32,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 -13
View File
@@ -1,17 +1,13 @@
import React from "react";
import {currentUser, requiredCurrentUser} from "@/auth/current-user";
import {notFound} from "next/navigation";
import { notFound } from "next/navigation";
import { currentUser } from "@/lib/auth/current-user";
export default async function Layout({children}: { children: React.ReactNode }) {
export default async function Layout({ children }: { children: React.ReactNode }) {
const user = await currentUser();
const user = await requiredCurrentUser()
if(user.role != "admin"){
notFound()
if (user!.role !== "superadmin" && user!.role !== "admin") {
notFound();
}
return (
<>
{children}
</>
)
}
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,77 +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,
isArchived: {not: true}
},
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,48 +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,72 +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 {getCurrentOrganizationSlug} from "@/features/dashboard/organization-cookie";
import {notFound} from "next/navigation";
export default async function RoutePage(props: PageParams<{slug: string}>) {
const {slug: organizationSlug} = await props.params
const projects = await prisma.project.findMany({
where: {
isArchived: {not: true},
organization: {
slug: organizationSlug,
// users: {
// some: {
// userId: user.id
// },
// },
}
},
include: {
databases: {}
}
})
return (
<Page>
<PageHeader>
<PageTitle>
Projects
</PageTitle>
{projects.length > 0 && (
<PageActions>
<Link href={`/dashboard/${organizationSlug}/projects/new`}>
<Button>+ Create Project</Button>
</Link>
</PageActions>
)}
</PageHeader>
<PageContent className="mt-10">
{projects.length > 0 ?
<CardsWithPagination
organizationSlug={organizationSlug}
data={projects}
cardItem={ProjectCard}
cardsPerPage={4}
numberOfColumns={1}
/>
:
<Link
href={`/dashboard/${organizationSlug}/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 -31
View File
@@ -1,37 +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, requiredCurrentUser} 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 {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 requiredCurrentUser()
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 -8
View File
@@ -1,15 +1,14 @@
import {redirect} from "next/navigation";
import {currentUser} from "@/auth/current-user";
import {getCurrentOrganizationSlug} from "@/features/dashboard/organization-cookie";
import { redirect } from "next/navigation";
import { getCurrentOrganizationSlug } from "@/features/dashboard/organization-cookie";
import { currentUser } from "@/lib/auth/current-user";
export default async function Index() {
const user = await currentUser()
const user = await currentUser();
if (user) {
const currentOrganizationSlug = await getCurrentOrganizationSlug()
redirect(`/dashboard/${currentOrganizationSlug}/projects`)
const currentOrganizationSlug = await getCurrentOrganizationSlug();
redirect(`/dashboard/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/files/";
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'},
+88 -19
View File
@@ -1,34 +1,103 @@
import path from "path";
import fs from "fs/promises";
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(
request: Request,
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 {
const fileName = (await params).fileName
const filePath = path.join(process.cwd(), "private/uploads/images", fileName);
if (storageType === "local") {
const filePath = path.join(process.cwd(), "private/uploads/images", fileName);
// Check if the file exists
try {
await fs.access(filePath); // Ensures the file exists
} catch {
return NextResponse.json({error: "File not found"}, {status: 404});
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
}
}
// Read the file
const fileContent = await fs.readFile(filePath); // Returns a Buffer
const exists = await checkFileExistsInBucket({
bucketName: env.S3_BUCKET_NAME!,
fileName: `${privateS3ImageDir}${fileName}`,
});
if (!exists) return NextResponse.json({error: "File not found"}, {status: 404});
return new NextResponse(fileContent, {
const nodeStream = await getObjectFromClient({
bucketName: env.S3_BUCKET_NAME!,
fileName: `${privateS3ImageDir}${fileName}`,
});
const webStream = nodeStreamToWebStream(nodeStream);
return new NextResponse(webStream, {
headers: {
"Content-Disposition": `attachment; filename="${fileName}"`,
"Content-Type": "application/octet-stream", // Adjust MIME type as needed
"Content-Type": contentType,
"Cache-Control": "no-store",
"Content-Disposition": `inline; filename="${fileName}"`,
},
});
} catch (error) {
console.error("Error reading file:", error);
return NextResponse.json({error: "Internal Server Error"}, {status: 500});
} catch (err) {
console.error("Error streaming image:", err);
return NextResponse.json({error: "Error fetching file"}, {status: 500});
}
}
}
-32
View File
@@ -1,32 +0,0 @@
import {NextResponse} from "next/server";
export type BodyInit = {
initialize: boolean
}
export async function POST(
request: Request,
) {
try {
const body: BodyInit = await request.json();
console.log(body)
return NextResponse.json(
{
message: "Initialization successfully done!",
},
{ status: 200 }
);
} catch (error) {
console.error("Error in POST initialization:", error);
return NextResponse.json(
{ error: "Internal server error" },
{ 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

+17 -12
View File
@@ -1,29 +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>
<body
className={cn(inter.className, "h-full")}
>
<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"
}
+13 -4
View File
@@ -1,6 +1,15 @@
import {redirect, useRouter} from "next/navigation";
import BackButton from "@/components/wrappers/common/button/back-button";
export default async function NotFound() {
console.log("not found");
redirect("/")
}
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>
)
}
+18 -20
View File
@@ -1,32 +1,30 @@
"use client"
"use client";
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 (
<Suspense fallback={null}>
<SessionProvider>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
// disableTransitionOnChange
>
<QueryClientProvider client={queryClient}>
<Toaster/>
{props.children}
</QueryClientProvider>
</ThemeProvider>
</SessionProvider>
<ThemeProvider
attribute="class"
defaultTheme="system"
enableSystem
>
<ThemeMetaUpdaterRoot/>
<QueryClientProvider client={queryClient}>
<Toaster/>
{props.children}
</QueryClientProvider>
</ThemeProvider>
</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=${S3_ACCESS_KEY}
- MINIO_ROOT_PASSWORD=${S3_SECRET_KEY}
- MINIO_DEFAULT_BUCKETS=${S3_BUCKET_NAME}
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=${S3_ACCESS_KEY}
- MINIO_ROOT_PASSWORD=${S3_SECRET_KEY}
- MINIO_DEFAULT_BUCKETS=${S3_BUCKET_NAME}
volumes:
postgres-data:
minio_data:
+36 -33
View File
@@ -1,32 +1,37 @@
FROM node:23.5.0-alpine AS base
FROM node:22-alpine AS base
ENV YARN_VERSION=4.6.0
RUN apk add --update --no-cache \
libc6-compat \
openssl \
tzdata
RUN apk add --no-cache libc6-compat
RUN apk add --no-cache openssl
FROM base AS build-env
# install and use yarn 4.x
RUN corepack enable && corepack prepare yarn@${YARN_VERSION}
RUN yarn set version ${YARN_VERSION}
RUN apk add --update --no-cache \
build-base \
g++ \
make \
libtool \
autoconf \
automake
RUN corepack enable && corepack prepare pnpm@latest --activate
FROM base AS deps
FROM build-env AS deps
WORKDIR /app
COPY .yarn ./.yarn
COPY .yarnrc.yml ./
COPY package.json yarn.lock* package-lock.json* pnpm-lock.yaml* ./
COPY package.json pnpm-lock.yaml pnpm-workspace.yaml ./
RUN \
if [ -f yarn.lock ]; then yarn; \
elif [ -f package-lock.json ]; then npm ci; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm i --frozen-lockfile; \
else echo "Lockfile not found." && exit 1; \
fi
RUN pnpm i --frozen-lockfile
FROM base AS dev
FROM build-env AS dev
WORKDIR /app
@@ -39,8 +44,11 @@ RUN chmod +x /app/docker/entrypoints/app-dev-entrypoint.sh
ENTRYPOINT ["sh","/app/docker/entrypoints/app-dev-entrypoint.sh"]
# Rebuild the source code only when needed
FROM base AS builder
FROM build-env AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
@@ -48,16 +56,11 @@ COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
RUN npx prisma generate
RUN npx zenstack generate
RUN pnpm run build
RUN \
if [ -f yarn.lock ]; then yarn run build; \
elif [ -f package-lock.json ]; then npm run build; \
elif [ -f pnpm-lock.yaml ]; then corepack enable pnpm && pnpm run build; \
else echo "Lockfile not found." && exit 1; \
fi
FROM base AS prod
@@ -73,18 +76,18 @@ RUN adduser --system --uid 1001 nextjs
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
RUN chown nextjs:nodejs .next
COPY --from=builder /app/next.config.mjs ./
COPY --from=builder /app/schema.zmodel ./
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
+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..."
pnpm drizzle-kit generate
current_date=$(date +"%Y-%m-%d")
npx zenstack generate
npx prisma migrate dev --name $current_date
echo "▶ Applying migrations..."
pnpm drizzle-kit migrate
npm run dev
exec "$@"
echo "▶ Starting Next.js dev server..."
exec pnpm dev
+8 -10
View File
@@ -1,16 +1,14 @@
#!/bin/bash
echo " ____ __ __ "
echo " / __ \____ _____/ /_____ _/ /_ ____ _________ "
echo " / /_/ / __ \/ ___/ __/ __ / __ \/ __ / ___/ _ \ "
echo " / ____/ /_/ / / / /_/ /_/ / /_/ / /_/ (__ ) __/ "
echo " /_/ \____/_/ \__/\__,_/_.___/\__,_/____/\___/ "
echo " "
echo " Community Edition v1.0.0 "
echo " "
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 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

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