Compare commits

...
506 Commits
Author SHA1 Message Date
github-actions[bot] 8e5d6eec6c chore: release v1.4.27 2026-03-06 08:13:08 +00:00
github-actions[bot] 92f1400a35 chore: release v1.4.26 2026-03-06 08:13:05 +00:00
47f1938136 fix: workflow
Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 09:12:30 +01:00
github-actions[bot] f226218b3a chore: release v1.4.25 2026-03-06 08:07:38 +00:00
Charles GTEandGitHub 8f7fa0dea1 Merge pull request #153 from Portabase/dev
fix: workflow
2026-03-06 09:07:05 +01:00
charlesgauthereau 292b3c805d Merge branch 'main' into dev 2026-03-06 09:06:17 +01:00
charlesgauthereau ded26f92af fix 2026-03-06 09:05:43 +01:00
github-actions[bot] 8cce7bbaf4 chore: release v1.4.24 2026-03-06 08:03:42 +00:00
228b8c3ec7 fix: workflow
* fix: .release-it.json

* fix: .release-it.json

* fix: .release-it.json

* fix: .release-it.json

* fix: .release-it.json

* fix: .release-it.json

* fix: .release-it.json

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 09:03:15 +01:00
charlesgauthereau 2e298ce757 Merge branch 'main' into dev
# Conflicts:
#	.release-it.json
2026-03-06 09:02:45 +01:00
charlesgauthereau 81daa9f0b0 fix: .release-it.json 2026-03-06 09:01:55 +01:00
github-actions[bot] 713c8d9177 chore: release v1.4.23 2026-03-06 07:58:23 +00:00
f571162eed fix: workflow
* fix: .release-it.json

* fix: .release-it.json

* fix: .release-it.json

* fix: .release-it.json

* fix: .release-it.json

* fix: .release-it.json

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 08:57:54 +01:00
charlesgauthereau 19d2872943 Merge branch 'main' into dev 2026-03-06 08:57:25 +01:00
charlesgauthereau 0752b8bf7f fix: .release-it.json 2026-03-06 08:57:11 +01:00
github-actions[bot] c01097196a chore: release v1.4.22 2026-03-06 07:50:24 +00:00
5f1bee018d fix: workflow
* fix: .release-it.json

* fix: .release-it.json

* fix: .release-it.json

* fix: .release-it.json

* fix: .release-it.json

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 08:49:56 +01:00
charlesgauthereau 8655b81618 Merge branch 'main' into dev 2026-03-06 08:49:05 +01:00
charlesgauthereau 1702e43373 fix: .release-it.json 2026-03-06 08:48:53 +01:00
github-actions[bot] a7d411f6ff chore: release v1.4.21 2026-03-06 07:47:06 +00:00
6a84da0f96 fix: github workflow
* fix: .release-it.json

* fix: .release-it.json

* fix: .release-it.json

* fix: .release-it.json

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 08:46:34 +01:00
charlesgauthereau 79425f4490 fix: .release-it.json 2026-03-06 08:45:30 +01:00
charlesgauthereau 2fcddea0fa Merge branch 'main' into dev 2026-03-06 08:45:08 +01:00
github-actions[bot] ac1c8552e1 chore: release v1.4.20 2026-03-06 07:40:29 +00:00
6a18eefe34 fix: workflow
* fix: .release-it.json

* fix: .release-it.json

* fix: .release-it.json

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 08:39:57 +01:00
charlesgauthereau a43eedaf9f Merge remote-tracking branch 'origin/dev' into dev 2026-03-06 08:39:24 +01:00
charlesgauthereau 77bc24990e fix: .release-it.json 2026-03-06 08:38:39 +01:00
charlesgauthereau edbc79c89e fix: .release-it.json 2026-03-06 08:38:06 +01:00
github-actions[bot] 527a090f1b chore: release v1.4.19 2026-03-06 07:23:15 +00:00
370b86337f fix: github workflow
Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 08:22:43 +01:00
charlesgauthereau 1ce3acdde0 fix: .release-it.json 2026-03-06 08:21:38 +01:00
github-actions[bot] 1a72be323b chore: release v1.4.18 2026-03-04 15:55:52 +00:00
Théo LAGACHEandGitHub bf252f22f2 fix: Fix (#146) 2026-03-04 16:55:22 +01:00
github-actions[bot] 937d92f686 chore: release v1.4.17 2026-03-04 15:33:49 +00:00
5572957731 fix: Fix/draft release (#145)
* fix

* Update release.yml

* Update release.yml

---------

Co-authored-by: Théo LAGACHE <theo.lagache@soluce-technologies.com>
2026-03-04 16:33:06 +01:00
github-actions[bot] d2db5722b7 chore: release v1.4.16 2026-03-04 15:31:21 +00:00
5404185a6c fix: Fix/draft release (#144)
* fix

* Update release.yml

* Update release.yml

---------

Co-authored-by: Théo LAGACHE <theo.lagache@soluce-technologies.com>
2026-03-04 16:30:40 +01:00
github-actions[bot] 800f2dfbbb chore: release v1.4.15 2026-03-04 15:11:36 +00:00
ed0b5410f6 fix: Fix/draft release (#143)
* fix

* Update release.yml

---------

Co-authored-by: Théo LAGACHE <theo.lagache@soluce-technologies.com>
2026-03-04 16:11:00 +01:00
github-actions[bot] ae72e262aa chore: release v1.4.14 2026-03-04 14:49:21 +00:00
Charles GTEandGitHub c4c7050c3f fix: Notification Logs Modal
fix: Notification Logs Modal
2026-03-04 15:48:29 +01:00
charlesgauthereau 81e5227270 fix: modal notification-log-modal.tsx 2026-03-04 15:44:33 +01:00
Charles GTEandGitHub 39319d9162 Merge pull request #141 from Portabase/feat/json-provider-notification
Feat/json provider notification
2026-03-04 15:38:10 +01:00
Charles GTEandGitHub 12206fa856 Merge branch 'dev' into feat/json-provider-notification 2026-03-04 15:35:43 +01:00
Théo LAGACHE 9d8c6e26f1 Update notification-log-modal.tsx 2026-03-04 15:09:55 +01:00
Théo LAGACHE eb624672e6 Update notification-log-modal.tsx 2026-03-04 14:18:46 +01:00
Théo LAGACHE 1b026b5f08 Update notification-log-modal.tsx 2026-03-04 14:12:23 +01:00
github-actions[bot] 21b9b8b44d chore: release v1.4.13 2026-03-04 12:37:48 +00:00
6ef7fcc0c2 fix: Feat/changelog icon (#137)
* Update .release-it.json

* Update .release-it.json

---------

Co-authored-by: Théo LAGACHE <theo.lagache@soluce-technologies.com>
2026-03-04 13:37:19 +01:00
fc4ce178cb fix: Update .release-it.json (#136)
Co-authored-by: Théo LAGACHE <theo.lagache@soluce-technologies.com>
2026-03-04 13:33:58 +01:00
github-actions[bot] 98b72988c4 chore: release v1.4.12 2026-03-04 12:30:32 +00:00
317473e528 fix: Update CONTRIBUTING.md (#135)
Co-authored-by: Théo LAGACHE <theo.lagache@soluce-technologies.com>
2026-03-04 13:30:04 +01:00
github-actions[bot] e13c5ddf51 chore: release v1.4.11 2026-03-04 12:24:36 +00:00
90be3ab731 fix: password input on sensible fields (#134)
Co-authored-by: Théo LAGACHE <theo.lagache@soluce-technologies.com>
2026-03-04 13:24:10 +01:00
github-actions[bot] 85c0445a64 chore: release v1.4.10 2026-03-04 12:02:54 +00:00
6a227ccd7c fix: Feat/flow (#133)
* fix

* fix

* fix

* fix

* Update docker.yml

* Update auto-release.yml

* Update pr-checker.yml

* Update pr-checker.yml

* fix: citation version

* Update .release-it.json

---------

Co-authored-by: Théo LAGACHE <theo.lagache@soluce-technologies.com>
2026-03-04 13:02:24 +01:00
github-actions[bot] 3d9a21f854 chore: release v1.4.9 2026-03-04 11:58:24 +00:00
d4229a985d fix: Feat/flow (#132)
* fix

* fix

* fix

* fix

* Update docker.yml

* Update auto-release.yml

* Update pr-checker.yml

* Update pr-checker.yml

* fix: citation version

---------

Co-authored-by: Théo LAGACHE <theo.lagache@soluce-technologies.com>
2026-03-04 12:57:56 +01:00
github-actions[bot] c6e0921dea chore: release v1.4.8 2026-03-04 10:57:55 +00:00
346a8a4375 fix: Feat/flow (#131)
* fix

* fix

* fix

* fix

* Update docker.yml

* Update auto-release.yml

* Update pr-checker.yml

---------

Co-authored-by: Théo LAGACHE <theo.lagache@soluce-technologies.com>
2026-03-04 11:57:11 +01:00
github-actions[bot] 4799faa827 chore: release v1.4.7 2026-03-04 10:34:00 +00:00
2c6f5c38ed fix: Feat/flow (#130)
* fix

* fix

* fix

* fix

* Update docker.yml

* Update auto-release.yml

---------

Co-authored-by: Théo LAGACHE <theo.lagache@soluce-technologies.com>
2026-03-04 11:33:25 +01:00
github-actions[bot] 4a884ab5cc chore: release v1.4.6 2026-03-04 10:27:05 +00:00
e59beaf749 fix: feat/flow (#129)
* fix

* fix

* fix

* fix

* Update docker.yml

---------

Co-authored-by: Théo LAGACHE <theo.lagache@soluce-technologies.com>
2026-03-04 11:26:28 +01:00
github-actions[bot] 0d4e1229ab chore: release v1.4.5 2026-03-04 10:23:35 +00:00
8810fd91c9 fix: Feat/flow (#128)
* fix

* fix

* fix

* fix

---------

Co-authored-by: Théo LAGACHE <theo.lagache@soluce-technologies.com>
2026-03-04 11:22:51 +01:00
charlesgauthereau 095444610d fix: removed express api 2026-03-04 11:01:39 +01:00
github-actions[bot] 7cf43c5e7c chore: release v1.4.4 2026-03-04 09:29:30 +00:00
77183af08d fix: Feat/flow (#127)
* fix

* fix

---------

Co-authored-by: Théo LAGACHE <theo.lagache@soluce-technologies.com>
2026-03-04 10:23:52 +01:00
3a2c5be5dd fix: bug
Co-authored-by: Théo LAGACHE <theo.lagache@soluce-technologies.com>
2026-03-04 10:21:02 +01:00
Théo LAGACHE 11c764c2f3 fix 2026-03-04 10:17:37 +01:00
d128234ae6 fix: Feat/workflow (#125)
* update: release

* fix

* fix

* fix

* Update docker.yml

* fix

* Update auto-release.yml

* Update auto-release.yml

* add: bumper

* Update auto-release.yml

* Update auto-release.yml

* fix

* Update auto-release.yml

* fix

---------

Co-authored-by: Théo LAGACHE <theo.lagache@soluce-technologies.com>
2026-03-04 09:46:47 +01:00
Théo LAGACHE ee31ee225d fix 2026-03-04 09:44:30 +01:00
e2195616d3 fix: Feat/workflow (#124)
* update: release

* fix

* fix

* fix

* Update docker.yml

* fix

* Update auto-release.yml

* Update auto-release.yml

* add: bumper

* Update auto-release.yml

* Update auto-release.yml

* fix

* Update auto-release.yml

---------

Co-authored-by: Théo LAGACHE <theo.lagache@soluce-technologies.com>
2026-03-04 09:34:32 +01:00
Théo LAGACHE 2ca818b996 Update auto-release.yml 2026-03-04 09:33:51 +01:00
131f2b4538 fix Feat/workflow (#123)
* update: release

* fix

* fix

* fix

* Update docker.yml

* fix

* Update auto-release.yml

* Update auto-release.yml

* add: bumper

* Update auto-release.yml

* Update auto-release.yml

* fix

---------

Co-authored-by: Théo LAGACHE <theo.lagache@soluce-technologies.com>
2026-03-04 09:30:26 +01:00
6731c3fea9 fix: Feat/new workflow (#122)
* update: release

* fix

* fix

* fix

* Update docker.yml

* fix

* Update auto-release.yml

* Update auto-release.yml

* add: bumper

* Update auto-release.yml

---------

Co-authored-by: Théo LAGACHE <theo.lagache@soluce-technologies.com>
2026-03-04 09:27:00 +01:00
d3da5e8c13 fix: Feat/new workflow (#121)
* update: release

* fix

* fix

* fix

* Update docker.yml

* fix

* Update auto-release.yml

* Update auto-release.yml

* add: bumper

* Update auto-release.yml

---------

Co-authored-by: Théo LAGACHE <theo.lagache@soluce-technologies.com>
2026-03-04 09:20:10 +01:00
3b0d4225ce fix: Feat/new workflow (#118)
* update: release

* fix

* fix

* fix

* Update docker.yml

* fix

* Update auto-release.yml

* Update auto-release.yml

* add: bumper

* Update auto-release.yml

---------

Co-authored-by: Théo LAGACHE <theo.lagache@soluce-technologies.com>
2026-03-03 17:00:01 +01:00
66f0930164 fix: Feat/new workflow (#117)
* update: release

* fix

* fix

* fix

* Update docker.yml

* fix

* Update auto-release.yml

* Update auto-release.yml

* add: bumper

---------

Co-authored-by: Théo LAGACHE <theo.lagache@soluce-technologies.com>
2026-03-03 16:50:36 +01:00
3979b83a0a fix: Feat/new workflow (#116)
* update: release

* fix

* fix

* fix

* Update docker.yml

* fix

* Update auto-release.yml

* Update auto-release.yml

---------

Co-authored-by: Théo LAGACHE <theo.lagache@soluce-technologies.com>
2026-03-03 16:47:59 +01:00
b65e432564 fix: Feat/new workflow (#115)
* update: release

* fix

* fix

* fix

* Update docker.yml

* fix

* Update auto-release.yml

---------

Co-authored-by: Théo LAGACHE <theo.lagache@soluce-technologies.com>
2026-03-03 16:45:31 +01:00
340e7c3e00 fix: new workflow CI (#114)
* update: release

* fix

* fix

* fix

* Update docker.yml

* fix

---------

Co-authored-by: Théo LAGACHE <theo.lagache@soluce-technologies.com>
2026-03-03 16:39:59 +01:00
charlesgauthereau dcdce63e15 chore(release): 1.4.3 2026-03-02 20:30:10 +01:00
Charles GTEandGitHub c2eaed757d Merge pull request #109 from Portabase/dev
dev
2026-03-02 20:29:30 +01:00
charlesgauthereau 920743bea7 fix: backup-button.tsx and s3.schema.ts 2026-03-02 20:28:47 +01:00
Charles GTEandGitHub 0970f5baae Merge pull request #105 from Portabase/feat/provider-validation
Feat/provider validation
2026-03-02 20:05:14 +01:00
Charles GTEandGitHub 291aa13e9a Merge pull request #107 from Portabase/feat/sensible-actions
Feat/sensible actions
2026-03-02 20:03:25 +01:00
Théo LAGACHE e644cde514 update: sensible action button confirm 2026-03-02 11:21:20 +01:00
Théo LAGACHE 2350e68f4f update: sensible action button confirm 2026-03-02 11:19:11 +01:00
Théo LAGACHE cce3b681e3 old 2026-03-02 11:18:42 +01:00
Théo LAGACHE 755799f19d remove: old edit page 2026-03-02 11:18:21 +01:00
Théo LAGACHE ec02dc8579 fix: button confirm 2026-03-02 11:17:54 +01:00
Théo LAGACHE 7028982ba1 add: provider validation 2026-03-02 10:30:57 +01:00
Théo LAGACHE f04503e38e add: topic channel id support 2026-03-02 10:27:21 +01:00
Charles GTEandGitHub f1edffed17 Update README.md 2026-03-02 08:46:12 +01:00
charlesgauthereau fac87acee7 chore(release): 1.4.2 2026-02-28 17:54:45 +01:00
charlesgauthereau c46dd91ede Merge branch 'main' into dev 2026-02-28 17:53:58 +01:00
Charles GTEandGitHub 648753f8cf Merge pull request #99 from formless63/fix/ntfy-priority-formatting-tags
fix(ntfy): fix silent delivery, malformed payload, and tag fallthrough
2026-02-28 17:51:54 +01:00
charlesgauthereau 76b3bda313 chore: ci 2026-02-28 17:50:59 +01:00
charlesgauthereau 8102dd1a8f Merge branch 'main' into dev 2026-02-28 17:41:39 +01:00
charlesgauthereau aceb4e7f4c fix: status in sendNotificationsBackupRestore 2026-02-28 17:39:20 +01:00
charlesgauthereau 4224389e96 fix: add PasswordInput in ntfy.form.tsx 2026-02-28 17:22:02 +01:00
formless63andGitHub 7c66c10b6d fix(ntfy): fix silent delivery, malformed payload, and tag fallthrough
### Description
Fixes three issues with the `ntfy` notification provider to improve mobile delivery and readability.

**Previous Behavior:**
1. **Silent Delivery:** Priority was hardcoded to `1` (Min). On Android and iOS, priority 1 notifications are delivered completely silently (no sound, no vibration, hidden from lock screen). 
2. **Malformed Payload:** The `payload.data` object was appended as a raw, stringified JSON blob, making the notification body difficult to read.
3. **Tag Fallthrough:** The chained ternary logic for tags caused `error` level events to fall through to the default `information_source` icon.

### Changes Made
- **Dynamic Priority Mapping:** Added `getPriority` to map severity levels to Ntfy's 1-5 scale. `info`/default maps to `2` (quiet ping), `warning` to `3` (default alert), `error` to `4` (high), and `critical` to `5` (max).
- **Plain Text Data Formatting:** Added `formatData` to parse the JSON payload into a clean `- key: value` list. Markdown was purposely avoided to ensure clean rendering in mobile OS lock screen previews.
- **Improved Tagging:** Refactored into a `getTags` helper. Added a base `floppy_disk` icon to instantly identify the notifications, and properly mapped `error` payloads to the `x` icon.

### Related Issues
None (Direct Fix)
2026-02-28 10:31:18 -05:00
charlesgauthereau bf06a2e50c fix: .env.example 2026-02-27 18:42:14 +01:00
Charles GTEandGitHub 8c69fdef95 Merge pull request #98 from Portabase/dev
dev
2026-02-27 17:38:35 +01:00
charlesgauthereau 883c1d2f57 chore(release): 1.4.1 2026-02-27 17:36:05 +01:00
charlesgauthereau e7f1652b4e fix(auth): linking default 2026-02-27 17:02:20 +01:00
charlesgauthereau ea07224a52 fix: docker-compose.prod.yml 2026-02-27 16:25:53 +01:00
charlesgauthereau b655154182 chore(release): 1.4.0 2026-02-27 16:10:51 +01:00
Charles GTEandGitHub a173892163 Merge pull request #75 from Portabase/feat/oidc
add: oidc,passkey,disable email/password,sign-up
2026-02-27 16:07:14 +01:00
Théo LAGACHE 14f48c4a35 Update profile-security.tsx 2026-02-27 15:38:55 +01:00
Théo LAGACHE 88aa4daf74 Update profile-security.tsx 2026-02-27 15:31:37 +01:00
Théo LAGACHE 25326dc9be Update profile-security.tsx 2026-02-27 15:30:55 +01:00
Théo LAGACHE 030cd13f7c fix: passkey, error mapping 2026-02-27 15:17:57 +01:00
Théo LAGACHE d557620c81 fix: passkey table 2026-02-27 14:58:52 +01:00
charlesgauthereau 3273f8dd11 fix: pocket 2026-02-27 14:46:16 +01:00
Théo LAGACHE 646719557c fix: disable register,link/unlink 2026-02-27 11:49:38 +01:00
Théo LAGACHE 1640b326be Update master-realm.json 2026-02-27 11:36:38 +01:00
Théo LAGACHE cc19c9ec19 fix 2026-02-27 11:34:09 +01:00
Théo LAGACHE bb84ab9697 fix: change port 2026-02-27 10:54:10 +01:00
Théo LAGACHE 0f7664201a Update Makefile 2026-02-27 10:07:12 +01:00
Théo LAGACHE 0b8a8213e6 Update Makefile 2026-02-27 10:07:05 +01:00
Théo LAGACHE 476b3da66b fix 2026-02-27 10:03:45 +01:00
Théo LAGACHE 41c632ccfa Update auth.ts 2026-02-27 09:53:24 +01:00
Théo LAGACHE dbf5addf12 fix: lower case role provisioning 2026-02-27 09:51:42 +01:00
Théo LAGACHE 8d414080a4 fix: toast error message,social legacy 2026-02-26 15:29:00 +01:00
Théo LAGACHE c8d390726c fix: error layout global app 2026-02-25 13:00:52 +01:00
Théo LAGACHE 31285dd0d6 fix: link social, admin delete account,update 2026-02-25 09:58:15 +01:00
Théo LAGACHE b9ef58044e Merge branch 'dev' into feat/oidc 2026-02-24 16:49:16 +01:00
charlesgauthereau e320f74dfc chore(release): 1.3.0 2026-02-24 08:44:33 +01:00
charlesgauthereau b9deb3cdf0 chore(release): 1.3.0-rc.1 2026-02-23 21:56:38 +01:00
Charles GTEandGitHub 5dbf293e1e Merge pull request #94 from Portabase/feat/sqlite
feat: add sqlite support
2026-02-23 21:50:37 +01:00
Théo LAGACHE 47f2308acc fix: improve oidc,fix truncate 2026-02-23 16:47:01 +01:00
Théo LAGACHE 496be692e7 fix 2026-02-23 12:04:48 +01:00
Théo LAGACHE 5df6dd169a fix 2026-02-23 12:04:26 +01:00
charlesgauthereau 7232f17cb7 feat: add sqlite support, first working version. 2026-02-22 21:46:54 +01:00
charlesgauthereau 5b1c3d0606 chore(release): 1.2.9 2026-02-19 21:13:33 +01:00
Charles GTEandGitHub 8e6ca64fa2 Merge pull request #93 from Portabase/fix/s3-port
fix: type error when user add port for s3 storages.
2026-02-19 21:12:09 +01:00
charlesgauthereau 9116b4d126 fix: type error when user add port for s3 storages. 2026-02-19 21:09:56 +01:00
charlesgauthereau 4ce38d9857 chore(release): 1.2.8 2026-02-19 13:49:16 +01:00
charlesgauthereau 8811c48384 Merge branch 'dev'
# Conflicts:
#	README.md
2026-02-19 13:48:20 +01:00
Killian LarcherandGitHub 078d0d1ee2 Update demo video link and dashboard image 2026-02-19 11:51:08 +01:00
Killian LarcherandGitHub 56d755afa6 Change demo video link and update image
Updated demo link and image in README.
2026-02-19 11:35:41 +01:00
Killian LarcherandGitHub 9e3abb366f Change dashboard image to video link
Updated the dashboard image link to a video.
2026-02-19 11:34:10 +01:00
charlesgauthereau 4ebac1c46f fix: ts-ignore 2026-02-18 19:09:20 +01:00
Charles GTEandGitHub 1476df7a1f Merge pull request #91 from Portabase/feat/manage-master-key
feat(master_key): download master key file
2026-02-18 19:08:17 +01:00
charlesgauthereau 833872603a feat(master_key): download master key file 2026-02-18 19:07:14 +01:00
charlesgauthereau 737834310b fix 2026-02-18 18:24:56 +01:00
charlesgauthereau 4474c8ee5e Merge branch 'dev' into feat/oidc
# Conflicts:
#	portabase.config.ts
#	src/env.mjs
2026-02-18 17:59:19 +01:00
Charles GTEandGitHub ebe5c63a7b Merge pull request #90 from Portabase/fix/add-backup-cleanup
fix/add-backup-cleanup
2026-02-18 17:49:27 +01:00
charlesgauthereau afd98ae652 chore(release): 1.2.8-rc.1 2026-02-17 08:48:52 +01:00
charlesgauthereau 8f905c8128 fix: stuck backups with ongoing status but deleted, for legacy. 2026-02-17 08:47:57 +01:00
charlesgauthereau f43f30c9b9 chore(release): 1.2.7 2026-02-16 16:40:01 +01:00
charlesgauthereau 6b1bea9b17 Merge remote-tracking branch 'origin/main' 2026-02-16 16:39:31 +01:00
charlesgauthereau cdaed9766e fix: twice email sent by better auth 2026-02-16 16:39:19 +01:00
Charles GTEandGitHub d31a9ab2d2 Merge pull request #87 from Portabase/dev
1.2.7
2026-02-16 16:38:36 +01:00
Killian LarcherandGitHub 07d49749fb Merge pull request #86 from Portabase/fix/test-button-notif
Fix/test button notif
2026-02-16 16:35:44 +01:00
killianlarcher a1f1da8a07 chore(release): 1.2.7-rc.3 2026-02-16 16:17:30 +01:00
killian-larcher 1b791fd287 fix: re-enable Test Button when editing notification channel. 2026-02-16 16:14:41 +01:00
charlesgauthereau 2a4af58a51 Merge remote-tracking branch 'origin/dev' into dev 2026-02-16 15:37:37 +01:00
charlesgauthereau b12f8ca044 chore(release): 1.2.7-rc.2 2026-02-16 15:37:06 +01:00
Charles GTEandGitHub 519b3abd1d Merge pull request #84 from Portabase/fix/twice-email-session
fix: twice email sent by better auth
2026-02-16 15:36:06 +01:00
charlesgauthereau e08a5bd6b6 fix: twice email sent by better auth 2026-02-16 15:34:31 +01:00
Killian LarcherandGitHub f6c597b42d Merge pull request #83 from Portabase/fix/optional-csp
Conditionally enable UPGRADE_INSECURE_REQUESTS based on PROJECT_URL scheme
2026-02-16 15:23:49 +01:00
killianlarcher f3b9032174 chore(release): 1.2.7-rc.1 2026-02-16 13:23:36 +01:00
killianlarcher 44b4ee2953 fix(csp): Conditionally sets UPGRADE_INSECURE_REQUESTS based on the PROJECT_URL scheme. 2026-02-16 13:21:09 +01:00
killianlarcher b77f79f42d fix(csp): Conditionally sets UPGRADE_INSECURE_REQUESTS based on the PROJECT_URL scheme. 2026-02-16 13:18:58 +01:00
Charles GTEandGitHub 8017bf7c8a Merge pull request #80 from Portabase/dev
fix: README.md
2026-02-15 11:50:54 +01:00
charlesgauthereau 85f4cda752 fix: README.md 2026-02-15 11:49:01 +01:00
charlesgauthereau 1fd2f553ed chore(release): 1.2.6 2026-02-15 10:59:43 +01:00
Charles GTEandGitHub d68170eda8 Merge pull request #79 from Portabase/dev
Dev
2026-02-15 10:59:09 +01:00
Charles GTEandGitHub 51f27226e7 Merge branch 'main' into dev 2026-02-15 10:48:37 +01:00
charlesgauthereau d54c366323 chore(release): 1.2.6-rc.2 2026-02-15 10:21:24 +01:00
Charles GTEandGitHub 0e7cb9fbab Merge pull request #78 from Portabase/feat/internal-database
feat/internal-database
2026-02-15 10:20:25 +01:00
charlesgauthereau 63e83032f1 fix: working on adding internal postgres database. 2026-02-15 10:19:28 +01:00
charlesgauthereau b4da6fdd9f fix: working on adding internal postgres database. 2026-02-14 22:23:04 +01:00
charlesgauthereau 12535a59e8 fix: docker-compose.prod.yml 2026-02-14 19:22:24 +01:00
Charles GTEandGitHub b345eda7aa Update README.md 2026-02-14 09:32:02 +01:00
Charles GTEandGitHub 660c1dcadf Merge pull request #76 from Portabase/fix/backup-delete
fix: bug ongoing backup deletion
2026-02-13 22:41:48 +01:00
charlesgauthereau be66856034 chore(release): 1.2.6-rc.1 2026-02-13 22:41:29 +01:00
charlesgauthereau 80e3027107 fix: bug ongoing backup deletion 2026-02-13 22:40:44 +01:00
Théo LAGACHE 3bf731055f add: oidc,passkey,disable email/password,sign-up 2026-02-13 16:48:13 +01:00
charlesgauthereau 362ca53efb fix: console.log 2026-02-13 15:58:53 +01:00
charlesgauthereau 7ab213e5e4 chore(release): 1.2.5 2026-02-13 14:35:11 +01:00
Charles GTEandGitHub f0eafb1162 Merge pull request #74 from Portabase/dev
dev
2026-02-13 14:31:52 +01:00
charlesgauthereau ad5baa7d6f Merge branch 'main' into dev 2026-02-13 14:31:03 +01:00
Charles GTEandGitHub 5840e30afb Merge pull request #73 from Portabase/feat/ui-ux-refactoring
feat/ui-ux-refactoring
2026-02-13 14:30:16 +01:00
charlesgauthereau 8f95606dd7 fix: review 2026-02-13 14:28:55 +01:00
Théo LAGACHE ca9bc7ae94 fix: policies reload 2026-02-13 11:24:53 +01:00
charlesgauthereau 288a40e697 chore(release): 1.2.5-rc.16 2026-02-12 20:25:29 +01:00
Charles GTEandGitHub fa91e8c289 Merge pull request #72 from Portabase/fix/encryption
fix/encryption
2026-02-12 20:20:25 +01:00
charlesgauthereau 4ab5fe4c6f fix: .gitignore 2026-02-12 20:18:56 +01:00
charlesgauthereau d87782d180 fix: Adding encryption of the settings. 2026-02-12 20:18:26 +01:00
charlesgauthereau 1c97c97fa1 fix: refactoring. 2026-02-12 19:07:34 +01:00
charlesgauthereau 2426166676 fix: working on AEA-GCM encryption. 2026-02-12 18:46:11 +01:00
Théo LAGACHE 6f6c1c7c52 fix 2026-02-12 15:48:51 +01:00
charlesgauthereau 7b9cb2ecb9 Merge remote-tracking branch 'origin/feat/fix-react-cve' into feat/agent-update 2026-02-11 19:42:57 +01:00
charlesgauthereau fc029af654 Merge branch 'feature' into feat/agent-update 2026-02-11 19:42:49 +01:00
Théo LAGACHE 110a69dc4b fix: react cve 19.2.4 2026-02-11 15:43:03 +01:00
Théo LAGACHE a61791fdb2 update: fix ui sidebar 2026-02-11 14:42:07 +01:00
charlesgauthereau c01440f9d4 chore(release): 1.2.5-rc.15 2026-02-11 12:34:12 +01:00
Charles GTEandGitHub 39584dd7a1 Merge pull request #69 from Portabase/feature
UI/UX enhancements
2026-02-11 12:33:16 +01:00
charlesgauthereau d9badeacba Merge branch 'feature' into feat/agent-update 2026-02-11 12:21:22 +01:00
Charles GTEandGitHub d4cde2ba9c Merge pull request #66 from Portabase/feat/refresh
Feat/refresh pagination
2026-02-11 12:18:52 +01:00
charlesgauthereau 824ef4528e fix: review and refactoring. 2026-02-11 12:18:25 +01:00
charlesgauthereau feb06a85ae Merge branch 'feature' into feat/refresh
# Conflicts:
#	app/api/agent/[agentId]/backup/route.ts
#	app/api/events/route.ts
#	src/components/wrappers/dashboard/database/backup/actions/backup-actions-form.tsx
#	src/components/wrappers/dashboard/database/backup/actions/backup-actions.action.ts
#	src/components/wrappers/dashboard/database/channels-policy/policy-form.tsx
#	src/lib/tasks/database/index.ts
2026-02-11 12:10:33 +01:00
Charles GTEandGitHub cfad645e33 Merge pull request #68 from Portabase/fix/large-files-storage-backend-v3
fix/large-files-storage-backend-v3
2026-02-11 12:00:57 +01:00
charlesgauthereau 675f8dec20 Merge branch 'main' into fix/large-files-storage-backend-v3
# Conflicts:
#	src/features/storages/providers/s3.ts
2026-02-11 11:54:09 +01:00
charlesgauthereau 11e85b613c chore(release): 1.2.5-rc.14 2026-02-10 20:50:33 +01:00
charlesgauthereau eb82833c7e chore(release): 1.2.5-rc.13 2026-02-10 20:47:04 +01:00
charlesgauthereau 399a3e09de chore(release): 1.2.5-rc.12 2026-02-10 20:39:55 +01:00
charlesgauthereau 1a735bda42 chore(release): 1.2.5-rc.11 2026-02-10 20:39:09 +01:00
charlesgauthereau 991196cc30 chore(release): 1.2.5-rc.10 2026-02-10 20:34:26 +01:00
charlesgauthereau 5f48903a73 chore(release): 1.2.5-rc.9 2026-02-10 20:25:54 +01:00
charlesgauthereau 5dfe1b758e fix: refactoring 2026-02-10 19:11:01 +01:00
Théo LAGACHE d24e534828 update: ui 2026-02-10 17:02:06 +01:00
Théo LAGACHE 80bdca138e add: agent update notifier, pagination 2026-02-10 15:52:08 +01:00
Théo LAGACHE 0a2ca8c0b6 add: font 2026-02-10 15:29:20 +01:00
Théo LAGACHE 921a355ae0 fix: project details table refresh 2026-02-10 11:44:45 +01:00
charlesgauthereau eff7b24b7a fix: nginx configration 2026-02-09 23:39:14 +01:00
charlesgauthereau c328620f3d fix: new storage architecture 2026-02-08 20:42:42 +01:00
Charles GTEandGitHub a49b6d6573 Merge pull request #61 from Portabase/revert-57-fix/s3
Revert "chore(release): 1.2.5-rc.1"
2026-02-04 09:57:01 +01:00
Charles GTEandGitHub baffd2efaf Revert "chore(release): 1.2.5-rc.1" 2026-02-04 09:56:34 +01:00
Charles GTEandGitHub 292693a1ed Merge pull request #57 from Portabase/fix/s3
chore(release): 1.2.5-rc.1
2026-02-04 09:56:11 +01:00
charlesgauthereau cdd01e695c fix: new storage architecture 2026-02-02 21:46:53 +01:00
charlesgauthereau 614a9e057c chore(release): 1.2.5-rc.8 2026-02-01 22:04:07 +01:00
charlesgauthereau 24d90bb750 chore(release): 1.2.5-rc.7 2026-02-01 21:14:35 +01:00
charlesgauthereau c2726e6b18 chore(release): 1.2.5-rc.6 2026-02-01 20:22:38 +01:00
charlesgauthereau a746ce9169 chore(release): 1.2.5-rc.5 2026-02-01 19:59:23 +01:00
charlesgauthereau c00b967179 chore(release): 1.2.5-rc.4 2026-02-01 19:56:02 +01:00
charlesgauthereau f6e749ecf9 chore(release): 1.2.5-rc.3 2026-02-01 19:51:16 +01:00
charlesgauthereau 8f5e0686c8 chore(release): 1.2.5-rc.3 2026-02-01 19:16:42 +01:00
charlesgauthereau b9f0d9279b chore(release): 1.2.5-rc.2 2026-02-01 18:25:00 +01:00
charlesgauthereau cd69f0c82f chore(release): 1.2.5-rc.1 2026-02-01 18:24:53 +01:00
charlesgauthereau 884746f3a5 chore(release): 1.2.5-rc.2 2026-02-01 18:24:22 +01:00
charlesgauthereau 9c4d862297 fix: refactoring, adding express api. 2026-02-01 15:53:50 +01:00
charlesgauthereau cc87cf68b5 fix: refactoring, adding express api. 2026-01-31 22:15:01 +01:00
killianlarcher db1a0235ed chore(release): 1.2.5-rc.1 2026-01-31 14:18:12 +01:00
Killian LarcherandGitHub 2cd6a7cae0 Merge pull request #56 from Portabase/fix/s3
fix(storage): add test button when creating channel (not only for existing one) and add region parameter for S3.
2026-01-31 13:03:58 +01:00
killianlarcher bd80fdc960 fix(storage): add test button when creating channel (not only for existing one) and add region parameter for S3. 2026-01-31 12:41:01 +01:00
charlesgauthereau 7c5d57ddc1 Merge branch 'main' into feature
# Conflicts:
#	CITATION.cff
#	package.json
2026-01-29 23:48:18 +01:00
charlesgauthereau 5653ff18c8 chore(release): 1.2.4 2026-01-29 23:47:56 +01:00
Charles GTEandGitHub 83ea444675 Merge pull request #52 from Portabase/fix/dialogs
fix/dialogs
2026-01-29 23:46:53 +01:00
charlesgauthereau 966edfa85a fix: some fix responsive 2026-01-29 23:46:04 +01:00
charlesgauthereau 30b9fc7765 fix: server/client error on some dialogs. 2026-01-29 23:11:33 +01:00
charlesgauthereau 95a60e9a96 fix: server/client error on some dialogs. 2026-01-29 19:10:59 +01:00
Killian LarcherandGitHub 9327331adc Merge pull request #51 from Portabase/feat/github-updater
fix: check if update is available based on auto-detected version channel.
2026-01-28 21:43:47 +01:00
killian-larcher e8baa441cd fix: check if update is available based on auto-detected version channel. 2026-01-28 21:40:55 +01:00
Killian LarcherandGitHub 61d142afbd Merge pull request #50 from Portabase/feat/github-updater
fix: check if update is available based on auto-detected version channel
2026-01-28 20:23:06 +01:00
killian-larcher 46022e8711 fix: check if update is available based on auto-detected version channel. 2026-01-28 20:22:14 +01:00
Killian LarcherandGitHub e59f3c46fd Merge pull request #49 from Portabase/feat/organization-dropdown
fix: adjust organisation dropdown size.
2026-01-28 19:49:23 +01:00
killian-larcher 7fcb3f2c31 fix: adjust organisation dropdown size. 2026-01-28 19:48:40 +01:00
Killian LarcherandGitHub a2243585a0 Merge pull request #48 from Portabase/feat/github-updater
Feat/GitHub updater
2026-01-28 19:18:22 +01:00
killian-larcher 6231002043 fix: error in package.json 2026-01-28 19:17:31 +01:00
killian-larcher 33eb476009 Merge branch 'main' into feat/github-updater
# Conflicts:
#	CITATION.cff
#	package.json
2026-01-28 19:14:33 +01:00
Killian LarcherandGitHub ebb0a4c7e5 Merge pull request #47 from Portabase/feat/modal-page
Feat/modal page
2026-01-28 19:07:55 +01:00
Théo LAGACHE d010ed6bc5 chore(release): 1.2.4-rc.8 2026-01-28 16:55:43 +01:00
Théo LAGACHE 1941e29175 chore(release): 1.2.4-rc.7 2026-01-28 16:39:53 +01:00
Théo LAGACHE 6e8932ff52 chore(release): 1.2.4-rc.6 2026-01-28 16:39:46 +01:00
Théo LAGACHE f9bfae9b9d chore(release): 1.2.4-rc.5 2026-01-28 15:56:24 +01:00
Théo LAGACHE 2fca928cf0 chore(release): 1.2.4-rc.4 2026-01-28 15:55:57 +01:00
Théo LAGACHE 8f3a362a3a chore(release): 1.2.4-rc.3 2026-01-28 15:55:28 +01:00
Théo LAGACHE 62a6189788 chore(release): 1.2.4-rc.3 2026-01-28 15:54:59 +01:00
Théo LAGACHE 2a6d631719 chore(release): 1.2.4-rc.1 2026-01-28 15:54:03 +01:00
Théo LAGACHEandGitHub 4942591d7f Merge pull request #45 from Portabase/feat/organization-dropdown
update: new org combobox
2026-01-28 13:57:08 +01:00
Théo LAGACHE 1760c1b852 update: new org combobox 2026-01-28 13:51:14 +01:00
charlesgauthereau 86d7ab9864 chore(release): 1.2.4-rc.2 2026-01-24 19:34:56 +01:00
charlesgauthereau f201e4593a Merge branch 'main' into feature 2026-01-24 19:34:49 +01:00
charlesgauthereau 71129e556e chore(release): 1.2.4-rc.1 2026-01-24 19:06:47 +01:00
Charles GTEandGitHub 0e7ebb79db Merge pull request #42 from Portabase/fix/ntfy
fix: added support for Basic Auth ntfy instance
2026-01-24 19:05:46 +01:00
charlesgauthereau 727d9eb57c fix: added support for Basic Auth ntfy instance 2026-01-24 19:04:49 +01:00
charlesgauthereau 228ff75468 chore(release): 1.2.3 2026-01-24 13:18:01 +01:00
Charles GTEandGitHub 80018cac08 Merge pull request #40 from Portabase/feat/google-drive-storage
feat/google-drive-storage
2026-01-24 13:17:06 +01:00
charlesgauthereau b2c19e7ed0 fix: refactoring of storage method 2026-01-24 13:14:38 +01:00
charlesgauthereau a64a80910d feat: Working version with google drive storage. 2026-01-24 12:13:55 +01:00
charlesgauthereau 4eeeef3427 feat: start working on google drive storage 2026-01-24 00:26:09 +01:00
charlesgauthereau 657586ddff feat: start working on google drive storage 2026-01-24 00:26:05 +01:00
Charles GTEandGitHub 797b287899 chore: README.md 2026-01-22 10:40:09 +01:00
Charles GTEandGitHub db2d95f2c5 Create FUNDING.yml
create and added buy me a coffee sponsor
2026-01-22 10:31:22 +01:00
charlesgauthereau 16a8f259ab chore(release): 1.2.2 2026-01-21 21:29:33 +01:00
Charles GTEandGitHub ee1cd8d08d Merge pull request #36 from Portabase/feat/arm-architecture
feat/arm-architecture
2026-01-21 21:22:45 +01:00
charlesgauthereau e53046324c fix: docker-compose.prod.yml 2026-01-21 21:21:51 +01:00
charlesgauthereau dd71977317 chore(release): 1.2.2-rc.2 2026-01-21 21:00:58 +01:00
charlesgauthereau bee0da1454 chore(release): 1.2.2-rc.2 2026-01-21 20:56:58 +01:00
charlesgauthereau b60feaa447 feat: added support for arm and amd docker images. 2026-01-21 20:56:23 +01:00
charlesgauthereau 743e54614a chore(release): 1.2.2-rc.1 2026-01-21 11:41:17 +01:00
charlesgauthereau bd96713af4 fix: some refactoring 2026-01-21 11:41:00 +01:00
Charles GTEandGitHub 8f7f9b457a Merge pull request #33 from Portabase/feat/avatar-upload
fix: Upload avatar image error for s3 storage
2026-01-21 11:38:47 +01:00
charlesgauthereau 4c0d68eab8 fix: Upload avatar image error for s3 storage 2026-01-21 11:36:50 +01:00
Charles GTEandGitHub 8211407cf2 chore: README.md 2026-01-20 22:59:47 +01:00
charlesgauthereau 5fb6c7d36a chore(release): 1.2.1 2026-01-20 22:53:08 +01:00
Charles GTEandGitHub 30b1cf4493 Merge pull request #31 from Portabase/feat/add-mongodb
feat/add-mongodb
2026-01-20 22:51:36 +01:00
charlesgauthereau f4789ac5fe fix: attribute default storage at startup in init. Adding support for mongodb. 2026-01-20 22:47:23 +01:00
charlesgauthereau 5fd826ae71 fix: attribute default storage at startup in init. 2026-01-20 20:31:42 +01:00
charlesgauthereau 22ce08f06c Merge branch 'feature' 2026-01-20 19:50:40 +01:00
charlesgauthereau 696c88f03f chore(release): 1.2.1-rc.3 2026-01-20 19:43:59 +01:00
charlesgauthereau 9d5d87e47b fix: smtp error for SMTP_SECURE false. Added in .env 2026-01-20 19:41:49 +01:00
charlesgauthereau e32539e02c chore(release): 1.2.1-rc.2 2026-01-19 13:58:35 +01:00
charlesgauthereau 46ba931a6c fix: Retention cron error. 2026-01-19 13:58:04 +01:00
charlesgauthereau 4bac5143d6 feat: working on mongodb 2026-01-18 21:19:19 +01:00
charlesgauthereau bcb1674491 feat: adding support for mongodb 2026-01-18 20:20:10 +01:00
Killian LarcherandGitHub 3cbd9677c2 Merge pull request #28 from Portabase/feat/coming-soon-storage-preview
feat(ui): add preview for upcoming storage provider
2026-01-18 16:08:41 +01:00
killianlarcher b216ee1a80 feat(ui): add preview for upcoming storage provider 2026-01-18 16:00:13 +01:00
charlesgauthereau 32ec482449 Merge branch 'feature' 2026-01-18 15:30:30 +01:00
charlesgauthereau 5074748055 chore(release): 1.2.1-rc.1 2026-01-18 15:30:14 +01:00
charlesgauthereau 6fa11f834b fix: retention delete backup cron action 2026-01-18 15:29:29 +01:00
charlesgauthereau 25c7702127 chore(release): 1.2.0 2026-01-18 15:21:37 +01:00
charlesgauthereau b546556d08 chore(release): 1.2.0 2026-01-18 15:04:11 +01:00
charlesgauthereau 379a85f6f6 chore: .env.example 2026-01-18 15:03:54 +01:00
Charles GTEandGitHub 92d2c14d6a Merge pull request #27 from Portabase/feat/storage
Feat/storage
2026-01-18 15:01:52 +01:00
charlesgauthereau a6c1f7d3f2 chore: Merging errors due to merge with main. 2026-01-18 15:00:53 +01:00
charlesgauthereau 6fa6187cc4 Merge remote-tracking branch 'origin/main' into feat/storage
# Conflicts:
#	src/components/wrappers/dashboard/admin/channels/channel/channel-form/channel-form.tsx
#	src/components/wrappers/dashboard/admin/notifications/helpers.tsx
#	src/db/migrations/meta/0020_snapshot.json
#	src/db/migrations/meta/_journal.json
2026-01-18 14:40:26 +01:00
charlesgauthereau 848e7a1609 chore: .gitignore 2026-01-18 14:35:39 +01:00
Charles GTEandGitHub 3e8c0a2896 Delete .vscode directory 2026-01-18 14:20:12 +01:00
Charles GTEandGitHub 11900b1500 Delete .idea directory 2026-01-18 14:19:16 +01:00
charlesgauthereau 85044fe3e0 chore(release): 1.2.0-rc.1 2026-01-18 14:16:40 +01:00
charlesgauthereau c0499d40ea fix: restoration columns.tsx is not backup storage linked, legacy 2026-01-18 14:14:49 +01:00
charlesgauthereau 8ee44ffea1 fix: migration errors by testing on a production instance. 2026-01-18 14:10:35 +01:00
Killian LarcherandGitHub 5668a1411f Merge pull request #26 from Portabase/feat/coming-soon-preview
feat(ui): add preview for upcoming notification provider
2026-01-18 12:37:29 +01:00
killianlarcher 6db60f111d feat(ui): add preview for upcoming notification provider 2026-01-18 12:36:12 +01:00
charlesgauthereau 3d695709d5 fix: migration storage backend and also user avatar upload. 2026-01-18 11:45:36 +01:00
Killian LarcherandGitHub 5c682addc9 Merge pull request #25 from Portabase/fix/profile
fix(ui) : improve profile modal
2026-01-18 10:29:38 +01:00
Killian LarcherandGitHub 24810cc818 Merge pull request #24 from Portabase/feat/last-connected
feat : add last login date
2026-01-18 10:29:19 +01:00
Killian LarcherandGitHub 8caeb11da5 Merge pull request #21 from Portabase/feat/connection-indicator
feat(ui): add pulse effect to connection indicator
2026-01-18 10:28:52 +01:00
Killian LarcherandGitHub 5e07b446fc Merge pull request #23 from Portabase/fix/development-conf
fix: update configuration for local development
2026-01-18 10:28:37 +01:00
charlesgauthereau f29444a219 fix: deleteBackupCronAction with new backend storage system 2026-01-18 09:48:14 +01:00
charlesgauthereau bce1c5d9b8 feat: adding storage system ping method in order to test. 2026-01-17 22:41:39 +01:00
charlesgauthereau 5b980fb382 feat: adding system settings page. And did some refactoring. 2026-01-17 22:26:44 +01:00
charlesgauthereau e63e62d3e0 fix: Sidebar focus on some items 2026-01-17 21:13:54 +01:00
charlesgauthereau 53f82792d8 fix: Upload with new storage backend system. 2026-01-17 20:56:57 +01:00
killianlarcher 760388307c fix(ui): reduce opacity on avatar hover and add bottom padding to profile modal 2026-01-17 20:40:57 +01:00
killianlarcher eebd8500ee feat: add lastConnectedAt to user schema and display it in user settings 2026-01-17 20:27:06 +01:00
killianlarcher 12112afa37 feat(ui):resize connection indicator and add pulse effect for all use cases. 2026-01-17 20:06:01 +01:00
charlesgauthereau 424fa581b9 feat: backup delete/restore actions. 2026-01-17 19:55:17 +01:00
killianlarcher cddb0a7e05 feat(ui): add pulse effect to connection indicator 2026-01-17 19:36:28 +01:00
charlesgauthereau 83ef0ee74e feat: backup delete/restore actions. 2026-01-17 19:23:15 +01:00
killianlarcher 21b317e4f1 fix: Update .env.example and docker-compose.yml for local development configuration 2026-01-17 17:44:31 +01:00
charlesgauthereau 7d818206b0 refactor: API route for backup/restore 2026-01-17 17:27:33 +01:00
charlesgauthereau 0216c40bfe feat: Working on UI/UX for multiple storage backends. 2026-01-17 15:57:37 +01:00
charlesgauthereau ecf80280f1 Merge branch 'main' into feat/storage 2026-01-16 23:21:04 +01:00
charlesgauthereau 6e9568b4fb Merge branch 'main' into feat/storage 2026-01-16 22:53:33 +01:00
charlesgauthereau 8d968e7d63 Merge branch 'main' into feature 2026-01-16 22:53:13 +01:00
charlesgauthereau bb264f0b9b chore(release): 1.1.13 2026-01-16 22:48:40 +01:00
Soluce TechnologiesandGitHub be4f770c35 Merge pull request #19 from Portabase/feat/no-email-config-error
Do not crash if no email config is provided in .env file.
2026-01-16 21:44:22 +00:00
charlesgauthereau 17d955dd69 chore(release): 1.1.13-rc.2 2026-01-16 22:24:08 +01:00
charlesgauthereau 3f31e85c06 fix: Do not crash if no email config is provided in .env file. 2026-01-16 22:22:38 +01:00
charlesgauthereau 5a0c230ead chore: working on backup api for new storage system. 2026-01-16 21:54:16 +01:00
Théo LAGACHEandGitHub e10350268d Merge pull request #17 from Portabase/feat/guard-cookie
fix: bad cookie name with https
2026-01-16 11:34:11 +00:00
Théo LAGACHE 969a48bef2 chore(release): 1.1.13-rc.1 2026-01-16 12:21:50 +01:00
Théo LAGACHE 8158934cb9 fix: bad cookie name with https 2026-01-16 12:15:18 +01:00
charlesgauthereau 670f6a1974 chore: working on backup api for new storage system. 2026-01-15 21:27:36 +01:00
charlesgauthereau dd1f791b3e chore: migration for the storage-backend feature 2026-01-14 23:01:06 +01:00
charlesgauthereau 91f25bc25d feat: Working on storage modal in database page in order to add multiple backend storages. Adding a system to manage independently channels from organizations and from system admin. 2026-01-14 21:54:13 +01:00
charlesgauthereau d8e9e2dac3 feat: Working on backend storage providers. 2026-01-13 22:22:01 +01:00
charlesgauthereau 67a89a12ac feat: backend storage 2026-01-12 21:16:57 +01:00
charlesgauthereau faaa4033bd feat: backend storage 2026-01-12 21:16:52 +01:00
charlesgauthereau f3eedf6f30 fix(ci): docker.yml 2026-01-11 14:30:52 +01:00
charlesgauthereau 29718d2a0c chore(release): 1.1.12 2026-01-11 14:26:10 +01:00
charlesgauthereau c7e6bd8b72 Merge branch 'feature'
# Conflicts:
#	CITATION.cff
#	package.json
2026-01-11 14:26:01 +01:00
charlesgauthereau a67ff12786 fix(ci): docker.yml 2026-01-11 14:24:13 +01:00
charlesgauthereau c92e671622 chore(release): 1.1.12-rc.4 2026-01-11 14:19:49 +01:00
charlesgauthereau 2bd25353da chore(release): 1.1.12-rc.3 2026-01-11 14:13:58 +01:00
charlesgauthereau e1bee19c18 fix(ci): docker.yml 2026-01-11 14:13:51 +01:00
charlesgauthereau 0ec49af72a chore(release): 1.1.12-rc.2 2026-01-11 14:12:43 +01:00
charlesgauthereau 6d4eb836ab fix(ci): working on github actions to add new docker repository 2026-01-11 14:12:40 +01:00
charlesgauthereau 0c3cfecb1d chore(release): 1.1.12-rc.1 2026-01-11 13:54:35 +01:00
charlesgauthereau f9186e7354 fix: overflow in agent-card.tsx 2026-01-11 13:54:22 +01:00
charlesgauthereau bc603a08cf fix: Remove card in some forms. 2026-01-11 13:52:02 +01:00
charlesgauthereau da5c65d5a6 chore(release): 1.1.11 2026-01-11 13:41:24 +01:00
charlesgauthereau 077aada056 chore(release): 1.1.11-rc.2 2026-01-11 13:36:57 +01:00
charlesgauthereau 4ed5f9872d fix: delete backup crash if no file. 2026-01-11 13:36:11 +01:00
charlesgauthereau 2d6be20b30 fix: improving organization-form.tsx UX/UI and removed the card. 2026-01-11 12:41:39 +01:00
charlesgauthereau be8f3bd98c fix: organization settings page responsive on mobile. 2026-01-11 12:37:57 +01:00
charlesgauthereau a3a20bee4a fix: agent-card.tsx overflow on mobile. 2026-01-11 12:23:32 +01:00
charlesgauthereau 09a1e75acf fix: notifier-card.tsx overflow on mobile. 2026-01-11 12:12:14 +01:00
charlesgauthereau ce6bdd01ef fix: overflow mobile view on the notification provider card in detabase details page. 2026-01-11 11:57:18 +01:00
charlesgauthereau a03a1d635f fix: responsive in project details. 2026-01-11 10:59:42 +01:00
charlesgauthereau 419b6d4891 chore(release): 1.1.11-rc.1 2026-01-09 19:16:17 +01:00
charlesgauthereau b4840318d5 fix: database page Backup button and route api. 2026-01-09 19:15:27 +01:00
Théo LAGACHE bf81f437d1 chore(release): 1.1.10 2026-01-07 15:29:53 +01:00
Théo LAGACHEandGitHub 81a4d33df1 Merge pull request #7 from Portabase/feat/webhooks
Feat/webhooks
2026-01-07 15:29:30 +01:00
Théo LAGACHE baab364b56 chore(release): 1.1.10-rc.2 2026-01-07 15:00:21 +01:00
Théo LAGACHE 585ee041ec ui fixes 2026-01-07 15:00:11 +01:00
Théo LAGACHE da3d4350cc chore(release): 1.1.10-rc.1 2026-01-07 13:33:42 +01:00
Théo LAGACHE df9390847f add: new notifiers and ui fixes 2026-01-07 13:33:23 +01:00
Théo LAGACHE 2a1acbfde8 chore(release): 1.1.9 2026-01-07 09:50:42 +01:00
Théo LAGACHEandGitHub 16ef8507b0 Merge pull request #6 from Portabase/readme
Readme
2026-01-07 09:48:56 +01:00
Théo LAGACHE df5d4e336c update: release 2026-01-07 09:46:30 +01:00
Théo LAGACHE ad2015329b chore(release): 1.1.9-rc6 2026-01-06 16:35:47 +01:00
Théo LAGACHE 4cc4bed785 chore(release): 1.1.9-rc5 2026-01-06 16:35:44 +01:00
Théo LAGACHE 6c506e6907 chore(release): 1.1.9-rc5 2026-01-06 16:25:12 +01:00
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
charlesgauthereau 3b93257890 [Release] - 1.1.8 2026-01-05 19:41:53 +01:00
charlesgauthereau 1104e2a074 [FIX] (profile) - error in local upload (#4) 2026-01-05 19:39:59 +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
554 changed files with 111287 additions and 18303 deletions
+61
View File
@@ -0,0 +1,61 @@
# Environment
NODE_ENV=development
# Database
DATABASE_URL=postgresql://devuser:changeme@localhost:5433/devdb?schema=public
# Project
PROJECT_NAME="Portabase"
PROJECT_DESCRIPTION="Portabase is a powerful database manager"
PROJECT_URL=http://localhost:8887
PROJECT_SECRET=
# SMTP (email)
SMTP_HOST=
SMTP_PORT=
SMTP_USER=
SMTP_PASSWORD=
SMTP_FROM=
#SMTP_SECURE="true"
#Port 587 -> secure: false
#Port 465 -> secure: true
#By default : true
# OIDC SSO Authentification
AUTH_OIDC_ID=""
AUTH_OIDC_TITLE=""
AUTH_OIDC_DESC=""
AUTH_OIDC_ICON=""
AUTH_OIDC_CLIENT=""
AUTH_OIDC_SECRET=""
AUTH_OIDC_ISSUER_URL=""
AUTH_OIDC_HOST=""
AUTH_OIDC_SCOPES=""
AUTH_OIDC_DISCOVERY_ENDPOINT=""
AUTH_OIDC_JWKS_ENDPOINT=""
AUTH_OIDC_PKCE=true
ALLOWED_GROUP="admin"
AUTH_OIDC_ROLE_MAP=""
AUTH_ALLOW_LINKING=true
AUTH_ALLOW_UNLINKING=true
# Social OAuth2 Authentification
AUTH_SOCIAL_ID=""
AUTH_SOCIAL_TITLE=""
AUTH_SOCIAL_DESC=""
AUTH_SOCIAL_ICON=""
AUTH_SOCIAL_CLIENT=""
AUTH_SOCIAL_SECRET=""
AUTH_SOCIAL_APPLE_APP_BUNDLE_IDENTIFIER=""
# If true, the user's role will be updated from OIDC claims on every login.
# If false, the role is only set when the user is first created.
AUTH_SYNC_OIDC_ROLES_ON_LOGIN=true
AUTH_EMAIL_PASSWORD_ENABLED=true
AUTH_SIGNUP_ENABLED=true
AUTH_PASSKEY_ENABLED=true
# Retention
RETENTION_CRON="* * * * *"
-33
View File
@@ -1,33 +0,0 @@
# Environnement
NODE_ENV=production
# Base de données
DATABASE_URL=postgresql://devuser:changeme@db:5432/devdb?schema=public
# Projet
NEXT_PUBLIC_PROJECT_NAME="Portabase"
NEXT_PUBLIC_PROJECT_DESCRIPTION="Portabase is a powerful database manager"
NEXT_PUBLIC_PROJECT_URL=http://app.portabase.io
PROJECT_SECRET=
# SMTP (email)
SMTP_HOST=
SMTP_PORT=587
SMTP_USER=
SMTP_PASSWORD=
SMTP_FROM=
# Google
AUTH_GOOGLE_ID=
AUTH_GOOGLE_SECRET=
# S3
S3_ENDPOINT=http://app.s3.portabase.io
S3_ACCESS_KEY=
S3_SECRET_KEY=
S3_BUCKET_NAME=portabase
S3_PORT=9000
S3_USE_SSL=true
# Storage Type (s3, local)
STORAGE_TYPE=local
+4 -4
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**
@@ -72,7 +72,7 @@ If you encounter a bug or have a suggestion for improvement, follow these steps:
3. **Push your branch**
```bash
git push origin feature/<feature-name>
git push origin feat/<feature-name>
```
4. **Open a Pull Request (PR)**
@@ -82,7 +82,7 @@ If you encounter a bug or have a suggestion for improvement, follow these steps:
## Code Style Guidelines
- Follow the [specific coding style guide] (e.g., Prettier, ESLint, PEP8).
- Follow the [specific coding style guide] (e.g., Prettier, ESLint, PEP8,Biome).
- Use meaningful variable names and include comments where necessary.
- Tests before submitting your changes.
@@ -107,4 +107,4 @@ If you encounter a bug or have a suggestion for improvement, follow these steps:
Thank you for contributing! 🙌
---
---
+15
View File
@@ -0,0 +1,15 @@
# These are supported funding model platforms
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
ko_fi: # Replace with a single Ko-fi username
tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: # Replace with a single Liberapay username
issuehunt: # Replace with a single IssueHunt username
lfx_crowdfunding: # Replace with a single LFX Crowdfunding project-name e.g., cloud-foundry
polar: # Replace with a single Polar username
buy_me_a_coffee: portabase
thanks_dev: # Replace with a single thanks.dev username
custom: # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2']
+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

+105
View File
@@ -0,0 +1,105 @@
#name: Auto Release bot
#
#on:
# pull_request:
# types: [ closed ]
# branches:
# - main
#
#jobs:
# release:
# if: github.event.pull_request.merged == true
# runs-on: ubuntu-latest
# permissions:
# contents: write
#
# steps:
# - uses: actions/create-github-app-token@v1
# id: app-token
# with:
# app-id: ${{ vars.APP_ID }}
# private-key: ${{ secrets.APP_PRIVATE_KEY }}
#
# - uses: actions/checkout@v4
# with:
# fetch-depth: 0
# token: ${{ steps.app-token.outputs.token }}
# ref: ${{ github.base_ref }}
#
# - uses: pnpm/action-setup@v4
#
# - uses: actions/setup-node@v4
# with:
# node-version: "lts/*"
# cache: "pnpm"
#
# - run: pnpm install --frozen-lockfile
#
# - run: git config --global user.name 'github-actions[bot]'
# - run: git config --global user.email 'github-actions[bot]@users.noreply.github.com'
#
# - name: Push tags
# run: git push origin --tags
#
# - name: Run release-it
# run: |
# git pull origin main
# pnpm exec release-it --ci
# env:
# GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
name: Auto Release bot
on:
pull_request:
types: [closed]
branches:
- main
jobs:
release:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/create-github-app-token@v1
id: app-token
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ steps.app-token.outputs.token }}
ref: ${{ github.base_ref }}
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: "lts/*"
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- run: git config --global user.name 'github-actions[bot]'
- run: git config --global user.email 'github-actions[bot]@users.noreply.github.com'
- name: Bump version and tag
run: pnpm exec release-it --ci --no-github
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
- name: Push commits and tags
run: |
git push origin main
git push origin --tags
- name: Create GitHub release
run: pnpm exec release-it --ci --only-github
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
+70
View File
@@ -0,0 +1,70 @@
name: Discord Notification
on:
workflow_call:
inputs:
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:
notify-discord:
runs-on: ubuntu-latest
steps:
- 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"
+114
View File
@@ -0,0 +1,114 @@
name: Docker Publish
on:
workflow_call:
inputs:
image_name:
required: false
type: string
default: "portabase/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:
name: Build architectures
runs-on: ${{ matrix.platform == 'linux/amd64' && 'ubuntu-latest' || 'ubuntu-24.04-arm' }}
strategy:
fail-fast: false
matrix:
platform: [linux/amd64, linux/arm64]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Prepare Image Tags
id: prep
run: |
ARCH=${{ matrix.platform == 'linux/amd64' && 'amd64' || 'arm64' }}
echo "image=${{ inputs.image_name }}:${GITHUB_REF#refs/tags/}-$ARCH" >> $GITHUB_OUTPUT
echo "safe_platform=${{ matrix.platform == 'linux/amd64' && 'linux-amd64' || 'linux-arm64' }}" >> $GITHUB_OUTPUT
- name: Build and push image
uses: docker/build-push-action@v6
with:
context: .
file: ${{ inputs.dockerfile }}
platforms: ${{ matrix.platform }}
push: true
tags: ${{ steps.prep.outputs.image }}
target: ${{ inputs.target }}
cache-from: type=gha,scope=build-${{ matrix.platform }}
cache-to: type=gha,mode=max,scope=build-${{ matrix.platform }}
- name: Save image name for manifest
run: echo "${{ steps.prep.outputs.image }}" > image.txt
- uses: actions/upload-artifact@v4
with:
name: image-${{ steps.prep.outputs.safe_platform }}
path: image.txt
retention-days: 1
create-manifest:
name: Create multi-arch manifest
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/download-artifact@v4
with:
name: image-linux-amd64
path: /tmp/digests/amd64
- uses: actions/download-artifact@v4
with:
name: image-linux-arm64
path: /tmp/digests/arm64
- name: Login to Docker
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ inputs.image_name }}
tags: |
type=semver,pattern={{version}}
type=semver,pattern={{major}}.{{minor}}
type=semver,pattern={{major}}
type=raw,value=latest,enable=${{ inputs.add_latest }}
- name: Create and push manifest list
working-directory: /tmp/digests
run: |
DOCKER_IMAGES="$(cat amd64/image.txt) $(cat arm64/image.txt)"
TAG_ARGS=$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON")
docker buildx imagetools create $DOCKER_IMAGES $TAG_ARGS
+47
View File
@@ -0,0 +1,47 @@
name: PR Checker
on:
pull_request:
branches:
- main
- dev
jobs:
validate-code:
name: Code Integrity Check
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "lts/*"
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Check code formatting
run: pnpm run format:check
continue-on-error: true
- name: Run Linter
run: pnpm run lint
continue-on-error: true
- name: Type Check
run: pnpm run typecheck
continue-on-error: true
- name: Run Tests
run: pnpm run test
continue-on-error: true
- name: Test Application Build
run: pnpm run build
+31
View File
@@ -0,0 +1,31 @@
name: Publish Docker image for release candidate
on:
push:
tags:
- "*.*.*-rc*"
- "!*-*-rc*"
permissions:
contents: read
packages: write
jobs:
docker_publish:
uses: ./.github/workflows/docker.yml
with:
add_latest: true
secrets:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME_2 }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD_2 }}
discord_notification:
needs: docker_publish
uses: ./.github/workflows/discord.yml
with:
discord_title: "New Release Candidate: ${{ github.ref_name }}"
discord_color: 2044800
discord_footer: "Portabase"
secrets:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+36 -60
View File
@@ -1,69 +1,45 @@
name: Publish Docker image for release
on:
release:
types: [published]
push:
tags:
- "*.*.*"
- "!*-*"
permissions:
contents: read
packages: write
jobs:
push_to_registry:
name: Push Docker image to Docker Hub
docker_publish:
uses: ./.github/workflows/docker.yml
with:
add_latest: true
secrets:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME_2 }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD_2 }}
publish_release:
needs: docker_publish
runs-on: ubuntu-latest
permissions:
packages: write
contents: read
attestations: write
id-token: write
steps:
- name: Check out the repo
uses: actions/checkout@v4
- name: Checkout code
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: Publish GitHub Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
gh release edit v${{ github.ref_name }} --draft=false
- name: Extract metadata (tags, labels) for Docker
id: meta
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
with:
images: solucetechnologies/portabase
- name: Set tags
id: set-tags
run: |
echo "RELEASE_TAG=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
TAGS="solucetechnologies/portabase:${GITHUB_REF#refs/tags/}"
if [ "${{ github.event.release.target_commitish }}" = "main" ]; then
TAGS="$TAGS,solucetechnologies/portabase:latest"
fi
echo "tags=$TAGS" >> $GITHUB_OUTPUT
- name: Build and push Docker image
id: push
uses: docker/build-push-action@v6
with:
context: .
file: ./docker/dockerfile/Dockerfile
push: true
tags: ${{ steps.set-tags.outputs.tags }}
target: prod
# Do not delete
# - 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
discord_notification:
needs: docker_publish
uses: ./.github/workflows/discord.yml
with:
discord_title: "New Release: v${{ github.ref_name }}"
discord_color: 3066993
discord_footer: "Portabase"
secrets:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+8 -4
View File
@@ -5,7 +5,7 @@ on:
branches: [ main ]
jobs:
sca-deps: # Dependency & container scan
sca-deps:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
@@ -17,12 +17,16 @@ jobs:
ignore-unfixed: true
secrets-gitleaks: # Secrets exposure
secrets-gitleaks:
if: github.event.pull_request.head.repo.fork == false
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0 # Fetch full history for gitleaks
fetch-depth: 0
- 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
config-path: .gitleaks.toml
+7 -1
View File
@@ -1,5 +1,6 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
.idea
.vscode
# dependencies
/node_modules
/.pnp
@@ -39,5 +40,10 @@ public/uploads/*
!public/uploads/
private/uploads/*
private/keys/*
/.env
seeds/pocket-id/*.db
certificates
+1 -3
View File
@@ -2,9 +2,7 @@ title = "Custom gitleaks config"
[allowlist]
# Global allowlist patterns (wont be flagged)
regexes = [
"s3SecretAccessKey: env.S3_SECRET_KEY ?? null",
]
regexes = []
paths = [
"src/utils/init.ts"
]
-8
View File
@@ -1,8 +0,0 @@
# Default ignored files
/shelf/
/workspace.xml
# Editor-based HTTP Client requests
/httpRequests/
# Datasource local storage ignored files
/dataSources/
/dataSources.local.xml
-8
View File
@@ -1,8 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="ProjectModuleManager">
<modules>
<module fileurl="file://$PROJECT_DIR$/.idea/portabase.iml" filepath="$PROJECT_DIR$/.idea/portabase.iml" />
</modules>
</component>
</project>
-12
View File
@@ -1,12 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<module type="JAVA_MODULE" version="4">
<component name="NewModuleRootManager" inherit-compiler-output="true">
<exclude-output />
<content url="file://$MODULE_DIR$">
<sourceFolder url="file://$MODULE_DIR$/backend" isTestSource="false" />
<excludeFolder url="file://$MODULE_DIR$/venv" />
</content>
<orderEntry type="inheritedJdk" />
<orderEntry type="sourceFolder" forTests="false" />
</component>
</module>
Generated
-6
View File
@@ -1,6 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="VcsDirectoryMappings">
<mapping directory="" vcs="Git" />
</component>
</project>
+57
View File
@@ -0,0 +1,57 @@
{
"github": {
"release": true,
"draft": true,
"tokenRef": "GITHUB_TOKEN"
},
"git": {
"commit": true,
"commitMessage": "chore: release v${version}",
"requireCleanWorkingDir": true,
"tag": true,
"tagName": "v${version}",
"push": true
},
"plugins": {
"@release-it/conventional-changelog": {
"preset": {
"name": "conventionalcommits",
"types": [
{
"type": "feat",
"section": "✨ Features"
},
{
"type": "fix",
"section": "🐛 Bug Fixes"
},
{
"type": "perf",
"section": "⚡️ Performance Improvements"
},
{
"type": "revert",
"section": "⏪️ Reverts"
},
{
"type": "docs",
"section": "📝 Documentation",
"hidden": true
},
{
"type": "chore",
"section": "🔧 Chores",
"hidden": true
}
]
}
},
"@release-it/bumper": {
"out": {
"file": "CITATION.cff",
"path": "version",
"type": "text/yaml"
}
}
}
}
-3
View File
@@ -1,3 +0,0 @@
{
"WillLuke.nextjs.hasPrompted": true
}
Binary file not shown.
-2
View File
@@ -1,2 +0,0 @@
nodeLinker: node-modules
+35
View File
@@ -0,0 +1,35 @@
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.4.27
date-released: '2026-03-02'
+1 -1
View File
@@ -186,7 +186,7 @@
same "printed page" as the copyright notice for easier
identification within third-party archives.
Copyright 2024 Soluce Technologies
Copyright 2024 Portabase
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
+45 -1
View File
@@ -1,4 +1,48 @@
CLUSTER_SCRIPT=docker/entrypoints/app-dev-entrypoint.sh
up:
@bash $(CLUSTER_SCRIPT)
@bash $(CLUSTER_SCRIPT)
seed-keycloak:
@[ $$(ls -1 seeds/keycloak/*.json 2>/dev/null | wc -l) -gt 0 ] || (echo "No realm export found in seeds/keycloak. Add an export from Keycloak first."; exit 1)
@docker compose -f docker-compose.func.yml stop keycloak >/dev/null 2>&1 || true
@docker compose -f docker-compose.func.yml rm -f -s keycloak >/dev/null 2>&1 || true
@docker volume rm portabase-dev-func_keycloak-data >/dev/null 2>&1 || true
@docker compose -f docker-compose.func.yml up -d keycloak
@echo "Keycloak seed import triggered from seeds/keycloak/*.json"
seed-pocket:
@[ -f seeds/pocket-id/portabase.zip ] || (echo "No export found in seeds/pocket-id. Add an export from Pocket ID first."; exit 1)
@docker compose -f docker-compose.func.yml stop pocket-id >/dev/null 2>&1 || true
@docker compose -f docker-compose.func.yml rm -f -s pocket-id >/dev/null 2>&1 || true
@docker volume rm portabase-dev-func_pocket-id-data >/dev/null 2>&1 || true
@docker compose -f docker-compose.func.yml run --rm -v ./seeds/pocket-id/portabase.zip:/tmp/portabase.zip pocket-id ./pocket-id import --yes --path /tmp/portabase.zip >/dev/null
@docker compose -f docker-compose.func.yml up -d pocket-id
@sleep 2
@docker compose -f docker-compose.func.yml exec pocket-id ./pocket-id one-time-access-token admin
@echo "Pocket ID data restored from seeds/pocket-id/portabase.zip"
seed-auth: seed-keycloak seed-pocket
pocket-token:
@docker compose -f docker-compose.func.yml exec pocket-id ./pocket-id one-time-access-token admin
export-pocket:
@echo "Exporting Pocket ID data to seeds/pocket-id/portabase.zip..."
@mkdir -p ./seeds/pocket-id
@docker compose -f docker-compose.func.yml exec pocket-id ./pocket-id export --path /tmp/portabase-export.zip
@docker compose -f docker-compose.func.yml cp pocket-id:/tmp/portabase-export.zip ./seeds/pocket-id/portabase.zip
@docker compose -f docker-compose.func.yml exec pocket-id rm /tmp/portabase-export.zip
@echo "Pocket ID data exported to seeds/pocket-id/portabase.zip"
export-keycloak:
@echo "Exporting Keycloak configuration and users to seeds/keycloak/*.json..."
@mkdir -p ./seeds/keycloak
@rm -f ./seeds/keycloak/*.json
@docker compose -f docker-compose.func.yml stop keycloak >/dev/null 2>&1
@docker rm -f kc-exporter >/dev/null 2>&1 || true
@docker compose -f docker-compose.func.yml run --name kc-exporter keycloak export --dir /tmp/kc-export --users realm_file
@docker cp kc-exporter:/tmp/kc-export/. ./seeds/keycloak/
@docker rm -f kc-exporter >/dev/null 2>&1
@docker compose -f docker-compose.func.yml start keycloak >/dev/null 2>&1
@echo "Keycloak configuration and users exported to seeds/keycloak/*.json"
+63 -195
View File
@@ -1,222 +1,90 @@
<br />
<div align="center">
<a href="https://portabase.io">
<img src="/public/images/logo.png" alt="Logo" width="80" height="80">
<img src="/.github/assets/logo.png" alt="Logo" width="80" height="80">
</a>
<h3 align="center">Portabase</h3>
<h3 align="center">Portabase</h3>
<p align="center">
Easily backup and restore your database instances with Portabase
<br />
<a href="https://portabase.io"><strong>Explore the Docs »</strong></a>
<br />
<br />
<a href="https://www.youtube.com/watch?v=D9uFrGxLc4s">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>
![portabase-ezgif com-video-to-gif-converter](https://github.com/user-attachments/assets/e3929e58-fefd-44cc-ab26-6f8e1a860c43)
<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-rust">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)
[![Support Portabase](https://img.shields.io/badge/Support-Portabase-orange)](https://www.buymeacoffee.com/portabase)
[![PostgreSQL](https://img.shields.io/badge/PostgreSQL-336791?logo=postgresql&logoColor=white)](https://www.postgresql.org/)
[![MySQL](https://img.shields.io/badge/MySQL-4479A1?logo=mysql&logoColor=white)](https://www.mysql.com/)
[![MariaDB](https://img.shields.io/badge/MariaDB-003545?logo=mariadb&logoColor=white)](https://mariadb.org/)
[![SQLite](https://img.shields.io/badge/-SQLite-blue?logo=sqlite&logoColor=white)](https://sqlite.org/)
[![MongoDB](https://img.shields.io/badge/-MongoDB-13aa52?logo=mongodb&logoColor=white)](https://www.mongodb.com/)
[![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)
[![NextJS][NextJS]][NextJS-url]
[![BetterAuth][BetterAuth]][BetterAuth-url]
[![Drizzle][Drizzle]][Drizzle-url]
[![ShadcnUI][ShadcnUI]][ShadcnUI-url]
[![Docker][Docker]][Docker-url]
<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=nSTzT27GgAg">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>
![portabase-dashboard](https://github.com/user-attachments/assets/8f2c69d6-f1f9-4b80-b51c-01f6f13b9b62)
</div>
## Installation
You have 4 ways to install Portabase:
- Automated CLI (recommended) - [details](https://portabase.io/docs/dashboard/setup#cli)
- Docker Run - [details](https://portabase.io/docs/dashboard/setup#docker)
- Docker Compose setup - [details](https://portabase.io/docs/dashboard/setup#docker-compose)
- Kubernetes with Helm (soon)
- Development setup - [details](https://portabase.io/docs/dashboard/setup#development)
## 📚 Table of Contents
**Ensure Docker is installed on your machine before getting started.**
- [About The Project](#about-the-project)
- [Built With](#built-with)
- [Getting Started](#getting-started)
- [Prerequisites](#prerequisites)
- [Installation](#installation)
- [Usage](#usage)
- [Roadmap](#roadmap)
- [Contributing](#contributing)
- [License](#license)
- [Contact](#contact)
- [Acknowledgments](#acknowledgments)
## Contributors
---
[![Contributors](https://contrib.rocks/image?repo=Portabase/portabase)](https://github.com/Portabase/portabase/graphs/contributors)
## About The Project
[!["Support Portabase"](https://www.buymeacoffee.com/assets/img/custom_images/orange_img.png)](https://www.buymeacoffee.com/portabase)
**Portabase** is a server dashboard tool designed to simplify the backup and restoration of your database instances. It integrates seamlessly with Portabase agents for managing operations securely and efficiently.
GitHub Repository: [Portabase](https://github.com/Soluce-Technologies/portabase)
### 🔧 Built With
- [![NextJS][NextJS]][NextJS-url] (v15 with App Router)
- [![Drizzle][Drizzle]][Drizzle-url]
- [![ShadcnUI][ShadcnUI]][ShadcnUI-url]
- BetterAuth
- React Email
---
## 🚀 Getting Started
### Prerequisites
Ensure Docker is installed on your machine before getting started.
### Installation
To run Portabase locally:
1. Clone the repository:
```bash
git clone https://github.com/Soluce-Technologies/portabase
cd portabase
```
2. Start the development environment:
```bash
docker compose up
```
---
## 🛠️ Usage
Portabase provides a dashboard to manage database instances and backups. It supports:
- PostgreSQL (current)
- MongoDB (coming soon)
- MySQL (coming soon)
You can access the dashboard at `http://localhost:3000` after starting the project.
---
## 🗺️ Roadmap
- [ ] Add changelog
- [ ] Enhance documentation
- [ ] Implement testing procedures
- [ ] Include release file
- [ ] Improve security
- [ ] Enhance UX/UI
- [ ] Improve workspace management system
- [x] Migrate to Drizzle ORM
- [ ] Add multi-language support
- [ ] Extend multi-database support:
- [x] PostgreSQL
- [ ] MongoDB
- [ ] MySQL
Check out [open issues](https://github.com/Soluce-Technologies/portabase/issues) for more.
---
## 🤝 Contributing
Contributions are welcome and appreciated! Here's how to get started:
1. Fork the repository
2. Create a new branch:
```bash
git checkout -b feature/YourFeature
```
3. Commit your changes:
```bash
git commit -m "Add YourFeature"
```
4. Push to the branch:
```bash
git push origin feature/YourFeature
```
5. Open a pull request
Give the project a ⭐ if you like it!
### Top Contributors
[![Contributors](https://contrib.rocks/image?repo=Soluce-Technologies/portabase)](https://github.com/Soluce-Technologies/portabase/graphs/contributors)
---
## Developer Notes
### Environment Variables
```yml
# Environment
NODE_ENV=production
# Database
DATABASE_URL=postgresql://devuser:changeme@db:5432/devdb?schema=public
# Project Info
NEXT_PUBLIC_PROJECT_NAME="Portabase"
NEXT_PUBLIC_PROJECT_DESCRIPTION="Portabase is a powerful database manager"
NEXT_PUBLIC_PROJECT_URL=http://app.portabase.io
PROJECT_SECRET=
# SMTP (Email)
SMTP_HOST=
SMTP_PORT=587
SMTP_USER=
SMTP_PASSWORD=
SMTP_FROM=
# Google OAuth
AUTH_GOOGLE_ID=
AUTH_GOOGLE_SECRET=
# S3/MinIO Configuration
S3_ENDPOINT=http://app.s3.portabase.io
S3_ACCESS_KEY=
S3_SECRET_KEY=
S3_BUCKET_NAME=portabase
S3_PORT=9000
S3_USE_SSL=true
# Storage Backend: 'local' or 's3'
STORAGE_TYPE=local
```
### Semantic Versioning
Use the following format for Docker image versioning:
```bash
major.minor.patch-rc.release
# Example: 1.0.0-rc.1
major.minor.patch-rc.release-tag
# Example: 1.0.0-rc.1-dev
```
---
## 📄 License
## License
Distributed under the Apache License. See `LICENSE.txt` for more details.
---
## 📬 Contact
- Killian Larcher - killian.larcher@soluce-technologies.com
- Charles Gauthereau - charles.gauthereau@soluce-technologies.com
- Project Link: [Portabase GitHub](https://github.com/Soluce-Technologies/portabase)
---
## 🙏 Acknowledgments
Thanks to all contributors and the open-source community!
---
[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
[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/
+29
View File
@@ -0,0 +1,29 @@
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 { env } from "@/env.mjs";
import { CardAuth } from "@/features/layout/card-auth";
import { redirect } from "next/navigation";
export default async function RoutePage(props: { searchParams: Promise<{ callbackUrl: string | undefined }> }) {
if (env.AUTH_EMAIL_PASSWORD_ENABLED !== "true") {
redirect("/login");
}
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 || cookieStore.get("__Secure-better-auth.two_factor")?.value;
if (!token) {
redirect("/login");
}
return (
<TooltipProvider>
<CardAuth className="w-full">
<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>
);
}
+34 -50
View File
@@ -1,54 +1,38 @@
"use client"
import React, {useEffect, useState} from "react";
import {LayoutAdmin} from "@/components/layout";
import Image from "next/image";
import {env} from "@/env.mjs";
import {useTheme} from "next-themes";
import {useSession} from "@/lib/auth/auth-client";
import {useRouter} from "next/navigation";
import React, { Suspense } from "react";
import { redirect } from "next/navigation";
import { currentUser } from "@/lib/auth/current-user";
import { AuthLogoSection } from "@/components/wrappers/auth/auth-logo-section";
import { env } from "@/env.mjs";
import { Heart } from "lucide-react";
import { ErrorLayout } from "@/components/wrappers/common/error-layout";
export default async function Layout({
children,
}: {
children: React.ReactNode;
}) {
const user = await currentUser();
export default function Layout({children}: { children: React.ReactNode }) {
if (user && !user.banned && user.role !== "pending") {
redirect("/dashboard/home");
}
const { resolvedTheme } = useTheme();
const [mounted, setMounted] = useState(false);
const router = useRouter();
const { data: session } = useSession();
if (session && session.user && !session.user.banned && session.user.role !== "pending") {
router.replace("/dashboard/home");
}
useEffect(() => {
setMounted(true);
}, []);
if (!mounted) return null;
const imageTheme = resolvedTheme === "dark" ? "/images/logo-white.png" : "/images/logo-black.png";
return (
<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">
<div className="sm:mx-auto sm:w-full sm:max-w-md flex items-center justify-center space-x-2">
<img
className="p-12 text-black dark:text-white"
src={imageTheme}
// loading="eager"
alt="Logo"
// width={1024}
// height={1024}
/>
<span
className="text-sm text-muted-foreground -ml-12 -mb-12">
v{env.NEXT_PUBLIC_PROJECT_VERSION}
</span>
</div>
<div>{children}</div>
</div>
<footer className="py-4 text-center text-xs justify-items-end text-muted-foreground">
Powered by <span className="font-medium">Soluce Technologies</span>
</footer>
return (
<div className="flex min-h-screen flex-col justify-between py-10 sm:px-6 lg:px-8 ">
<div className="flex flex-col items-center justify-center flex-1">
<div className="mx-auto w-full max-w-md">
<AuthLogoSection />
<div className="mt-4">{children}</div>
</div>
)
}
</div>
<footer className="mt-8 text-center text-xs text-muted-foreground flex flex-col gap-1">
<p className="flex items-center justify-center gap-1">
Made with{" "}
<Heart className="size-3 fill-red-500 text-red-500 animate-pulse" />{" "}
by <span className="font-medium text-foreground">Portabase</span>
</p>
<p>v{env.NEXT_PUBLIC_PROJECT_VERSION}</p>
</footer>
</div>
);
}
+55 -27
View File
@@ -1,32 +1,60 @@
"use client"
import { LoginForm } from "@/components/wrappers/auth/login/login-form/login-form";
import { Metadata } from "next";
import { SUPPORTED_PROVIDERS } from "@/lib/auth/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 { env } from "@/env.mjs";
import { en } from "zod/v4/locales";
import {useEffect, useRef, useState} from "react";
import {useSearchParams} from "next/navigation";
export const metadata: Metadata = {
title: "Login",
};
import {LoginForm} from "@/components/wrappers/auth/login/login-form/login-form";
import {toast} from "sonner";
export default async function SignInPage() {
return (
<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">
{env.AUTH_EMAIL_PASSWORD_ENABLED === "true" && (
<LoginForm isPasskeyEnabled={env.AUTH_PASSKEY_ENABLED === "true"} />
)}
export default function SignInPage(props: {
searchParams: Promise<{ callbackUrl: string | undefined }>
}) {
{env.AUTH_EMAIL_PASSWORD_ENABLED === "true" &&
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>
)}
const [urlParams, setUrlParams] = useState<URLSearchParams>();
{SUPPORTED_PROVIDERS.filter((p) => !p.isManual && p.isActive).length >
0 && <SocialAuthButtons providers={SUPPORTED_PROVIDERS} />}
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
setUrlParams(urlParams);
const error = urlParams.get("error");
console.log(urlParams.get("redirect"));
if (error?.includes("pending")) {
toast.error("Your account is not active.");
urlParams.delete("error");
window.history.replaceState({}, document.title, window.location.pathname + "?" + urlParams.toString());
}
}, []);
return (
<div className="mx-auto grid w-full gap-6">
<LoginForm/>
</div>
)
}
{env.AUTH_SIGNUP_ENABLED !== "true" ||
(env.AUTH_EMAIL_PASSWORD_ENABLED === "true" && (
<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>
);
}
+22 -8
View File
@@ -1,10 +1,24 @@
import {PageParams} from "@/types/next";
import {RegisterForm} from "@/components/wrappers/auth/register/register-form/register-form";
import { PageParams } from "@/types/next";
import { RegisterForm } from "@/components/wrappers/auth/register/register-form/register-form";
import { Metadata } from "next";
import { redirect } from "next/navigation";
import { env } from "@/env.mjs";
export const metadata: Metadata = {
title: "Register",
};
export default async function RoutePage(props: PageParams<{}>) {
return (
<div className="mx-auto grid w-full gap-6">
<RegisterForm/>
</div>
)
}
if (
env.AUTH_SIGNUP_ENABLED !== "true" ||
env.AUTH_EMAIL_PASSWORD_ENABLED !== "true"
) {
redirect("/login");
}
return (
<div className="mx-auto grid w-full gap-6">
<RegisterForm />
</div>
);
}
+64
View File
@@ -0,0 +1,64 @@
import { 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";
import { CardAuth } from "@/features/layout/card-auth";
import { env } from "@/env.mjs";
export default async function RoutePage(props: { searchParams: Promise<{ token: string | undefined }> }) {
if (env.AUTH_EMAIL_PASSWORD_ENABLED !== "true") {
return redirect("/login");
}
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>
<CardAuth className="w-full">
<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>
</CardAuth>
</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,31 +0,0 @@
import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {AdminTabs} from "@/components/wrappers/dashboard/admin/admin-tabs";
import {db} from "@/db";
import {isNull} from "drizzle-orm";
export default async function RoutePage(props: PageParams<{}>) {
const users = await db.query.user.findMany({
where: (fields) => isNull(fields.deletedAt),
with: {
accounts: true
}
});
const settings = await db.query.setting.findFirst({
where: (fields, {eq}) => eq(fields.name, "system"),
});
return (
<Page>
<PageHeader>
<PageTitle>Administration Panel</PageTitle>
</PageHeader>
<PageContent>
<AdminTabs settings={settings!} users={users}/>
</PageContent>
</Page>
);
}
@@ -0,0 +1,40 @@
import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {db} from "@/db";
import {notFound} from "next/navigation";
import {SettingsTabs} from "@/components/wrappers/dashboard/admin/settings/settings-tabs";
import {desc, isNull} from "drizzle-orm";
import * as drizzleDb from "@/db";
import {StorageChannelWith} from "@/db/schema/12_storage-channel";
export default async function RoutePage(props: PageParams<{}>) {
const settings = await db.query.setting.findFirst({
where: (fields, {eq}) => eq(fields.name, "system"),
});
const storageChannels = await db.query.storageChannel.findMany({
with: {
organizations: true
},
where: isNull(drizzleDb.schemas.storageChannel.organizationId),
orderBy: desc(drizzleDb.schemas.storageChannel.createdAt)
}) as StorageChannelWith[]
if (!settings || !storageChannels ) {
notFound()
}
return (
<Page>
<PageHeader className="flex flex-col">
<div className="flex justify-between">
<PageTitle className="mb-3">System settings</PageTitle>
</div>
</PageHeader>
<PageContent className="flex flex-col gap-5">
<SettingsTabs storageChannels={storageChannels} settings={settings} />
</PageContent>
</Page>
);
}
@@ -0,0 +1,45 @@
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";
import {SUPPORTED_PROVIDERS} from "@/lib/auth/config";
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,
},
});
const credentialProvider = SUPPORTED_PROVIDERS.find(p => p.id === 'credential');
const isPasswordAuthEnabled = credentialProvider?.isActive || false;
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} isPasswordAuthEnabled={isPasswordAuthEnabled}/>
</PageContent>
</Page>
);
}
@@ -1,32 +0,0 @@
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;
const agent = await db.query.agent.findFirst({
where: (fields, { eq }) => eq(fields.id, agentId),
});
if (!agent) {
notFound();
}
return (
<Page>
<PageHeader>
<PageTitle>Edit {agent.name}</PageTitle>
</PageHeader>
<PageContent>
<AgentForm defaultValues={agent} agentId={agent.id} />
</PageContent>
</Page>
);
}
@@ -1,93 +1,69 @@
import Link from "next/link";
import {GearIcon} from "@radix-ui/react-icons";
import {PageParams} from "@/types/next";
import {Page, PageActions, PageContent, PageDescription, PageTitle} from "@/features/layout/page";
import {formatDateLastContact} from "@/utils/date-formatting";
import {buttonVariants} from "@/components/ui/button";
import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
import {DatabaseCard} from "@/components/wrappers/dashboard/projects/project-card/project-database-card";
import {AgentCardKey} from "@/components/wrappers/dashboard/agent/agent-card-key/agent-card-key";
import { PageParams } from "@/types/next";
import {
Page,
PageContent,
PageDescription,
PageTitle,
} from "@/features/layout/page";
import { db } from "@/db";
import * as drizzleDb from "@/db";
import {and, eq} from "drizzle-orm";
import {notFound} from "next/navigation";
import {
ButtonDeleteProject
} from "@/components/wrappers/dashboard/projects/button-delete-project/button-delete-project";
import {ButtonDeleteAgent} from "@/components/wrappers/dashboard/agent/button-delete-agent/button-delete-agent";
import {capitalizeFirstLetter} from "@/utils/text";
import {Server} from "lucide-react";
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";
import { AgentDialog } from "@/features/agents/components/agent.dialog";
import { AgentType } from "@/features/agents/agents.schema";
export default async function RoutePage(
props: PageParams<{ agentId: string }>,
) {
const { agentId } = await props.params;
export default async function RoutePage(props: PageParams<{ agentId: string }>) {
const agent = await db.query.agent.findFirst({
where: eq(drizzleDb.schemas.agent.id, agentId),
with: {
databases: true,
},
});
const {agentId} = await props.params
if (!agent) {
notFound();
}
const agent = await db.query.agent.findFirst({
where: eq(drizzleDb.schemas.agent.id, agentId),
with: {
databases: true
}
})
const edgeKey = await generateEdgeKey(getServerUrl(), agent.id);
if (!agent) {
notFound()
}
return (
<Page>
<div className="justify-between gap-2 sm:flex">
<PageTitle className="flex items-center">
{capitalizeFirstLetter(agent.name)}
<Link className={buttonVariants({variant: "outline"})}
href={`/dashboard/agents/${agent.id}/edit`}>
<GearIcon className="w-7 h-7"/>
</Link>
</PageTitle>
<PageActions className="justify-between">
<ButtonDeleteAgent agentId={agentId} text={"Delete Agent"} />
</PageActions>
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(agent.name)}
</div>
<div className="flex items-center gap-2 md:justify-between w-full ">
<div className="flex items-center gap-2">
<AgentDialog
agent={agent as AgentType & { id: string }}
typeTrigger={"edit"}
/>
</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">
<div className="flex items-center gap-2">
<ButtonDeleteAgent agentId={agentId} text={"Delete Agent"} />
</div>
</div>
</PageTitle>
</div>
<Card className="w-full sm:w-auto flex-1">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Databases</CardTitle>
<Server className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{agent.databases.length}</div>
<p className="text-xs text-muted-foreground">Databases linked to this agent</p>
</CardContent>
</Card>
<Card className="w-full sm:w-auto flex-1">
<CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
<CardTitle className="text-sm font-medium">Last contact</CardTitle>
<Server className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{formatDateLastContact(agent.lastContact)}</div>
<p className="text-xs text-muted-foreground">Last contact with agent</p>
</CardContent>
</Card>
</div>
<Card className="w-full sm:w-auto flex-1 mb-4">
<CardHeader className="font-bold text-xl">
Edge Key
</CardHeader>
<CardContent>
<AgentCardKey agent={agent}/>
</CardContent>
</Card>
<CardsWithPagination cardsPerPage={2} data={agent.databases} cardItem={DatabaseCard}/>
</PageContent>
</Page>
)
}
{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,16 +0,0 @@
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";
export default async function RoutePage(props: PageParams<{}>) {
return (
<Page>
<PageHeader>
<PageTitle>Create new agent</PageTitle>
</PageHeader>
<PageContent>
<AgentForm />
</PageContent>
</Page>
);
}
@@ -1,26 +1,28 @@
import { PageParams } from "@/types/next";
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 { Page, PageActions, PageContent, PageHeader, PageTitle } from "@/features/layout/page";
import { notFound } from "next/navigation";
import { db } from "@/db";
import {PageParams} from "@/types/next";
import {AgentCard} from "@/components/wrappers/dashboard/agent/agent-card/agent-card";
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination";
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {notFound} from "next/navigation";
import {db} from "@/db";
import * as drizzleDb from "@/db";
import {desc, eq, not} from "drizzle-orm";
import {Metadata} from "next";
import {AgentDialog} from "@/features/agents/components/agent.dialog";
import {and, eq, not} from "drizzle-orm";
import {Plus} from "lucide-react";
import {cn} from "@/lib/utils";
import {EmptyStatePlaceholder} from "@/components/wrappers/common/empty-state-placeholder";
export const dynamic = "force-dynamic";
export const metadata: Metadata = {
title: "Agents",
};
export default async function RoutePage(props: PageParams<{}>) {
const agents = await db.query.agent.findMany({
where: not(eq(drizzleDb.schemas.agent.isArchived, true))
where: not(eq(drizzleDb.schemas.agent.isArchived, true)),
with: {
databases: true
},
orderBy: (fields) => desc(fields.lastContact),
});
if (!agents) {
notFound();
}
@@ -31,22 +33,17 @@ export default async function RoutePage(props: PageParams<{}>) {
<PageTitle>Agents</PageTitle>
{agents.length > 0 && (
<PageActions>
<Link href={"/dashboard/agents/new"}>
<Button>+ Create Agent</Button>
</Link>
<AgentDialog typeTrigger={"create"} />
</PageActions>
)}
</PageHeader>
<PageContent>
{agents.length > 0 ? (
<CardsWithPagination data={agents} cardItem={AgentCard} cardsPerPage={4} numberOfColumns={1} />
<CardsWithPagination data={agents} cardItem={AgentCard} cardsPerPage={4} numberOfColumns={1}/>
) : (
<EmptyStatePlaceholder
url={"/dashboard/agents/new"}
text={"Create new Agent"}
/>
<AgentDialog typeTrigger="empty"/>
)}
</PageContent>
</Page>
);
}
}
@@ -0,0 +1,47 @@
import {PageParams} from "@/types/next";
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {Metadata} from "next";
import {db} from "@/db";
import {notificationChannel, NotificationChannelWith} from "@/db/schema/09_notification-channel";
import {desc, isNull} from "drizzle-orm";
import {ChannelsSection} from "@/components/wrappers/dashboard/admin/channels/channels-section";
import {ChannelAddEditModal} from "@/components/wrappers/dashboard/admin/channels/channel/channel-add-edit-modal";
import * as drizzleDb from "@/db";
export const metadata: Metadata = {
title: "Notification Channels",
};
export default async function RoutePage(props: PageParams<{}>) {
const notificationChannels = await db.query.notificationChannel.findMany({
with: {
organizations: true
},
where: isNull(drizzleDb.schemas.notificationChannel.organizationId),
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>
<ChannelAddEditModal kind="notification" adminView={false}/>
</PageActions>
</PageHeader>
<PageContent>
<ChannelsSection kind="notification" organizations={organizations}
channels={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,50 @@
import {PageParams} from "@/types/next";
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {Metadata} from "next";
import {ChannelsSection} from "@/components/wrappers/dashboard/admin/channels/channels-section";
import {db} from "@/db";
import {desc, eq, isNull} from "drizzle-orm";
import * as drizzleDb from "@/db";
import {StorageChannelWith} from "@/db/schema/12_storage-channel";
import {ChannelAddEditModal} from "@/components/wrappers/dashboard/admin/channels/channel/channel-add-edit-modal";
export const metadata: Metadata = {
title: "Storage Channels",
};
export default async function RoutePage(props: PageParams<{}>) {
const storageChannels = await db.query.storageChannel.findMany({
with: {
organizations: true
},
where: isNull(drizzleDb.schemas.storageChannel.organizationId),
orderBy: desc(drizzleDb.schemas.storageChannel.createdAt)
}) as StorageChannelWith[]
const organizations = await db.query.organization.findMany({
where: (fields) => isNull(fields.deletedAt),
with: {
members: true,
},
});
const settings = await db.query.setting.findFirst({
where: eq(drizzleDb.schemas.setting.name, "system"),
});
return (
<Page>
<PageHeader>
<PageTitle>Storage channels</PageTitle>
<PageActions>
<ChannelAddEditModal kind={"storage"} adminView={false}/>
</PageActions>
</PageHeader>
<PageContent>
<ChannelsSection defaultStorageChannelId={settings?.defaultStorageChannelId} kind={"storage"} organizations={organizations} channels={storageChannels}/>
</PageContent>
</Page>
);
}
@@ -1,41 +0,0 @@
import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {notFound} from "next/navigation";
import {RestoreForm} from "@/components/wrappers/dashboard/database/restore-form";
import {currentUser} from "@/lib/auth/current-user";
import {db} from "@/db";
import {eq} from "drizzle-orm";
import * as drizzleDb from "@/db";
export default async function RoutePage(
props: PageParams<{
databaseId: string;
}>
) {
const {databaseId} = await props.params;
const user = await currentUser();
if (!user) notFound();
const [dbToRestore] = await db.select().from(drizzleDb.schemas.database).where(eq(drizzleDb.schemas.database.id, databaseId));
if (!dbToRestore) {
notFound();
}
const dbsOfSameType = await db.select().from(drizzleDb.schemas.database).where(eq(drizzleDb.schemas.database.dbms!, dbToRestore.dbms!));
const successfulBackups = await db.select().from(drizzleDb.schemas.backup).where(eq(drizzleDb.schemas.backup.status, "success"));
return (
<Page>
<PageHeader>
<PageTitle>Restore {dbToRestore.name}</PageTitle>
</PageHeader>
<PageContent>
<RestoreForm databaseToRestore={dbToRestore} databases={dbsOfSameType} backups={successfulBackups}/>
</PageContent>
</Page>
);
}
@@ -1,36 +0,0 @@
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>
);
}
@@ -1,19 +1,13 @@
import {PageParams} from "@/types/next";
import {notFound, redirect} 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/database-tabs";
import {DatabaseKpi} from "@/components/wrappers/dashboard/projects/database/database-kpi";
import {EditButton} from "@/components/wrappers/dashboard/database/edit-button/edit-button";
import {CronButton} from "@/components/wrappers/dashboard/database/cron-button/cron-button";
import {Page} from "@/features/layout/page";
import {db} from "@/db";
import {eq, and, inArray} from "drizzle-orm";
import * as drizzleDb from "@/db";
import {getOrganizationProjectDatabases} from "@/lib/services";
import {getOrganization} from "@/lib/auth/auth";
import {RetentionPolicySheet} from "@/components/wrappers/dashboard/database/retention-policy/retention-policy-sheet";
import {capitalizeFirstLetter} from "@/utils/text";
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
import {BackupModalProvider} from "@/components/wrappers/dashboard/database/backup/backup-modal-context";
import {DatabaseContent} from "@/components/wrappers/dashboard/projects/database/database-content";
export default async function RoutePage(props: PageParams<{
projectId: string;
@@ -22,8 +16,9 @@ export default async function RoutePage(props: PageParams<{
const {projectId, databaseId} = await props.params;
const organization = await getOrganization({});
const activeMember = await getActiveMember()
if (!organization) {
if (!organization || !activeMember) {
notFound();
}
@@ -36,7 +31,9 @@ export default async function RoutePage(props: PageParams<{
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
retentionPolicy: true,
alertPolicies: true,
storagePolicies: true
}
});
@@ -48,6 +45,11 @@ export default async function RoutePage(props: PageParams<{
where: eq(drizzleDb.schemas.backup.databaseId, dbItem.id),
with: {
restorations: true,
storages: {
with: {
storageChannel: true
}
}
},
orderBy: (b, {desc}) => [desc(b.createdAt)],
});
@@ -57,7 +59,7 @@ export default async function RoutePage(props: PageParams<{
orderBy: (r, {desc}) => [desc(r.createdAt)],
});
const isAlreadyBackup = backups.some((b) => b.status === "waiting");
const isAlreadyBackup = backups.some((b) => b.status === "waiting" || b.status === "ongoing");
const isAlreadyRestore = restorations.some((r) => r.status === "waiting");
const totalBackups = await db.select({count: drizzleDb.schemas.backup.id})
@@ -81,39 +83,29 @@ export default async function RoutePage(props: PageParams<{
notFound();
}
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=" w-full md:w-fit">
{capitalizeFirstLetter(dbItem.name)}
</div>
<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}/>
</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 settings={settings} database={dbItem} isAlreadyRestore={isAlreadyRestore}
backups={backups}
restorations={restorations}/>
</PageContent>
<BackupModalProvider>
<DatabaseContent
activeMember={activeMember}
settings={settings}
database={dbItem}
isAlreadyRestore={isAlreadyRestore}
restorations={restorations}
backups={backups}
totalBackups={totalBackups}
availableBackups={availableBackups}
successRate={successRate}
organizationId={organization.id}
activeOrganizationChannels={[]}
activeOrganizationStorageChannels={[]}
/>
</BackupModalProvider>
</Page>
);
}
}
@@ -1,65 +0,0 @@
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>
);
}
@@ -1,8 +1,5 @@
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 {Page, PageContent, PageTitle} from "@/features/layout/page";
import {
ButtonDeleteProject
} from "@/components/wrappers/dashboard/projects/button-delete-project/button-delete-project";
@@ -12,9 +9,14 @@ import {notFound, redirect} from "next/navigation";
import {db} from "@/db";
import {eq} from "drizzle-orm";
import {getOrganization} from "@/lib/auth/auth";
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
import * as drizzleDb from "@/db";
import {capitalizeFirstLetter} from "@/utils/text";
import {ProjectDialog} from "@/features/projects/components/project.dialog";
import {DatabaseWith} from "@/db/schema/07_database";
import {ProjectWith} from "@/db/schema/06_project";
import {isUuidv4} from "@/utils/verify-uuid";
export default async function RoutePage(props: PageParams<{
projectId: string
@@ -23,7 +25,13 @@ export default async function RoutePage(props: PageParams<{
projectId
} = await props.params;
if (!isUuidv4(projectId)) {
notFound()
}
const organization = await getOrganization({});
const activeMember = await getActiveMember()
if (!organization) {
notFound();
}
@@ -48,23 +56,45 @@ export default async function RoutePage(props: PageParams<{
redirect("/dashboard/projects");
}
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)],
})
) as DatabaseWith[];
const isMember = activeMember?.role === "member";
return (
<Page>
<div className="justify-between gap-2 sm:flex">
<PageTitle className="flex items-center">
{capitalizeFirstLetter(proj.name)}
<Link className={buttonVariants({variant: "outline"})} href={`/dashboard/projects/${proj.id}/edit`}>
<GearIcon className="w-7 h-7"/>
</Link>
<PageTitle className="flex flex-col md:flex-row items-center justify-between w-full ">
<div className="min-w-full md:min-w-fit ">
{capitalizeFirstLetter(proj.name)}
</div>
{!isMember && (
<div className="flex items-center gap-2 md:justify-between w-full ">
<div className="flex items-center gap-2">
<ProjectDialog
databases={availableDatabases}
organization={org}
project={proj as ProjectWith}
isEdit={true}
/>
</div>
<div className="flex items-center gap-2">
<ButtonDeleteProject projectId={projectId} text={"Delete Project"}/>
</div>
</div>
)}
</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">
{proj.databases.length > 0 ? (
<CardsWithPagination
@@ -72,8 +102,8 @@ export default async function RoutePage(props: PageParams<{
organizationSlug={organization.slug}
// @ts-ignore
cardItem={ProjectDatabaseCard}
cardsPerPage={4}
numberOfColumns={1}
cardsPerPage={6}
numberOfColumns={3}
extendedProps={proj}
/>
) : (
@@ -85,4 +115,4 @@ export default async function RoutePage(props: PageParams<{
</PageContent>
</Page>
);
}
}
@@ -1,49 +0,0 @@
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>
);
}
@@ -1,18 +1,23 @@
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 {getOrganization} from "@/lib/auth/auth";
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
import {EmptyStatePlaceholder} from "@/components/wrappers/common/empty-state-placeholder";
import {Metadata} from "next";
import {ProjectDialog} from "@/features/projects/components/project.dialog";
import {DatabaseWith} from "@/db/schema/07_database";
export default async function RoutePage(props: PageParams<{ }>) {
export const metadata: Metadata = {
title: "Projects",
};
export default async function RoutePage(props: PageParams<{}>) {
const organization = await getOrganization({});
const activeMember = await getActiveMember()
if (!organization) {
notFound();
@@ -28,31 +33,48 @@ export default async function RoutePage(props: PageParams<{ }>) {
databases: true,
},
});
const isMember = activeMember?.role === "member";
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[];
return (
<Page>
<PageHeader>
<PageTitle>Projects</PageTitle>
{projects.length > 0 && (
{(projects.length > 0 && !isMember) && (
<PageActions>
<Link href={`/dashboard/projects/new`}>
<Button>+ Create Project</Button>
</Link>
<ProjectDialog databases={availableDatabases} organization={organization}/>
</PageActions>
)}
</PageHeader>
<PageContent>
{projects.length > 0 ? (
<CardsWithPagination organizationSlug={organization.slug} data={projects} cardItem={ProjectCard}
cardsPerPage={4} numberOfColumns={1}/>
) : (
<EmptyStatePlaceholder
url={"/dashboard/projects/new"}
text={"Create new Project"}
<CardsWithPagination
organizationSlug={organization.slug}
data={projects}
cardItem={ProjectCard}
cardsPerPage={9}
numberOfColumns={3}
/>
) : isMember ? (
<EmptyStatePlaceholder text="No project available"/>
) : (
<ProjectDialog databases={availableDatabases} organization={organization} isEmpty={true}/>
)}
</PageContent>
</Page>
);
}
}
@@ -1,38 +0,0 @@
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>
)
}
@@ -1,53 +1,95 @@
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 { PageParams } from "@/types/next";
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 {
SettingsOrganizationMembersTable
} from "@/components/wrappers/dashboard/settings/settings-organization-members-table";
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 { 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";
import { getOrganizationStorageChannels } from "@/db/services/storage-channel";
import { DeleteOrganizationButton } from "@/components/wrappers/dashboard/organization/delete-organization-button";
import { EditOrganizationDialog } from "@/features/organization/components/edit-organization.dialog";
import { db } from "@/db";
import { isNull } from "drizzle-orm";
import * as drizzleDb from "@/db";
import { eq } from "drizzle-orm";
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()
const organization = await getOrganization({});
const user = await currentUser();
const activeMember = await getActiveMember();
if (!organization) {
notFound();
}
if (!organization || !activeMember || !user) {
notFound();
}
const isMember = activeMember?.role === "member";
const notificationChannels = await getOrganizationChannels(organization.id);
const storageChannels = await getOrganizationStorageChannels(organization.id);
const permissions = computeOrganizationPermissions(activeMember);
if (isMember) {
notFound();
}
const users = await db.query.user.findMany({
where: (fields) => isNull(fields.deletedAt),
});
return (
<Page>
<PageHeader>
<PageTitle className="flex items-center">
Organization settings
{!isMember && organization.slug !== "default" && (
<EditButtonSettings/>
)}
</PageTitle>
<PageActions>
{!isMember && organization.slug !== "default" && (
<DeleteOrganizationButton organizationSlug={organization.slug}/>
)}
</PageActions>
</PageHeader>
{/*<PageDescription>*/}
{/* Manage your organization settings.*/}
{/*</PageDescription>*/}
<PageContent>
<SettingsOrganizationMembersTable organization={organization}/>
</PageContent>
</Page>
)
}
const organizationWithMembers = await db.query.organization.findFirst({
where: eq(drizzleDb.schemas.organization.id, organization.id),
with: {
members: {
with: {
user: true,
},
},
},
});
if (!organizationWithMembers) notFound();
return (
<Page>
<PageHeader>
<PageTitle className="flex flex-col md:flex-row items-center justify-between w-full ">
<div className="min-w-full md:min-w-fit ">Organization settings</div>
<div className="flex items-center gap-2 md:justify-between w-full ">
<div className="flex items-center gap-2">
{permissions.canManageSettings &&
organization.slug !== "default" && (
<EditOrganizationDialog
organization={organizationWithMembers}
users={users}
currentUser={user}
/>
)}
</div>
<div className="flex items-center gap-2">
{permissions.canManageDangerZone &&
organization.slug !== "default" && (
<DeleteOrganizationButton
organizationSlug={organization.slug}
/>
)}
</div>
</div>
</PageTitle>
</PageHeader>
<PageContent>
<OrganizationTabs
activeMember={activeMember}
organization={organization}
notificationChannels={notificationChannels}
storageChannels={storageChannels}
/>
</PageContent>
</Page>
);
}
@@ -8,7 +8,12 @@ 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 {DatabaseBackup, Folder, RefreshCcw} from "lucide-react";
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({});
@@ -45,79 +50,6 @@ export default async function RoutePage(props: PageParams<{}>) {
});
// const tomorrow = new Date();
// tomorrow.setDate(tomorrow.getDate() + 1);
//
// const before = new Date();
// before.setDate(before.getDate() - 1);
//
// const backupsEvolution = [
// {
// id: '22e84aa4-228c-45b3-82ec-846a639cd509',
// createdAt: new Date()
// },
// {
// id: '2c114dc3-1fa6-4ef1-972c-9765c65e9331',
// createdAt: new Date()
// },
// {
// id: '2c114dc3-1fa6-4ef1-972c-9765c65e9331',
// createdAt: new Date()
// },
// {
// id: '2c114dc3-1fa6-4ef1-972c-9765c65e9331',
// createdAt: new Date()
// },
// {
// id: '2c114dc3-1fa6-4ef1-972c-9765c65e9331',
// createdAt: new Date(before)
// },
// {
// id: '6a6106fe-7f45-48eb-a56f-0a1e734126a1',
// createdAt: new Date(before)
// },
// {
// id: 'a529d790-502e-4609-ad37-9b1c00c73477',
// createdAt: new Date()
// },
// {
// id: 'a8c105a8-3e29-423e-b7dd-d3218092cde1',
// createdAt: new Date()
// },
// {
// id: 'd33aaf4f-8525-4490-addb-12e3c8650d6d',
// createdAt: new Date()
// },
// {
// id: 'f1d5a4e2-1c33-41c4-932b-02456c2a6f1d',
// createdAt: new Date(tomorrow)
// },
// {
// id: 'f1d5a4e2-1c33-41c4-932b-02456c2a6f1d',
// createdAt: new Date(tomorrow)
// },
// {
// id: 'e88a588a-2353-4470-9976-8c3eb2ffc88d',
// createdAt: new Date()
// },
// {
// id: 'ee11441e-4b41-4c1b-9d91-929565b4204a',
// createdAt: new Date()
// },
//
// // Entries with tomorrow's date
// {
// id: 'f1d5a4e2-1c33-41c4-932b-02456c2a6f1d',
// createdAt: new Date(tomorrow)
// },
// {
// id: 'c0a8323d-9241-4896-9e64-01e905c24e51',
// createdAt: new Date(tomorrow)
// }
// ];
const backupsRate = await db
.select({
createdAt: drizzleDb.schemas.backup.createdAt,
@@ -130,7 +62,6 @@ export default async function RoutePage(props: PageParams<{}>) {
.orderBy(drizzleDb.schemas.backup.createdAt);
const restorationsCountResult = await db
.select({
count: count(),
@@ -139,62 +70,64 @@ export default async function RoutePage(props: PageParams<{}>) {
.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</PageTitle>
<PageTitle>Statistics Overview</PageTitle>
</PageHeader>
<PageContent className="flex flex-col gap-y-4">
<div className="flex flex-col md:flex-row gap-4">
<Card className="w-full flex-1">
<CardHeader className="flex items-center gap-2">
<Folder className="w-5 h-5 text-muted-foreground" />
<CardTitle>Projects</CardTitle>
<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 className="text-3xl font-bold">{projectsCount}</CardContent>
<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 flex-1">
<CardHeader className="flex items-center gap-2">
<DatabaseBackup className="w-5 h-5 text-muted-foreground" />
<CardTitle>Backups</CardTitle>
<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 className="text-3xl font-bold">{backupsEvolutionCount}</CardContent>
<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 flex-1">
<CardHeader className="flex items-center gap-2">
<RefreshCcw className="w-5 h-5 text-muted-foreground" />
<CardTitle>Restorations</CardTitle>
<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 className="text-3xl font-bold">{restorationsCount}</CardContent>
<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">
<Card className="w-full">
<CardHeader>
<CardTitle>Evolution of the number of backups</CardTitle>
</CardHeader>
<CardContent>
<EvolutionLineChart data={sortedBackupsEvolution}/>
</CardContent>
</Card>
<Card className="w-full">
<CardHeader>
<CardTitle>Success rate of backups</CardTitle>
</CardHeader>
<CardContent>
<PercentageLineChart data={backupsRate}/>
</CardContent>
</Card>
<EvolutionLineChart
data={sortedBackupsEvolution}
/>
<PercentageLineChart
data={backupsRate}/>
</div>
</PageContent>
</Page>
+5
View File
@@ -8,6 +8,11 @@ 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<{}>) {
+22 -22
View File
@@ -1,26 +1,26 @@
import React from "react";
import {redirect} from "next/navigation";
import { ReactNode } from "react";
import { redirect } from "next/navigation";
import {SidebarInset, SidebarProvider} from "@/components/ui/sidebar";
import {AppSidebar} from "@/components/wrappers/dashboard/common/sidebar/app-sidebar";
import {Header} from "@/features/layout/Header";
import {currentUser} from "@/lib/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 { currentUser } from "@/lib/auth/current-user";
import { ThemeMetaUpdater } from "@/features/browser/theme-meta-updater";
export default async function Layout({children}: { children: React.ReactNode }) {
const user = await currentUser();
if (!user) redirect("/login");
export default async function Layout({ children }: { children: ReactNode }) {
const user = await currentUser();
if (!user) redirect("/login");
return (
<>
<SidebarProvider>
<div className="flex flex-col lg:flex-row w-full">
<AppSidebar/>
<SidebarInset>
<Header/>
<main className="h-full">{children}</main>
</SidebarInset>
</div>
</SidebarProvider>
</>
);
return (
<SidebarProvider>
<div className="flex flex-col lg:flex-row w-full">
<ThemeMetaUpdater />
<AppSidebar />
<SidebarInset>
<Header />
<main className="h-full">{children}</main>
</SidebarInset>
</div>
</SidebarProvider>
);
}
-62
View File
@@ -1,62 +0,0 @@
import {PageParams} from "@/types/next";
import {Page, PageContent, PageTitle} from "@/features/layout/page";
import {notFound} from "next/navigation";
import {UserForm} from "@/components/wrappers/dashboard/profile/user-form/user-form";
import {Badge} from "@/components/ui/badge";
import {ButtonDeleteAccount} from "@/components/wrappers/dashboard/profile/button-delete-account/button-delete-account";
import {AvatarWithUpload} from "@/components/wrappers/dashboard/profile/avatar/avatar-with-upload";
import {currentUser} from "@/lib/auth/current-user";
import {getAccounts, getSessions} from "@/lib/auth/auth";
export default async function RoutePage(props: PageParams<{}>) {
const user = await currentUser();
if (!user) {
return notFound();
}
if (user.role !== "user" && user.role !== "admin" && user.role !== "superadmin") {
return notFound();
}
const sessions = await getSessions();
const accounts = await getAccounts();
return (
<Page>
<div className="justify-between gap-2 sm:flex">
<PageTitle className="flex items-center">
<AvatarWithUpload
user={{
...user,
image: user.image ?? null,
role: user.role ?? null,
banned: user.banned ?? null,
banReason: user.banReason ?? null,
banExpires: user.banExpires ?? null,
deletedAt: user.deletedAt ? new Date(user.deletedAt) : null,
}}
/>
{user.name}
<Badge className="ml-3 hidden lg:block">{user.role}</Badge>
</PageTitle>
{/*<PageActions className="mt-2 hidden sm:block">*/}
{/* <ButtonDeleteAccount text="Delete my account"/>*/}
{/*</PageActions>*/}
</div>
<PageContent >
<UserForm
userId={user.id}
sessions={sessions} accounts={accounts}
defaultValues={{
name: user.name,
email: user.email,
role: user.role ?? undefined,
}}
/>
{/*<div className="mt-4 sm:hidden ">*/}
{/* <ButtonDeleteAccount text="Delete my account"/>*/}
{/*</div>*/}
</PageContent>
</Page>
);
}
+50
View File
@@ -1,2 +1,52 @@
import { NextResponse } from "next/server";
import { eq } from "drizzle-orm";
import { db } from "@/db";
import * as drizzleDb from "@/db";
export function withAgentCheck(handler: Function) {
return async (request: Request, context: { params: Promise<{ agentId: string }> }) => {
try {
const agentId = (await context.params).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 }
);
}
return handler(request, { ...context, agent });
} catch (err) {
console.error("Error in agent middleware:", err);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
};
}
export async function getDatabaseOrThrow(generatedId: string) {
const database = await db.query.database.findFirst({
where: eq(drizzleDb.schemas.database.agentDatabaseId, generatedId),
with: {
project: true,
alertPolicies: true,
storagePolicies: true
}
});
if (!database) {
throw NextResponse.json(
{ error: "Database associated with generatedId not found" },
{ status: 404 }
);
}
return database;
}
+114 -136
View File
@@ -1,82 +1,63 @@
import {NextResponse} from "next/server";
import {isUuidv4} from "@/utils/verify-uuid";
import {uploadLocalPrivate, uploadS3Private} from "@/features/upload/private/upload.action";
import {v4 as uuidv4} from "uuid";
import {eventEmitter} from "../../../events/route";
import {db} from "@/db";
import {Backup} from "@/db/schema/07_database";
import {and, eq} from "drizzle-orm";
import * as drizzleDb from "@/db";
import {env} from "@/env.mjs";
import {db as dbClient, db} from "@/db";
import {getDatabaseOrThrow, withAgentCheck} from "./helpers";
import {Backup} from "@/db/schema/07_database";
import {withUpdatedAt} from "@/db/utils";
import {eventEmitter} from "@/features/shared/event";
import {sendNotificationsBackupRestore} from "@/features/notifications/helpers";
import {EventKind} from "@/features/notifications/types";
export async function POST(
request: Request,
{params}: { params: Promise<{ agentId: string }> }
) {
export type BodyPost = {
method: "manual" | "automatic"
generatedId: string
}
export type BodyPatch = {
backupId: string
status: "success" | "failed"
size: number
generatedId: string
}
export const POST = withAgentCheck(async (request: Request, {params, agent}: {
params: Promise<{ agentId: string }>,
agent: any
}) => {
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}
);
}
eventEmitter.emit('modification', {update: true});
const agentId = (await params).agentId;
const formData = await request.formData();
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}
);
}
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 db.query.database.findFirst({
where: eq(drizzleDb.schemas.database.agentDatabaseId, generatedId),
with: {
project: true
}
});
if (!database) {
return NextResponse.json(
{error: "Database associated with generatedId not found"},
{status: 404}
);
}
const body: BodyPost = await request.json();
const method = body.method
const database = await getDatabaseOrThrow(body.generatedId);
let backup: Backup | null | undefined = null;
if (method === "automatic") {
[backup] = await db
.insert(drizzleDb.schemas.backup)
.values({
status: 'ongoing',
databaseId: database.id,
})
.returning();
const ongoingBackup = await db.query.backup.findFirst({
where: and(
eq(drizzleDb.schemas.backup.status, 'ongoing'),
eq(drizzleDb.schemas.backup.databaseId, database.id),
),
});
if (!backup) {
if (!ongoingBackup) {
[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}
);
}
} else {
return NextResponse.json(
{error: "Unable to create an automatic backup"},
{error: "A backup is already ongoing"},
{status: 500}
);
}
@@ -88,7 +69,6 @@ export async function POST(
),
});
if (!backup) {
return NextResponse.json(
{error: "Unable to find the corresponding backup"},
@@ -97,80 +77,78 @@ export async function POST(
}
}
const status = formData.get("status") as string | null;
if (status === "success") {
const file = formData.get("file") as File | null;
eventEmitter.emit('modification', {update: true});
if (!file) {
return NextResponse.json(
{error: "File is required for successful backup"},
{status: 400}
);
}
const uuid = uuidv4();
const fileName = `${uuid}.dump`;
const buffer = Buffer.from(await file.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.");
}
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}
);
}
await db
.update(drizzleDb.schemas.backup)
.set(withUpdatedAt({
file: fileName,
status: 'success',
}))
.where(eq(drizzleDb.schemas.backup.id, backup.id));
eventEmitter.emit('modification', {update: true});
return NextResponse.json(
{
message: "Backup successfully uploaded",
},
{status: 200}
);
} else {
await db
.update(drizzleDb.schemas.backup)
.set(withUpdatedAt({status: 'failed'}))
.where(eq(drizzleDb.schemas.backup.id, backup.id));
eventEmitter.emit('modification', {update: true});
return NextResponse.json(
{
message: "Backup successfully updated with status failed",
},
{status: 200}
);
}
return NextResponse.json(
{
message: "Init backup success",
backup: backup,
},
{status: 200}
);
} catch (error) {
console.error("Error in POST handler:", error);
console.error("Error in POST for INIT backup:", error);
return NextResponse.json(
{error: "Internal server error"},
{status: 500}
);
}
}
});
export const PATCH = withAgentCheck(async (request: Request, {params, agent}: {
params: Promise<{ agentId: string }>,
agent: any
}) => {
try {
const body: BodyPatch = await request.json();
console.log("body", body);
const status = body.status
const backupId = body.backupId
const backupSize = body.size
const database = await getDatabaseOrThrow(body.generatedId);
const backup = await db.query.backup.findFirst({
where: eq(drizzleDb.schemas.backup.id, backupId),
});
if (!backup) {
return NextResponse.json(
{error: "No backup found"},
{status: 500}
);
}
const [backupUpdated] = await dbClient
.update(drizzleDb.schemas.backup)
.set(withUpdatedAt({
status: status,
fileSize: backupSize
}))
.where(eq(drizzleDb.schemas.backup.id, backup.id))
.returning();
eventEmitter.emit('modification', {update: true});
await sendNotificationsBackupRestore(database, status == "failed" ? "error_backup" : "success_backup" as EventKind);
return NextResponse.json(
{
message: "Backup successfully updated",
backup: backupUpdated,
},
{status: 200}
);
} catch (error) {
console.error("Error in PATCH backup:", error);
return NextResponse.json(
{error: "Internal server error"},
{status: 500}
);
}
});
@@ -0,0 +1,76 @@
import {NextResponse} from "next/server";
import {and, eq} from "drizzle-orm";
import * as drizzleDb from "@/db";
import {db} from "@/db";
import {getDatabaseOrThrow, withAgentCheck} from "../../helpers";
import {isUuidv4} from "@/utils/verify-uuid";
import {eventEmitter} from "@/features/shared/event";
export type Body = {
generatedId: string
storageChannelId: string
backupId: string
}
export const POST = withAgentCheck(async (request: Request, {params, agent}: {
params: Promise<{ agentId: string }>,
agent: any
}) => {
try {
const body: Body = await request.json();
console.log("body", body);
const generatedId = body.generatedId;
const storageChannelId = body.storageChannelId;
const backupId = body.backupId;
if (!generatedId || !isUuidv4(generatedId)) {
return NextResponse.json(
{error: "generatedId is not a valid UUID"},
{status: 400}
);
}
const database = await getDatabaseOrThrow(generatedId);
const backup = await db.query.backup.findFirst({
where: and(
eq(drizzleDb.schemas.backup.id, backupId),
eq(drizzleDb.schemas.backup.databaseId, database.id),
),
});
if (!backup) {
return NextResponse.json(
{error: "Unable to find the corresponding backup"},
{status: 404}
);
}
const [backupStorage] = await db
.insert(drizzleDb.schemas.backupStorage)
.values({
backupId: backup.id,
storageChannelId: storageChannelId,
status: "pending",
})
.returning();
eventEmitter.emit('modification', {update: true});
return NextResponse.json(
{
message: "Backup storage successfully created",
backupStorage: backupStorage
},
{status: 200}
);
} catch (error) {
console.error("Error in POST for INIT backup:", error);
return NextResponse.json(
{error: "Internal server error"},
{status: 500}
);
}
});
@@ -0,0 +1,95 @@
import {NextResponse} from "next/server";
import {and, eq} from "drizzle-orm";
import * as drizzleDb from "@/db";
import {db as dbClient, db} from "@/db";
import {withUpdatedAt} from "@/db/utils";
import {getDatabaseOrThrow, withAgentCheck} from "../../helpers";
import {eventEmitter} from "@/features/shared/event";
export type Body = {
generatedId: string
status: "success" | "failed"
backupStorageId: string
path: string
size: number
backupId: string
}
export const PATCH = withAgentCheck(async (request: Request, {params, agent}: {
params: Promise<{ agentId: string }>,
agent: any
}) => {
try {
const body: Body = await request.json();
const generatedId = body.generatedId;
const status = body.status;
const filePath = body.path;
const fileSize = body.size;
const backupStorageId = body.backupStorageId;
const backupId = body.backupId;
console.log("body", body);
const database = await getDatabaseOrThrow(generatedId);
const backup = await db.query.backup.findFirst({
where: and(
eq(drizzleDb.schemas.backup.id, backupId),
eq(drizzleDb.schemas.backup.databaseId, database.id),
),
with: {
storages: true
}
});
if (!backup) {
return NextResponse.json(
{error: "Unable to find the corresponding backup"},
{status: 404}
);
}
const [backupStorage] = await dbClient
.update(drizzleDb.schemas.backupStorage)
.set(withUpdatedAt({
status: status,
path: filePath,
size: fileSize
}))
.where(eq(drizzleDb.schemas.backupStorage.id, backupStorageId))
.returning();
if (backup.storages.length > 0) {
const hasSuccessfulStorage = backup.storages.some(
(storage) => storage.status === "success"
);
if (hasSuccessfulStorage && backup.status !== "success") {
await db
.update(drizzleDb.schemas.backup)
.set({
status: "success",
fileSize: fileSize,
})
.where(eq(drizzleDb.schemas.backup.id, backup.id));
}
}
eventEmitter.emit('modification', {update: true});
return NextResponse.json({
message: "Backup status successfully updated",
backupStorage: backupStorage
},
{status: 200}
);
} catch (error) {
console.error("Error in POST for INIT backup:", error);
return NextResponse.json(
{error: "Internal server error"},
{status: 500}
);
}
});
+10 -13
View File
@@ -1,10 +1,10 @@
import {NextResponse} from "next/server";
import {isUuidv4} from "@/utils/verify-uuid";
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";
import {eventEmitter} from "@/features/shared/event";
export type BodyResultRestore = {
generatedId: string
@@ -13,20 +13,16 @@ export type BodyResultRestore = {
type RestorationStatus = 'waiting' | 'ongoing' | 'failed' | 'success';
export async function POST(
request: Request,
{params}: { params: Promise<{ agentId: string }> }
) {
try {
eventEmitter.emit('modification', {update: true});
const agentId = (await params).agentId
const body: BodyResultRestore = await request.json();
console.log(body)
if (!isUuidv4(body.generatedId)) {
return NextResponse.json(
@@ -43,8 +39,10 @@ export async function POST(
}
const database = await db.query.database.findFirst({
where: eq(drizzleDb.schemas.database.agentDatabaseId, body.generatedId)
where: eq(drizzleDb.schemas.database.agentDatabaseId, body.generatedId),
with: {
alertPolicies: true
}
})
if (!database) {
@@ -59,22 +57,21 @@ export async function POST(
return NextResponse.json({error: "Unable to fin the corresponding restoration"}, {status: 404})
}
await db
.update(drizzleDb.schemas.restoration)
.set({ status: body.status as RestorationStatus })
.set({status: body.status as RestorationStatus})
.where(eq(drizzleDb.schemas.restoration.id, restoration.id));
await sendNotificationsBackupRestore(database, body.status == "failed" ? "error_restore" : "success_restore");
const response = {
message: true,
details: "Restoration successfully updated"
}
eventEmitter.emit('modification', {update: true});
return Response.json(response, {status: 200})
} catch (error) {
console.error('Error in POST handler:', error);
return NextResponse.json(
+140 -59
View File
@@ -1,24 +1,25 @@
import {NextResponse} from "next/server";
import {Body} from "./route";
import {isUuidv4} from "@/utils/verify-uuid";
import {getFileUrlPresignedLocal, getFileUrlPreSignedS3Action} from "@/features/upload/private/upload.action";
import {Agent} from "@/db/schema/08_agent";
import {Database} from "@/db/schema/07_database";
import {Database, DatabaseWith} from "@/db/schema/07_database";
import * as drizzleDb from "@/db";
import {db as dbClient} from "@/db";
import {and, eq} from "drizzle-orm";
import {db, db as dbClient} from "@/db";
import {and, eq, inArray} from "drizzle-orm";
import {dbmsEnumSchema, 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";
import type {StorageInput} from "@/features/storages/types";
import {dispatchStorage} from "@/features/storages/dispatch";
import {Setting} from "@/db/schema/01_setting";
export async function handleDatabases(body: Body, agent: Agent, lastContact: Date) {
export async function handleDatabases(body: Body, agent: Agent, lastContact: Date, settings: Setting) {
const databasesResponse = [];
const formatDatabase = (database: Database, backupAction: boolean, restoreAction: boolean, UrlBackup: string) => ({
const formatDatabase = (database: DatabaseWith, backupAction: boolean, restoreAction: boolean, UrlBackup: string | null, storages: PingDatabaseStorageChannels[], urlMeta: string | null) => ({
generatedId: database.agentDatabaseId,
dbms: database.dbms,
storages: storages,
encrypt: settings.encryption,
data: {
backup: {
action: backupAction,
@@ -27,6 +28,7 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
restore: {
action: restoreAction,
file: UrlBackup,
metaFile: urlMeta
},
},
});
@@ -34,12 +36,16 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
for (const db of body.databases) {
const existingDatabase = await dbClient.query.database.findFirst({
where: eq(drizzleDb.schemas.database.agentDatabaseId, db.generatedId)
where: eq(drizzleDb.schemas.database.agentDatabaseId, db.generatedId),
with: {
project: true
}
});
let backupAction: boolean = false
let restoreAction: boolean = false
let urlBackup: string = ""
let urlBackup: string | null = null;
let urlMeta: string | null = null
if (!existingDatabase) {
if (!isUuidv4(db.generatedId)) {
@@ -48,6 +54,12 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
{status: 500}
);
}
if (!dbmsEnumSchema.safeParse(db.dbms).success) {
console.log(`Database type not available: ${db.dbms}`);
continue;
}
const [databaseCreated] = await dbClient
.insert(drizzleDb.schemas.database)
.values({
@@ -59,90 +71,95 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
})
.returning();
if (databaseCreated) {
databasesResponse.push(formatDatabase(databaseCreated, backupAction, restoreAction, urlBackup));
const storages = await getDatabaseStorageChannels(databaseCreated.id)
databasesResponse.push(formatDatabase(databaseCreated, backupAction, restoreAction, urlBackup, storages, null));
}
} else {
const [databaseUpdated] = await dbClient
.update(drizzleDb.schemas.database)
.set({lastContact: lastContact})
.set(withUpdatedAt({
name: db.name,
agentId: agent.id,
lastContact: lastContact
}))
.where(eq(drizzleDb.schemas.database.id, existingDatabase.id))
.returning();
const backup = await dbClient.query.backup.findFirst({
where: and(eq(drizzleDb.schemas.backup.databaseId, databaseUpdated.id), eq(drizzleDb.schemas.backup.status, "waiting"))
const activeBackup = await dbClient.query.backup.findFirst({
where: and(
eq(drizzleDb.schemas.backup.databaseId, databaseUpdated.id),
inArray(drizzleDb.schemas.backup.status, ["waiting", "ongoing"])
)
})
const restoration = await dbClient.query.restoration.findFirst({
where: and(eq(drizzleDb.schemas.restoration.databaseId, databaseUpdated.id), eq(drizzleDb.schemas.restoration.status, "waiting"))
where: and(eq(drizzleDb.schemas.restoration.databaseId, databaseUpdated.id), eq(drizzleDb.schemas.restoration.status, "waiting")),
with: {
backupStorage: true
}
})
if (backup) {
if (activeBackup && activeBackup.status == "waiting") {
backupAction = true
await dbClient
.update(drizzleDb.schemas.backup)
.set(withUpdatedAt({status: "ongoing"}))
.where(eq(drizzleDb.schemas.backup.id, backup.id));
.where(eq(drizzleDb.schemas.backup.id, activeBackup.id));
}
if (restoration) {
restoreAction = true
const backupToRestore = await dbClient.query.backup.findFirst({
where: eq(drizzleDb.schemas.backup.id, restoration.backupId),
with: {
database: {
with: {
project: true
}
}
}
})
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}
);
if (!restoration.backupStorage || restoration.backupStorage.status != "success" || !restoration.backupStorage.path) {
restoreAction = false
continue;
}
const input: StorageInput = {
action: "get",
data: {
path: restoration.backupStorage.path,
signedUrl: true,
},
metadata: {
storageId: restoration.backupStorage.storageChannelId,
fileKind: "backups"
}
};
const fileName = backupToRestore?.file
const inputMeta: StorageInput = {
action: "get",
data: {
path: `${restoration.backupStorage.path}.meta`,
signedUrl: true,
},
metadata: {
storageId: restoration.backupStorage.storageChannelId,
fileKind: "backups"
}
};
let data: SafeActionResult<string, ZodString, readonly [], {
_errors?: string[] | undefined;
}, readonly [], ServerActionResult<string>, object> | undefined
try {
const result = await dispatchStorage(input, undefined, restoration.backupStorage.storageChannelId);
const resultMeta = await dispatchStorage(inputMeta, undefined, restoration.backupStorage.storageChannelId);
if (settings.storage == "local") {
data = await getFileUrlPresignedLocal(fileName!)
} else if (settings.storage == "s3") {
data = await getFileUrlPreSignedS3Action(`backups/${backupToRestore?.database.project?.slug}/${fileName}`);
}
if (data?.data?.success) {
urlBackup = data.data.value ?? "";
if (result.success) {
urlBackup = result.url ?? null;
urlMeta = resultMeta.url ?? null
} 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";
const errorMessage = "Failed to get backup URL";
console.error("Restoration failed: ", errorMessage);
continue;
}
} catch (err) {
@@ -151,17 +168,81 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
.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));
const storages = await getDatabaseStorageChannels(databaseUpdated.id)
databasesResponse.push(formatDatabase(databaseUpdated, backupAction, restoreAction, urlBackup, storages, urlMeta));
}
}
return databasesResponse;
}
type PingDatabaseStorageChannels = {
id: string;
config: any
provider: string
}
async function getDatabaseStorageChannels(databaseId: string): Promise<PingDatabaseStorageChannels[]> {
const database = await db.query.database.findFirst({
where: eq(drizzleDb.schemas.database.id, databaseId),
with: {
project: true,
retentionPolicy: true,
alertPolicies: true,
storagePolicies: true
}
});
if (!database) {
return []
}
const settings = await db.query.setting.findFirst({
where: eq(drizzleDb.schemas.setting.name, "system"),
with: {storageChannel: true},
});
const defaultStorageChannel: PingDatabaseStorageChannels[] = settings?.storageChannel
? [{
id: settings.storageChannel.id,
provider: settings.storageChannel.provider,
config: settings.storageChannel.config,
}]
: [];
const enabledDatabaseStorageChannels = await Promise.all(
(database.storagePolicies ?? [])
.filter(p => p.enabled)
.map(async policy => {
const storageChannel = await db.query.storageChannel.findFirst({
where: eq(drizzleDb.schemas.storageChannel.id, policy.storageChannelId),
});
if (!storageChannel) return null;
return {
id: storageChannel.id,
config: storageChannel.config,
provider: storageChannel.provider,
} as PingDatabaseStorageChannels;
})
);
const filteredChannels: PingDatabaseStorageChannels[] = enabledDatabaseStorageChannels.filter(
(c): c is PingDatabaseStorageChannels => c !== null
);
return filteredChannels.length > 0 ? filteredChannels : defaultStorageChannel;
}
+18 -16
View File
@@ -1,12 +1,13 @@
import {NextResponse} from "next/server";
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";
import {eventEmitter} from "@/features/shared/event";
import {notFound} from "next/navigation";
export type databaseAgent = {
name: string,
@@ -15,16 +16,10 @@ export type databaseAgent = {
}
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")
return Response.json({
message: url
})
}
export async function POST(
request: Request,
@@ -32,14 +27,14 @@ export async function POST(
) {
try {
const agentId = (await params).agentId
console.log(agentId)
const body: Body = await request.json();
const lastContact = new Date();
let message: string
if (!isUuidv4(agentId)) {
message = "agentId is not a valid uuid"
console.log(message)
console.error(message)
return NextResponse.json(
{error: "agentId is not a valid uuid"},
{status: 500}
@@ -52,18 +47,25 @@ export async function POST(
if (!agent) {
message = "Agent not found"
console.log(message)
return NextResponse.json({error: message}, {status: 404})
}
const databasesResponse = await handleDatabases(body, agent, lastContact)
const [settings] = await db.select().from(drizzleDb.schemas.setting).where(eq(drizzleDb.schemas.setting.name, "system")).limit(1);
if (!settings) {
return NextResponse.json({error: "An error occured"}, {status: 404})
}
const databasesResponse = await handleDatabases(body, agent, lastContact, settings)
await db
.update(drizzleDb.schemas.agent)
.set({lastContact: lastContact})
.set(withUpdatedAt({
version: body.version,
lastContact: lastContact
}))
.where(eq(drizzleDb.schemas.agent.id, agentId));
eventEmitter.emit('modification', {update: true});
const response = {
agent: {
@@ -72,7 +74,7 @@ export async function POST(
},
databases: databasesResponse
}
console.log(response)
return Response.json(response)
} catch (error) {
+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,
});
}
+12 -17
View File
@@ -1,15 +1,22 @@
import { EventEmitter } from 'events';
export const eventEmitter = new EventEmitter();
import {auth} from "@/lib/auth/auth";
import {headers} from "next/headers";
import {NextResponse} from "next/server";
import {eventEmitter} from "@/features/shared/event";
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({
start(controller) {
console.log('Stream started');
const handleModification = (data: any) => {
console.log('Modification event triggered:', data);
controller.enqueue(`event: modification\n`);
@@ -18,7 +25,6 @@ export async function GET(request: Request) {
eventEmitter.on('modification', handleModification);
// Handle client disconnect
request.signal.addEventListener('abort', () => {
console.log('Client disconnected');
controller.close();
@@ -35,15 +41,4 @@ 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 });
}
@@ -1,29 +1,46 @@
import * as fs from "node:fs";
import {NextResponse} from "next/server";
import path from "path";
import type {StorageInput} from "@/features/storages/types";
import {dispatchStorage} from "@/features/storages/dispatch";
import {Readable} from "node:stream";
export async function GET(
request: Request,
{params}: { params: Promise<{ fileName: string }> }
) {
const {searchParams} = new URL(request.url);
const token = searchParams.get('token');
const expires = searchParams.get('expires');
const fileName = (await params).fileName
const pathFromUrl = searchParams.get('path');
const storageId = searchParams.get('storageId');
const privateLocalDir = "private/uploads/files/";
const filePath = path.join(privateLocalDir, fileName);
const crypto = require('crypto');
if (!fs.existsSync(filePath)) {
return NextResponse.json(
{error: 'File not found'},
{status: 404}
);
if (!pathFromUrl || !storageId) {
return NextResponse.json({error: "Missing search params"}, {status: 404})
}
const input: StorageInput = {
action: "get",
data: {
path: pathFromUrl,
signedUrl: true,
},
metadata: {
storageId: storageId,
fileKind: "backups",
}
};
console.debug(input);
const result = await dispatchStorage(input, undefined, storageId);
if (!result.success) {
return NextResponse.json({error: "Enable to get file from provided storage channel, an error occurred !"})
}
const fileName = path.basename(pathFromUrl);
const crypto = require('crypto');
const expectedToken = crypto.createHash('sha256').update(`${fileName}${expires}`).digest('hex');
if (token !== expectedToken) {
return NextResponse.json(
@@ -31,8 +48,8 @@ export async function GET(
{status: 403}
);
}
//@ts-ignore
const expiresAt = parseInt(expires, 10);
const expiresAt = parseInt(expires!, 10);
if (Date.now() > expiresAt) {
return NextResponse.json(
{error: 'Signed token expired'},
@@ -40,7 +57,15 @@ export async function GET(
);
}
const fileStream = fs.createReadStream(filePath);
if (!result.file || !(result.file instanceof Readable)) {
return NextResponse.json(
{error: "Invalid file payload"},
{status: 500}
);
}
const fileStream = Readable.from(result.file as Readable);
const stream = new ReadableStream({
start(controller) {
fileStream.on('data', (chunk) => controller.enqueue(chunk));
+88
View File
@@ -0,0 +1,88 @@
import {NextResponse} from "next/server";
import {auth} from "@/lib/auth/auth";
import {headers} from "next/headers";
import {db} from "@/db";
import * as drizzleDb from "@/db";
import {eq} from "drizzle-orm";
import {StorageInput} from "@/features/storages/types";
import {dispatchStorage} from "@/features/storages/dispatch";
import {Readable} from "node:stream";
export async function GET(
req: Request,
{params}: { params: Promise<{ fileName: string }> }
) {
const {searchParams} = new URL(req.url);
const fileName = (await params).fileName;
const storageId = searchParams.get('storageId');
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});
if (!storageId) {
return NextResponse.json({error: "Missing storageId in search params"}, {status: 404})
}
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 path = `images/${fileName}`;
const input: StorageInput = {
action: "get",
data: {
path: path,
},
metadata: {
storageId: storageId,
fileKind: "images"
}
}
const result = await dispatchStorage(input, undefined, storageId);
if (!result.file || !(result.file instanceof Readable)) {
console.error(`An error occurred while getting file :`, result);
return NextResponse.json(
{error: "Invalid file payload"},
{status: 500}
);
}
const fileStream = Readable.from(result.file as Readable);
const stream = new ReadableStream({
start(controller) {
fileStream.on('data', (chunk) => controller.enqueue(chunk));
fileStream.on('end', () => controller.close());
fileStream.on('error', (err) => controller.error(err));
},
});
return new NextResponse(stream, {
headers: {
'Content-Disposition': `inline; filename="${fileName}"`,
"Cache-Control": "no-store",
"Content-Type": contentType,
},
});
} catch (err) {
console.error("Error streaming image:", err);
return NextResponse.json({error: "Error fetching file"}, {status: 500});
}
}
+23
View File
@@ -0,0 +1,23 @@
export async function GET(request: Request) {
const url = new URL(request.url);
const code = url.searchParams.get("code");
const success = Boolean(code);
return new Response(
`
<!DOCTYPE html>
<html>
<body>
<h1>${success ? "Success" : "Failed"}</h1>
</body>
</html>
`,
{
status: success ? 200 : 400,
headers: {
"Content-Type": "text/html; charset=utf-8",
},
}
);
}
-38
View File
@@ -1,38 +0,0 @@
import path from "path";
import fs from "fs/promises";
import { NextResponse } from "next/server";
export async function GET(
request: Request,
{params}: { params: Promise<{ fileName: string }> }
) {
try {
const fileName = (await params).fileName;
console.log("fileName", fileName);
const filePath = path.join(process.cwd(), "private/uploads/images", fileName);
// Check if the file exists
try {
await fs.access(filePath); // Ensures the file exists
} catch {
return NextResponse.json({ error: "File not found" }, { status: 404 });
}
// Read the file
const fileContent = await fs.readFile(filePath); // Returns a Buffer
return new NextResponse(fileContent, {
headers: {
"Content-Disposition": `attachment; filename="${fileName}"`,
"Content-Type": "application/octet-stream", // Adjust MIME type as needed
},
});
} catch (error) {
console.error("Error reading file:", error);
return NextResponse.json({ error: "Internal Server Error" }, { status: 500 });
}
}
-23
View File
@@ -1,23 +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 });
}
}
+80
View File
@@ -0,0 +1,80 @@
import {NextResponse} from "next/server";
import fs from "fs";
import path from "path";
import {env} from "@/env.mjs";
export async function POST(request: Request) {
try {
const body = await request.json();
const event = body.Event
const headers = event.HTTPRequest.Header
const uploadLength = headers["X-File-Size"]?.[0];
const uploadOffset = headers["Upload-Offset"]?.[0];
const status = headers["X-Status"]?.[0];
console.log(`Upload ID : ${event.Upload.ID} (${uploadOffset}/${uploadLength})`);
if (status === "success") {
if (
body.Type === "post-receive" &&
event.Upload.SizeIsDeferred === false &&
event.Upload.Offset === event.Upload.Size
) {
const id = event.Upload.ID;
const fileName = headers["X-File-Name"]?.[0];
const filePath = headers["X-File-Path"]?.[0];
if (!filePath) {
return NextResponse.json({error: "Missing X-File-Path"}, {status: 500});
}
// const uploadDir = path.join(process.cwd(), "/private/uploads/");
const uploadDir = path.join(env.PRIVATE_PATH, "/uploads/");
const oldFilePath = path.join(uploadDir, "tmp", id);
const newFilePath = path.join(uploadDir, filePath);
fs.mkdirSync(path.dirname(newFilePath), {recursive: true});
let retries = 10;
while (!fs.existsSync(oldFilePath)) {
if (retries-- === 0) {
return NextResponse.json({error: `Upload file not found: ${oldFilePath}`}, {status: 500});
}
await new Promise(r => setTimeout(r, 200));
}
fs.renameSync(oldFilePath, newFilePath);
const infoFilePath = `${oldFilePath}.info`;
if (fs.existsSync(infoFilePath)) {
fs.unlinkSync(infoFilePath);
}
const metadataHeaderB64 = headers["Upload-Metadata"]?.[0];
if (metadataHeaderB64) {
const metadataHeader = Buffer.from(metadataHeaderB64, "base64").toString("utf-8");
if (metadataHeader) {
const tomlContent = metadataHeader
.split(",")
.map((pair) => {
const [key, value] = pair.split(" ");
const escapedValue = value.replace(/"/g, '\\"');
return `${key} = "${escapedValue}"`;
})
.join("\n");
const metaFilePath = `${newFilePath}.meta`;
fs.writeFileSync(metaFilePath, tomlContent, "utf-8");
}
}
}
}
return NextResponse.json({});
} catch (error) {
console.error("Hook error:", error);
return NextResponse.json({error: "Internal server error"}, {status: 500});
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 11 KiB

After

Width:  |  Height:  |  Size: 6.4 KiB

+30 -82
View File
@@ -1,85 +1,33 @@
// "use client"
//
// import {useSearchParams} from "next/navigation"
//
// enum Error {
// Configuration = "Configuration",
// }
//
// const errorMap = {
// [Error.Configuration]: (
// <p>
// There was a problem when trying to authenticate. Please contact us if this
// error persists. Unique error code:{" "}
// <code className="rounded-sm bg-slate-100 p-1 text-xs">Configuration</code>
// </p>
// ),
// }
//
// export default function AuthErrorPage() {
// const search = useSearchParams()
// const error = search.get("error") as Error
//
// return (
// <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-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
// </h5>
// <div className="font-normal text-gray-700 dark:text-gray-400">
// {errorMap[error] || "Please contact us if this error persists."}
// </div>
// </a>
// </div>
// )
// }
"use client"
"use client";
import { useSearchParams } from "next/navigation"
import React, { Suspense } from "react"
import { Loader2 } from "lucide-react";
import { useRouter, useSearchParams } from "next/navigation";
import { useEffect } from "react";
import { useSession } from "@/lib/auth/auth-client";
enum Error {
Configuration = "Configuration",
export default function ErrorPage() {
const router = useRouter();
const searchParams = useSearchParams();
const { data: session, isPending } = useSession();
useEffect(() => {
if (isPending) {
return;
}
const error = searchParams.get("error");
const params = new URLSearchParams();
if (error) {
params.set("error", error);
}
const destination = session ? "/dashboard/home" : "/login";
router.replace(`${destination}?${params.toString()}`);
}, [isPending, session, router, searchParams]);
return (
<div className="flex h-screen w-full items-center justify-center">
<Loader2 className="h-8 w-8 animate-spin" />
</div>
);
}
const errorMap = {
[Error.Configuration]: (
<p>
There was a problem when trying to authenticate. Please contact us if this
error persists. Unique error code:{" "}
<code className="rounded-sm bg-slate-100 p-1 text-xs">Configuration</code>
</p>
),
}
function AuthErrorPageContent() {
const search = useSearchParams()
const error = search.get("error") as Error
return (
<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-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
</h5>
<div className="font-normal text-gray-700 dark:text-gray-400">
{errorMap[error] || "Please contact us if this error persists."}
</div>
</a>
</div>
)
}
export default function AuthErrorPage() {
return (
<Suspense fallback={<div>Loading...</div>}>
<AuthErrorPageContent />
</Suspense>
)
}
BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

After

Width:  |  Height:  |  Size: 15 KiB

+26 -11
View File
@@ -6,8 +6,11 @@
@theme inline {
--color-background: var(--background);
--color-foreground: var(--foreground);
--font-sans: var(--font-geist-sans);
--font-sans: var(--font-poppins), ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
--font-title: var(--font-poppins), var(--font-poppins), sans-serif;
--font-mono: var(--font-geist-mono);
--font-author: var(--font-author);
--font-poppins: var(--font-poppins);
--color-sidebar-ring: var(--sidebar-ring);
--color-sidebar-border: var(--sidebar-border);
--color-sidebar-accent-foreground: var(--sidebar-accent-foreground);
@@ -71,7 +74,7 @@
--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: oklch(0.611 0.204 42.149);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.97 0 0);
--secondary-foreground: oklch(0.205 0 0);
@@ -82,7 +85,7 @@
--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);
--ring: oklch(0.611 0.204 42.149);
--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);
@@ -90,12 +93,12 @@
--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: oklch(0.611 0.204 42.149);
--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);
--sidebar-ring: oklch(0.611 0.204 42.149);
}
.dark {
@@ -105,8 +108,8 @@
--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);
--primary: oklch(0.704 0.191 47.132);
--primary-foreground: oklch(0.985 0 0);
--secondary: oklch(0.269 0 0);
--secondary-foreground: oklch(0.985 0 0);
--muted: oklch(0.269 0 0);
@@ -116,7 +119,7 @@
--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);
--ring: oklch(0.704 0.191 47.132);
--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);
@@ -124,12 +127,12 @@
--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: oklch(0.704 0.191 47.132);
--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);
--sidebar-ring: oklch(0.704 0.191 47.132);
}
@layer base {
@@ -137,6 +140,18 @@
@apply border-border outline-ring/50;
}
body {
@apply bg-background text-foreground;
@apply bg-background text-foreground font-sans;
}
h1, h2, h3, h4, h5, h6 {
@apply font-title;
}
.scrollbar-hide {
-ms-overflow-style: none;
scrollbar-width: none;
}
.scrollbar-hide::-webkit-scrollbar {
display: none;
}
}
+1 -3
View File
File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 90 KiB

After

Width:  |  Height:  |  Size: 20 KiB

BIN
View File
Binary file not shown.

Before

Width:  |  Height:  |  Size: 7.7 KiB

After

Width:  |  Height:  |  Size: 3.9 KiB

+33 -23
View File
@@ -1,32 +1,42 @@
import React from "react";
import type {Metadata} from "next";
import {Inter} from "next/font/google";
import type { Metadata } from "next";
import type React from "react";
import "./globals.css";
import {Providers} from "./providers";
import {cn} from "@/lib/utils";
import {ConsoleSilencer} from "@/components/wrappers/common/console-silencer";
import { ConsoleSilencer } from "@/components/wrappers/common/console-silencer";
import { author, geistMono, poppins } from "@/fonts/fonts";
import { cn } from "@/lib/utils";
import { Providers } from "./providers";
const inter = Inter({subsets: ["latin"]});
const title = process.env.PROJECT_NAME ?? "App Title";
export const metadata: Metadata = {
title: process.env.NEXT_PUBLIC_PROJECT_NAME ?? "App Title",
description: process.env.NEXT_PUBLIC_PROJECT_DESCRIPTION ?? undefined,
title: {
default: title,
template: `%s - ${title}`,
},
description: process.env.PROJECT_DESCRIPTION ?? undefined,
};
export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
export default async function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en" suppressHydrationWarning>
<head>
<meta name="apple-mobile-web-app-title" content="Portabase"/>
</head>
<body className={cn(inter.className, "h-full")}>
<ConsoleSilencer/>
return (
<html lang="en" suppressHydrationWarning>
<head>
<meta name="apple-mobile-web-app-title" content={title} />
</head>
<body
className={cn(
poppins.variable,
author.variable,
geistMono.variable,
"font-sans h-full",
)}
>
<ConsoleSilencer />
<Providers>{children}</Providers>
</body>
</html>
);
</body>
</html>
);
}
+15 -9
View File
@@ -1,13 +1,19 @@
import BackButton from "@/components/wrappers/common/button/back-button";
export default async function NotFound() {
return(
<div className="flex items-center min-h-screen px-4 py-12 sm:px-6 md:px-8 lg:px-12 xl:px-16">
<div className="w-full space-y-6 text-center">
<div className="space-y-3">
<h1 className="scroll-m-20 text-4xl font-extrabold tracking-tight lg:text-5xl">Not found</h1>
<p className="leading-7 [&:not(:first-child)]:mt-6">The content you are trying to view is not available.</p>
</div>
</div>
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:mt-6">
The content you are trying to view is not available.
</p>
</div>
)
<BackButton>Go home</BackButton>
</div>
</div>
);
}
+18 -13
View File
@@ -1,23 +1,28 @@
"use client";
import { PropsWithChildren, Suspense } from "react";
import { ThemeProvider } from "@/features/theme/theme-provider";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { type PropsWithChildren, Suspense } from "react";
import { Toaster } from "@/components/ui/sonner";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ErrorLayout } from "@/components/wrappers/common/error-layout";
import { ThemeMetaUpdaterRoot } from "@/features/browser/theme-meta-updater-root";
import { ThemeProvider } from "@/features/theme/theme-provider";
export type ProviderProps = PropsWithChildren<{}>;
const queryClient = new QueryClient();
export const Providers = (props: ProviderProps) => {
return (
<Suspense fallback={null}>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<QueryClientProvider client={queryClient}>
<Toaster />
{props.children}
</QueryClientProvider>
</ThemeProvider>
</Suspense>
);
return (
<Suspense fallback={null}>
<ThemeProvider attribute="class" defaultTheme="system" enableSystem>
<ThemeMetaUpdaterRoot />
<QueryClientProvider client={queryClient}>
<ErrorLayout>
<Toaster />
{props.children}
</ErrorLayout>
</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"
}
}
}
+35
View File
@@ -0,0 +1,35 @@
name: portabase-dev-func
services:
keycloak:
image: quay.io/keycloak/keycloak:latest
command: start-dev --import-realm
# environment:
# KC_BOOTSTRAP_ADMIN_USERNAME: admin
# KC_BOOTSTRAP_ADMIN_PASSWORD: admin
ports:
- "3056:8080"
volumes:
- keycloak-data:/opt/keycloak/data
- ./seeds/keycloak:/opt/keycloak/data/import:ro
- ./export:/tmp/export
pocket-id:
image: ghcr.io/pocket-id/pocket-id
restart: unless-stopped
environment:
- APP_URL=http://localhost:3055
- ENCRYPTION_KEY=QwHyjbZvSsDUAcjpdmSPsuYxaH6vET6OeBaeLwXccCb43L6Om3W1AoU5pKIJTzYr
ports:
- 3055:1411
volumes:
- pocket-id-data:/app/data
- ./seeds/pocket-id:/seed:ro
healthcheck:
test: "curl -f http://localhost:1411/healthz"
interval: 1m30s
timeout: 5s
retries: 2
start_period: 10s
volumes:
keycloak-data:
pocket-id-data:
+10 -25
View File
@@ -1,24 +1,23 @@
name: portabase-prod
services:
app:
build:
context: .
dockerfile: docker/dockerfile/Dockerfile
target: prod
# build:
# context: .
# dockerfile: docker/dockerfile/Dockerfile
# target: prod
image: portabase/portabase:1.4.0
ports:
- '8887:80'
environment:
TZ: "Europe/Paris"
env_file:
- .env
volumes:
- portabase-data:/data
depends_on:
db:
condition: service_healthy
container_name: portabase-app-prod
db:
image: postgres:16-alpine
ports:
@@ -35,20 +34,6 @@ 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:
portabase-data:
postgres-data:
minio_data:
+30 -67
View File
@@ -1,73 +1,36 @@
name: portabase-dev
services:
# app:
# build:
# context: .
# dockerfile: docker/dockerfile/Dockerfile
# target: dev
# ports:
# - "8887:8887"
# environment:
# - TIME_ZONE="Europe/Paris"
# - NODE_ENV=development
# depends_on:
# db:
# condition: service_healthy
# volumes:
# - .:/app
# - /app/node_modules
# container_name: portabase-app
#
db:
image: postgres:17-alpine
ports:
- "5433:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=devdb
- POSTGRES_USER=devuser
- POSTGRES_PASSWORD=changeme
healthcheck:
test: ["CMD-SHELL", "pg_isready -U devuser -d devdb"]
interval: 10s
timeout: 5s
retries: 5
db:
image: postgres:16-alpine
ports:
- "5433:5432"
volumes:
- postgres-data:/var/lib/postgresql/data
environment:
- POSTGRES_DB=devdb
- POSTGRES_USER=devuser
- POSTGRES_PASSWORD=changeme
healthcheck:
test: ["CMD-SHELL", "pg_isready -U devuser -d devdb"]
interval: 10s
timeout: 5s
retries: 5
# db-pgadmin:
# image: dpage/pgadmin4
# environment:
# PGADMIN_DEFAULT_EMAIL: "devuser@devuser.devuser"
# PGADMIN_DEFAULT_PASSWORD: "changeme"
# PGADMIN_CONFIG_SERVER_MODE: "False"
# POSTGRES_USER: "devuser"
# POSTGRES_PASSWORD: "changeme"
# volumes:
# - pgadmin-data:/var/lib/pgadmin
# ports:
# - "8080:80"
# restart: unless-stopped
# depends_on:
# - db
s3:
container_name: s3-portabase-dev
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}
- MINIO_BROWSER=on
tusd:
image: tusproject/tusd:v2.8.0
ports:
- "1080:8080"
command: >
-upload-dir /data/uploads/tmp
-hooks-http http://localhost:8887/api/tus/hooks
-max-size 21474836480
-base-path /tus/files/
extra_hosts:
- "localhost:host-gateway"
volumes:
- ./private/uploads/tmp:/data/uploads/tmp
user: "1000:1000"
volumes:
postgres-data:
minio_data:
# pgadmin-data:
postgres-data:
+76 -66
View File
@@ -1,114 +1,124 @@
FROM node:22-alpine AS base
FROM --platform=$BUILDPLATFORM node:22-bullseye AS base
ENV YARN_VERSION=4.9.1
RUN apk add --update --no-cache \
build-base \
libc6-compat \
openssl \
make \
RUN apt-get update && apt-get install -y \
curl \
ca-certificates \
bash \
tzdata \
libc6 \
build-essential \
g++ \
jpeg-dev \
cairo-dev \
giflib-dev \
pango-dev \
libtool \
make \
autoconf \
automake \
libpng
RUN corepack enable && corepack prepare yarn@${YARN_VERSION}
RUN yarn set version ${YARN_VERSION}
libtool \
git \
nginx \
&& rm -rf /var/lib/apt/lists/*
FROM base AS deps
RUN mkdir -p /etc/apt/keyrings \
&& curl -fsSL https://www.postgresql.org/media/keys/ACCC4CF8.asc \
| gpg --dearmor -o /etc/apt/keyrings/postgresql.gpg \
&& echo "deb [signed-by=/etc/apt/keyrings/postgresql.gpg] http://apt.postgresql.org/pub/repos/apt bullseye-pgdg main" \
> /etc/apt/sources.list.d/pgdg.list
RUN apt-get update && apt-get install -y \
postgresql-18 \
postgresql-client-18 \
postgresql-contrib-18 \
&& rm -rf /var/lib/apt/lists/*
ENV PATH="/usr/lib/postgresql/18/bin:${PATH}"
FROM --platform=$BUILDPLATFORM golang:1.23-bookworm AS tusd-base
ARG TUSD_VERSION=2.8.0
RUN apt-get update && apt-get install -y --no-install-recommends git ca-certificates \
&& rm -rf /var/lib/apt/lists/*
WORKDIR /build
RUN git clone https://github.com/tus/tusd.git . \
&& git checkout v${TUSD_VERSION}
RUN CGO_ENABLED=0 GOOS=linux GOARCH=${TARGETARCH} \
go build -ldflags="-s -w" -o /tusddist/tusd ./cmd/tusd
FROM scratch AS tusd-dist
COPY --from=tusd-base /tusddist/tusd /tusd
FROM base AS build-env
RUN corepack enable && corepack prepare pnpm@latest --activate
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 pnpm i --frozen-lockfile
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
FROM base AS dev
WORKDIR /app
FROM build-env AS dev
COPY --from=deps /app/node_modules ./node_modules
COPY --from=tusd-base /usr/local/bin/tusd /usr/local/bin/tusd
WORKDIR /app
COPY . .
USER root
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
COPY . .
ENV NEXT_TELEMETRY_DISABLED=1
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
RUN pnpm run build
FROM base AS prod
WORKDIR /app
ENV NODE_ENV=production
ENV NEXT_TELEMETRY_DISABLED=1
ENV PORT=80
ENV HOSTNAME="0.0.0.0"
ENV PGDATA=/data/postgres
ENV PRIVATE_PATH=/data/private
RUN addgroup --system --gid 1001 nodejs
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
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 src/db ./src/db
COPY --from=builder --chown=1001:1001 /app/.next/standalone ./
COPY --from=builder --chown=1001:1001 /app/.next/static ./.next/static
COPY --chown=1001:1001 src/db ./src/db
COPY --from=deps /app/node_modules ./node_modules
COPY --from=tusd-dist /tusd /usr/local/bin/tusd
RUN chmod +x /usr/local/bin/tusd
COPY docker/nginx/nginx.conf /etc/nginx/nginx.conf
RUN mkdir -p .next /data/private/uploads \
&& chown -R nextjs:nodejs .next /data/private /app/public
USER root
COPY ./docker/entrypoints/app-prod-entrypoint.sh /app/app-prod-entrypoint.sh
RUN chmod +x /app/app-prod-entrypoint.sh
EXPOSE 80
ENV PORT=80
ENV HOSTNAME="0.0.0.0"
USER nextjs
ENTRYPOINT ["sh","/app/app-prod-entrypoint.sh"]
+4 -10
View File
@@ -1,18 +1,12 @@
#!/bin/bash
#
#npx drizzle-kit generate
#npx drizzle-kit migrate
#
#npm run dev
#
#exec "$@"
set -euo pipefail
echo "▶ Running Drizzle codegen..."
npx drizzle-kit generate
pnpm drizzle-kit generate
echo "▶ Applying migrations..."
npx drizzle-kit migrate
pnpm drizzle-kit migrate
echo "▶ Starting Next.js dev server..."
exec npm run dev
exec pnpm dev
+79 -10
View File
@@ -1,14 +1,83 @@
#!/bin/bash
#echo " ____ __ __ "
#echo " / __ \____ _____/ /_____ _/ /_ ____ _________ "
#echo " / /_/ / __ \/ ___/ __/ __ / __ \/ __ / ___/ _ \ "
#echo " / ____/ /_/ / / / /_/ /_/ / /_/ / /_/ (__ ) __/ "
#echo " /_/ \____/_/ \__/\__,_/_.___/\__,_/____/\___/ "
#echo " "
#echo " Community Edition v1.1.1 "
#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
POSTGRES_BIN=$(ls -d /usr/lib/postgresql/*/bin | head -n 1)
if [ -z "$POSTGRES_BIN" ]; then
echo "PostgreSQL binaries not found"
exit 1
fi
export PATH="$POSTGRES_BIN:$PATH"
if [ -z "$DATABASE_URL" ]; then
echo "[INFO] No DATABASE_URL provided, starting internal Postgres..."
mkdir -p "$PGDATA"
chown -R postgres:postgres "$PGDATA"
if [ ! -f "$PGDATA/PG_VERSION" ]; then
echo "[INFO] Initializing database cluster..."
if ! su postgres -c "initdb -D '$PGDATA'" > /dev/null 2>&1; then
echo "[ERROR] initdb failed"
exit 1
fi
fi
if ! su postgres -c "pg_ctl -D '$PGDATA' \
-o \"-c listen_addresses='localhost' -c logging_collector=on\" \
-l $PGDATA/postgres.log -w start" > /dev/null 2>&1; then
echo "[ERROR] PostgreSQL failed to start"
exit 1
fi
until su postgres -c "pg_isready -h 127.0.0.1 -p 5432" > /dev/null 2>&1; do
sleep 1
done
echo "[INFO] PostgreSQL server is up and accepting connections"
DB_USER="${POSTGRES_USER:-portabase_user}"
DB_PASS="${POSTGRES_PASSWORD:-JaB6b1SUtIWYvt7srnOt}"
DB_NAME="${POSTGRES_DB:-portabase_db}"
USER_EXISTS=$(su postgres -c "psql -tAc \"SELECT 1 FROM pg_roles WHERE rolname='$DB_USER'\"" 2>/dev/null)
if [ "$USER_EXISTS" != "1" ]; then
if ! su postgres -c "psql -c \"CREATE USER $DB_USER WITH PASSWORD '$DB_PASS';\"" > /dev/null 2>&1; then
echo "[ERROR] Failed creating user"
exit 1
fi
fi
DB_EXISTS=$(su postgres -c "psql -tAc \"SELECT 1 FROM pg_database WHERE datname='$DB_NAME'\"" 2>/dev/null)
if [ "$DB_EXISTS" != "1" ]; then
if ! su postgres -c "psql -c \"CREATE DATABASE $DB_NAME OWNER $DB_USER;\"" > /dev/null 2>&1; then
echo "[ERROR] Failed creating database"
exit 1
fi
fi
export DATABASE_URL="postgres://$DB_USER:$DB_PASS@127.0.0.1:5432/$DB_NAME"
echo "[SUCCESS] Internal PostgreSQL started successfully"
echo "[SUCCESS] Database: $DB_NAME | User: $DB_USER | Host: 127.0.0.1:5432"
fi
mkdir -p /data/private/uploads/tmp
echo "▶ Starting tusd server..."
tusd --base-path /tus/files/ --upload-dir /data/private/uploads/tmp --hooks-http http://127.0.0.1:3000/api/tus/hooks --port 1080 --max-size 21474836480 &
echo "▶ Starting Next.js server..."
PORT=3000 node server.js &
echo "▶ Starting nginx..."
exec nginx -g "daemon off;"
node server.js
exec "$@"
+41
View File
@@ -0,0 +1,41 @@
events {}
http {
client_max_body_size 20G;
ignore_invalid_headers off;
server {
listen 80;
location /tus/ {
proxy_pass http://127.0.0.1:1080/tus/;
proxy_pass_request_headers on;
proxy_request_buffering off;
proxy_buffering off;
proxy_http_version 1.1;
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
location / {
proxy_pass http://127.0.0.1:3000;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
}
-6
View File
@@ -1,6 +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} />;
};
-25
View File
@@ -1,25 +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;
-13
View File
@@ -1,13 +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,
</Button>
</Html>
);
}
-16
View File
@@ -1,16 +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;
-6
View File
@@ -1,6 +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
-86
View File
@@ -1,86 +0,0 @@
import { NextRequest, NextResponse } from "next/server";
import { loggingMiddleware } from "@/middleware/loggingMiddleware";
import { errorHandler } from "@/middleware/errorHandler";
import { auth } from "@/lib/auth/auth";
import { headers } from "next/headers";
import { signOut } from "@/lib/auth/auth-client";
export async function middleware(request: NextRequest) {
const url = request.nextUrl.clone();
const redirectUrl = encodeURIComponent(request.nextUrl.pathname)
if (url.pathname.startsWith("/dashboard")) {
const session = await auth.api.getSession({
headers: await headers(),
});
if (!session) {
return NextResponse.redirect(new URL(`/login?redirect=${redirectUrl}`, request.url));
}
if (session.user.banned) {
signOut();
return NextResponse.redirect(new URL("/login?error=banned", request.url));
}
if (session.user.role === "pending") {
signOut();
return NextResponse.redirect(new URL(`/login?error=pending?redirect=${redirectUrl}`, request.url));
}
if (url.pathname === "/dashboard") {
return NextResponse.redirect(new URL(`/dashboard/home`, request.url));
}
return NextResponse.next();
}
// Exclude `/api/auth` and its subpaths
if (url.pathname.startsWith("/api/auth")) {
return NextResponse.next();
}
if (url.pathname.startsWith("/api")) {
const routeExists = checkRouteExists(url.pathname);
// If the route does not exist, return a 404 JSON response
if (!routeExists) {
return new NextResponse(JSON.stringify({ message: "This API route does not exist.", status: 404 }), {
status: 404,
headers: { "Content-Type": "application/json" },
});
}
}
try {
loggingMiddleware(request);
} catch (err) {
errorHandler(err);
}
}
// Function to check if the route exists (supports dynamic routes)
function checkRouteExists(pathname: string) {
// Define static and dynamic routes with patterns
const routePatterns = [
//do not delete
// /^\/api\/auth\/\d+$/, // Dynamic route with a number as a parameter (e.g., /api/dynamic/123)
// /^\/api\/auth\/\w+$/, // Dynamic route with a number as a parameter (e.g., /api/dynamic/123)
// /^\/api\/agent\/healthcheck\/\w+$/, // Dynamic route with an alphanumeric parameter (e.g., /api/user/username)
/^\/api\/agent\/[^/]+\/status\/?$/, // Dynamic route for /api/agent/[id]/status
/^\/api\/agent\/[^/]+\/backup\/?$/,
/^\/api\/agent\/[^/]+\/restore\/?$/,
/^\/api\/files\/[^/]+\/?$/,
/^\/api\/images\/[^/]+\/?$/,
/^\/api\/events\/?$/,
/^\/api\/init\/?$/,
];
return routePatterns.some((pattern) => pattern.test(pathname));
}
export const config = {
runtime: "nodejs",
matcher: [
// '/api/agent/:path*',
"/api/:path*",
"/dashboard/:path*",
"/dashboard",
],
};
+1 -1
View File
@@ -1,6 +1,6 @@
/// <reference types="next" />
/// <reference types="next/image-types/global" />
/// <reference path="./.next/types/routes.d.ts" />
import "./.next/dev/types/routes.d.ts";
// NOTE: This file should not be edited
// see https://nextjs.org/docs/app/api-reference/config/typescript for more information.
+18 -2
View File
@@ -1,6 +1,8 @@
import type {NextConfig} from "next";
import {PORTABASE_DEFAULT_SETTINGS} from "./portabase.config";
const isDev = process.env.NODE_ENV === "development";
function buildCSPHeader(): string {
const {CSP} = PORTABASE_DEFAULT_SETTINGS.SECURITY;
@@ -41,9 +43,23 @@ const nextConfig: NextConfig = {
typescript: {
ignoreBuildErrors: true,
},
eslint: {
ignoreDuringBuilds: true,
experimental: {
serverActions: {
bodySizeLimit: "10gb",
},
proxyClientMaxBodySize: '10gb',
},
async rewrites() {
if (!isDev) return [];
return [
{
source: "/tus/:path*",
destination: "http://localhost:1080/tus/:path*",
},
];
},
async headers() {
return [
{

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