Compare commits

...
427 Commits
Author SHA1 Message Date
github-actions[bot] 31cbd52703 chore: release 1.9.5 2026-03-29 12:11:43 +00:00
dd85f7a25d fix: trusted url missing, better auth crash due to update (#240)
* fix: release.yml

* fix: S3ChannelConfigSchema type port mismatch

* chore: package.json

* fix: refactoring logs and adding pino logger for production and dev.

* fix: trusted url error due to better auth update.

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
Co-authored-by: killianlarcher <killian.larcher@soluce-technologies.com>
2026-03-29 14:10:58 +02:00
github-actions[bot] 203c25aabe chore: release 1.9.4 2026-03-29 09:43:29 +00:00
41b1c89283 fix: logs (#238)
* fix: release.yml

* fix: S3ChannelConfigSchema type port mismatch

* chore: package.json

* fix: refactoring logs and adding pino logger for production and dev.

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
Co-authored-by: killianlarcher <killian.larcher@soluce-technologies.com>
2026-03-29 11:42:48 +02:00
github-actions[bot] 180ea21041 chore: release 1.9.3 2026-03-29 09:23:24 +00:00
Charles GTEandGitHub e1c0fc6917 fix: refactoring logs and adding pino logger for production and dev. (#236) 2026-03-29 11:22:28 +02:00
github-actions[bot] 057c192314 chore: release 1.9.2 2026-03-29 07:25:20 +00:00
Charles GTEandGitHub aff782aa28 fix: mariadb and mysql separation (#234) 2026-03-29 09:24:31 +02:00
github-actions[bot] 2e729cb79c chore: release 1.9.1 2026-03-28 18:01:28 +00:00
Charles GTEandGitHub d8677d74a7 fix: default notification (#233) 2026-03-28 19:00:50 +01:00
github-actions[bot] 2f77714994 chore: release 1.9.0 2026-03-28 15:37:58 +00:00
Charles GTEandGitHub bc8050c4df feat: add-healthcheck (#231)
* fix: refactoring

* fix: refactoring

* fix: refactoring
2026-03-28 16:37:03 +01:00
Charles GTEandGitHub a9c69b112d Merge pull request #232 from Portabase/revert-230-revert-229-feat/add-healthcheck
Revert "Revert "feat: add-healthcheck""
2026-03-28 16:35:18 +01:00
Charles GTEandGitHub 04785c323b Revert "Revert "feat: add-healthcheck"" 2026-03-28 16:35:02 +01:00
Charles GTEandGitHub 1d209ceb28 Merge pull request #230 from Portabase/revert-229-feat/add-healthcheck
Revert "feat: add-healthcheck"
2026-03-28 16:28:25 +01:00
Charles GTEandGitHub 941a9256f0 Revert "feat: add-healthcheck" 2026-03-28 16:27:57 +01:00
Charles GTEandGitHub 9098012426 feat: add-healthcheck (#229 )
feat: add-healthcheck
2026-03-28 16:23:11 +01:00
Charles GTE 8e12ff59a4 fix: refactoring 2026-03-28 16:22:04 +01:00
Charles GTE 0f8a267861 chore: update security.yml 2026-03-28 16:17:24 +01:00
Charles GTE da53a00fd8 chore: update security.yml 2026-03-28 16:13:36 +01:00
Charles GTE 47e3a93e42 fix: healthcheck system 2026-03-28 16:06:55 +01:00
Charles GTE 0262a1c176 fix: database health cron 2026-03-28 15:10:46 +01:00
Charles GTE 7aecd911c0 feat: database health logs 2026-03-27 23:06:19 +01:00
Charles GTE 595ccadfb3 feat: added heath_error_count and the cron for health check agent errors 2026-03-27 22:13:32 +01:00
charles-gauthereau bded273f94 fix: set default notification channel 2026-03-27 19:22:44 +01:00
charles-gauthereau c095d393cf fix: set default notification channel 2026-03-26 22:45:42 +01:00
charles-gauthereau c3ae82fde7 feat: set default notification channel 2026-03-26 20:17:23 +01:00
charles-gauthereau a8e11ebef7 feat: set default notification channel 2026-03-25 22:19:41 +01:00
charles-gauthereau 61b1a2e724 feat: set default notification channel 2026-03-25 22:07:20 +01:00
charles-gauthereau 7a4af4e1e0 feat: added cron job to remove healthcheck logs older than 12h 2026-03-25 21:11:09 +01:00
charles-gauthereau a9d6481193 fix: health chart labels and get logs for 12h instead of 24h 2026-03-25 20:45:20 +01:00
charles-gauthereau fb97963cc3 feat: front form for default notification settings page choice 2026-03-25 09:06:53 +01:00
charles-gauthereau 0cc0ba989c feat: add agent healthcheck 2026-03-24 20:06:43 +01:00
github-actions[bot] 26eff02943 chore: release 1.8.0 2026-03-18 18:48:30 +00:00
67246efafc feat: adding basic end-to-end testing and ci workflow (#225)
* feat: add end-to-end testing

* chore: fix bug in github action.

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
Co-authored-by: killianlarcher <killian.larcher@soluce-technologies.com>
2026-03-18 19:47:41 +01:00
github-actions[bot] 260b3ba1f9 chore: release 1.7.1 2026-03-17 14:03:35 +00:00
Charles GTEandGitHub 90f8885e73 fix: removed version number on auth layout (#223)
fix: removed version number on auth layout
2026-03-17 15:02:55 +01:00
charles-gauthereau 0bacd6a65c fix: removed version number on auth layout 2026-03-17 15:01:34 +01:00
github-actions[bot] 2846deb4d0 chore: release 1.7.0 2026-03-16 13:39:18 +00:00
ae6dbcab25 feat: add-valkey engine (#222)
* feat: add-valkey engine

* fix: README.md with Valkey content

---------

Co-authored-by: charles-gauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-16 14:38:43 +01:00
charles-gauthereau d823ec2e82 fix: README.md with Valkey content 2026-03-16 14:26:25 +01:00
charles-gauthereau b0ce5273ce feat: add-valkey engine 2026-03-16 12:16:50 +01:00
github-actions[bot] b576862024 chore: release 1.6.1 2026-03-16 07:56:56 +00:00
dc2496ee18 fix: type mismatch for s3 (#221)
* fix: release.yml

* fix: S3ChannelConfigSchema type port mismatch

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-16 08:56:19 +01:00
87db207833 fix: add redis badge and Supported databases section in README.md (#219)
Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-15 13:56:01 +01:00
github-actions[bot] b1ccf6ec5f chore: release 1.6.0 2026-03-15 11:10:32 +00:00
Charles GTEandGitHub ca3f63af81 feat: add-redis #217
feat: add-redis
2026-03-15 12:08:44 +01:00
Charles GTEGitHubCopilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
29b0ca5bf5 fix: 'Semicolon insertion'
Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
2026-03-15 12:05:48 +01:00
charlesgauthereau da50193e8c fix: detabase details page in order to support backup only databases. 2026-03-15 12:02:00 +01:00
charlesgauthereau 1829c1fc1c feat: add redis database support for backup 2026-03-14 14:41:51 +01:00
github-actions[bot] 79a17407bd chore: release 1.5.4 2026-03-08 20:59:04 +00:00
dd1e3283d5 fix: release.yml (#214)
Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-08 21:58:25 +01:00
d1baedd092 fix: pipeline [skip-release] (#213)
* fix: README.md

* fix: release.yml [skip-release]

* fix: release.yml [skip-release]

* fix: release.yml [skip-release]

* fix: release.yml [skip-release]

* fix: release.yml [skip-release]

* fix: release.yml [skip-release]

* fix: release.yml [skip-release]

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-07 14:11:28 +01:00
9e504cefc1 fix: pipeline [skip-release] (#212)
* fix: README.md

* fix: release.yml [skip-release]

* fix: release.yml [skip-release]

* fix: release.yml [skip-release]

* fix: release.yml [skip-release]

* fix: release.yml [skip-release]

* fix: release.yml [skip-release]

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-07 14:09:39 +01:00
3cc5ab70fa fix: release.yml [skip-release] (#211)
* fix: README.md

* fix: release.yml [skip-release]

* fix: release.yml [skip-release]

* fix: release.yml [skip-release]

* fix: release.yml [skip-release]

* fix: release.yml [skip-release]

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-07 14:06:28 +01:00
0441af78a1 fix: readme [skip-release] (#210)
* fix: README.md

* fix: release.yml [skip-release]

* fix: release.yml [skip-release]

* fix: release.yml [skip-release]

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-07 13:59:18 +01:00
3c615a4fd7 fix: workflow [skip-release]
* fix: README.md

* fix: release.yml [skip-release]

* fix: release.yml [skip-release]

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-07 13:52:42 +01:00
8681e16fde fix: updated the README.md with helm and some bug fixes [skip-release]
* fix: README.md

* fix: release.yml [skip-release]

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-07 13:46:47 +01:00
github-actions[bot] 918c6c448c chore: release 1.5.3 2026-03-07 11:57:16 +00:00
Charles GTEandGitHub 1e2b4f16b3 fix: helm-directory
fix: move helm directory to root
2026-03-07 12:56:48 +01:00
charlesgauthereau f0d02eb8b3 fix: Chart.yaml 2026-03-07 12:55:54 +01:00
charlesgauthereau 96274206c5 fix: move helm directory to root 2026-03-07 12:43:18 +01:00
github-actions[bot] 7ae7ea6f79 chore: release 1.5.2 2026-03-07 11:29:44 +00:00
Charles GTEandGitHub 487e7ad83d fix: helm.yml
fix: helm.yml
2026-03-07 12:29:07 +01:00
charlesgauthereau c9dabd39fe fix: helm.yml 2026-03-07 12:28:33 +01:00
github-actions[bot] 30ab689c6e chore: release 1.5.1 2026-03-07 11:06:41 +00:00
Charles GTEandGitHub 8143af18af fix: workflow for helm chart
feat/helm-chart
2026-03-07 12:06:12 +01:00
charlesgauthereau 471c65cc48 fix: release.yml 2026-03-07 12:05:21 +01:00
charlesgauthereau 9162d5afcc fix: release.yml 2026-03-07 12:04:22 +01:00
charlesgauthereau 9236c664c2 Merge branch 'main' into feat/helm-chart 2026-03-07 12:02:56 +01:00
charlesgauthereau 767ed69020 fix: release.yml 2026-03-07 12:02:25 +01:00
github-actions[bot] 93c706719f chore: release 1.6.0 2026-03-07 10:56:16 +00:00
Charles GTEandGitHub ed1ac1926b feat: helm-chart
feat/helm-chart
2026-03-07 11:55:50 +01:00
charlesgauthereau 3adb6d01c0 fix: release.yml 2026-03-07 11:54:43 +01:00
Charles GTEandGitHub 9c195c1f30 feat: helm-chart
feat/helm-chart
2026-03-07 11:52:24 +01:00
charlesgauthereau e27093404b feat: update the release pipeline with helm publish 2026-03-07 11:49:16 +01:00
charlesgauthereau 9602427ca0 feat: add config files for helm chart 2026-03-07 11:11:48 +01:00
charlesgauthereau 46812cb5ff Merge branch 'main' into dev 2026-03-07 10:36:29 +01:00
github-actions[bot] 6258d92464 chore: release 1.5.0 2026-03-06 21:02:09 +00:00
c57d913c46 feat: new pipeline for release process
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix: workflow

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 22:01:40 +01:00
charlesgauthereau 5d285b7ac6 Merge branch 'main' into dev 2026-03-06 22:01:05 +01:00
charlesgauthereau c96219f851 fix: workflow 2026-03-06 22:00:46 +01:00
github-actions[bot] c7d949b1d1 chore: release 1.4.56 2026-03-06 20:47:32 +00:00
6593a8e324 fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 21:47:00 +01:00
charlesgauthereau 0988fa0115 Merge branch 'main' into dev
# Conflicts:
#	.github/workflows/docker.yml
2026-03-06 21:46:22 +01:00
charlesgauthereau 9f0b1a172c fix 2026-03-06 21:45:43 +01:00
github-actions[bot] 0126a829dd chore: release 1.4.55 2026-03-06 20:33:33 +00:00
b04a2750a3 fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 21:33:05 +01:00
charlesgauthereau 9e22160b16 Merge branch 'main' into dev 2026-03-06 21:32:28 +01:00
charlesgauthereau e8e4b093d2 fix 2026-03-06 21:32:10 +01:00
github-actions[bot] 0cb27f2637 chore: release 1.4.54 2026-03-06 18:25:30 +00:00
1497c7385b fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 19:24:55 +01:00
charlesgauthereau 327b06d2a1 Merge branch 'main' into dev 2026-03-06 19:24:18 +01:00
charlesgauthereau 2c2cfc0b73 fix 2026-03-06 19:22:52 +01:00
github-actions[bot] 93327b0a6a chore: release 1.4.53 2026-03-06 18:00:01 +00:00
11c74d4117 fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 18:59:29 +01:00
charlesgauthereau f93bb6e33e fix 2026-03-06 18:59:01 +01:00
charlesgauthereau 73699bbda9 Merge branch 'main' into dev 2026-03-06 18:55:35 +01:00
charlesgauthereau 5f24ec2d86 fix 2026-03-06 18:55:19 +01:00
c08bb8f8e0 fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 18:38:53 +01:00
charlesgauthereau d01e705866 fix 2026-03-06 18:37:12 +01:00
ecc80c3c01 fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 18:36:48 +01:00
charlesgauthereau 784a907384 fix 2026-03-06 18:36:19 +01:00
b68a5f32ca fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 18:35:19 +01:00
charlesgauthereau 467f9e2ffd fix 2026-03-06 18:33:36 +01:00
charlesgauthereau 1d25acde61 Merge branch 'main' into dev
# Conflicts:
#	.github/workflows/release.yml
2026-03-06 18:33:21 +01:00
charlesgauthereau cb6ac86bb0 fix 2026-03-06 18:32:10 +01:00
cd2da4441d fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 18:30:11 +01:00
charlesgauthereau d7515b172c Merge branch 'main' into dev 2026-03-06 18:28:21 +01:00
charlesgauthereau e4de903cc9 fix 2026-03-06 18:27:54 +01:00
github-actions[bot] 45166a3b9c chore: release 1.4.52 2026-03-06 17:15:05 +00:00
687b59e397 fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 18:14:35 +01:00
charlesgauthereau 9a3be8d5a5 fix 2026-03-06 18:13:49 +01:00
0c23c24651 fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 18:12:57 +01:00
charlesgauthereau e20e7f016c fix 2026-03-06 18:12:01 +01:00
charlesgauthereau a0c1a24ba9 Merge branch 'main' into dev
# Conflicts:
#	.github/workflows/release.yml
2026-03-06 18:09:27 +01:00
charlesgauthereau 0475b3d01a fix 2026-03-06 18:08:35 +01:00
39cb9b99f6 fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 18:07:51 +01:00
charlesgauthereau efd123ddfe fix 2026-03-06 18:07:06 +01:00
charlesgauthereau 413318886c Merge branch 'main' into dev
# Conflicts:
#	.github/workflows/release.yml
2026-03-06 18:06:51 +01:00
charlesgauthereau 187f9d2386 fix 2026-03-06 18:06:06 +01:00
github-actions[bot] 5f61c7baa6 chore: release 1.4.51 2026-03-06 17:03:58 +00:00
e3c5ec9a58 fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 18:03:32 +01:00
charlesgauthereau fde52415cb fix 2026-03-06 18:02:51 +01:00
charlesgauthereau d92225d093 Merge branch 'main' into dev 2026-03-06 18:02:31 +01:00
charlesgauthereau e2a0932229 fix 2026-03-06 18:02:19 +01:00
github-actions[bot] 6acdf73557 chore: release 1.4.50 2026-03-06 16:43:28 +00:00
4405b61e03 fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 17:43:01 +01:00
charlesgauthereau fba714d679 fix 2026-03-06 17:42:32 +01:00
charlesgauthereau 8e15282405 Merge branch 'main' into dev
# Conflicts:
#	.github/workflows/release.yml
2026-03-06 17:42:24 +01:00
charlesgauthereau 72b5816451 fix 2026-03-06 17:42:09 +01:00
github-actions[bot] 4016191497 chore: release 1.4.49 2026-03-06 16:37:57 +00:00
94b36c1622 fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 17:37:14 +01:00
charlesgauthereau aa736b8b4a fix 2026-03-06 17:36:42 +01:00
charlesgauthereau 94cd937c56 Merge branch 'main' into dev
# Conflicts:
#	.github/workflows/release.yml
2026-03-06 17:36:14 +01:00
charlesgauthereau 9ccf3b7242 fix 2026-03-06 17:35:56 +01:00
github-actions[bot] 01b7a58c75 chore: release 1.4.48 2026-03-06 16:30:16 +00:00
2b6c661fea fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 17:29:41 +01:00
charlesgauthereau fbd61cdf2f fix 2026-03-06 17:28:49 +01:00
charlesgauthereau e9534181da Merge branch 'main' into dev 2026-03-06 17:28:38 +01:00
fdc613fe8e fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 17:26:45 +01:00
charlesgauthereau fca09a84cb fix 2026-03-06 17:26:17 +01:00
charlesgauthereau 6ec6fb1f11 Merge branch 'main' into dev 2026-03-06 17:25:58 +01:00
charlesgauthereau 3d51d3548f fix 2026-03-06 17:25:33 +01:00
github-actions[bot] 8479b6c822 chore: release 1.4.47 2026-03-06 16:07:42 +00:00
21bb165103 fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 17:07:08 +01:00
charlesgauthereau c7ca01c0fb Merge branch 'main' into dev
# Conflicts:
#	.github/workflows/release.yml
2026-03-06 17:06:47 +01:00
charlesgauthereau 5d2172f2bf fix 2026-03-06 17:06:00 +01:00
1decd9d7a5 fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 17:05:09 +01:00
d0091cfed3 fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 16:59:42 +01:00
charlesgauthereau 0ebcf1729c Merge branch 'main' into dev
# Conflicts:
#	.github/workflows/release.yml
2026-03-06 16:59:04 +01:00
charlesgauthereau 51d101e8f0 fix 2026-03-06 16:58:35 +01:00
charlesgauthereau 2d9bf225a0 fix 2026-03-06 16:57:53 +01:00
github-actions[bot] f85d12448c chore: release 1.4.46 2026-03-06 15:07:34 +00:00
82c8ad98b4 fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 16:07:03 +01:00
charlesgauthereau 2499c7c426 Merge branch 'main' into dev
# Conflicts:
#	.github/workflows/release.yml
2026-03-06 16:06:18 +01:00
charlesgauthereau da530217ab fix 2026-03-06 16:05:44 +01:00
github-actions[bot] 4c70d8aac7 chore: release 1.4.45 2026-03-06 15:01:50 +00:00
ba1dbcc9f3 fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 16:01:15 +01:00
charlesgauthereau 0ad380cc81 fix 2026-03-06 16:00:40 +01:00
charlesgauthereau b77a2e374e Merge branch 'main' into dev
# Conflicts:
#	.github/workflows/release.yml
2026-03-06 16:00:00 +01:00
charlesgauthereau 647108e8ee fix 2026-03-06 15:59:33 +01:00
github-actions[bot] 8e4e41ca07 chore: release 1.4.44 2026-03-06 12:31:40 +00:00
a8eb3f5f1a fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 13:31:03 +01:00
charlesgauthereau eb5ac5c8ea Merge branch 'main' into dev
# Conflicts:
#	.github/workflows/release.yml
2026-03-06 13:30:35 +01:00
charlesgauthereau 29344e0c48 fix 2026-03-06 13:29:27 +01:00
github-actions[bot] 73f7ac057c chore: release 1.4.43 2026-03-06 12:25:50 +00:00
d359d53fb7 fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 13:25:14 +01:00
charlesgauthereau ff83ee9c62 fix 2026-03-06 13:24:18 +01:00
charlesgauthereau da3a8d7fbe Merge branch 'main' into dev 2026-03-06 13:23:18 +01:00
github-actions[bot] ba2fc55188 chore: release v1.4.42 2026-03-06 12:16:19 +00:00
a6896c9b50 dev (#176)
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 13:15:49 +01:00
charlesgauthereau f2563c5e8d Merge branch 'main' into dev
# Conflicts:
#	.github/workflows/release.yml
2026-03-06 13:14:53 +01:00
charlesgauthereau 9326291780 fix 2026-03-06 13:12:37 +01:00
github-actions[bot] 8dd74f310d chore: release v1.4.41 2026-03-06 12:08:08 +00:00
44ed9e9ca6 fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 13:07:39 +01:00
charlesgauthereau ab22680625 fix 2026-03-06 13:07:03 +01:00
charlesgauthereau a73e29efc9 Merge branch 'main' into dev 2026-03-06 13:05:51 +01:00
charlesgauthereau 26273b7240 fix 2026-03-06 13:04:58 +01:00
github-actions[bot] c925407f05 chore: release v1.4.40 2026-03-06 10:51:11 +00:00
622fc369d0 fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 11:50:44 +01:00
charlesgauthereau 0c4b7573cc Merge branch 'main' into dev 2026-03-06 11:49:58 +01:00
charlesgauthereau 401ce5c290 fix 2026-03-06 11:49:35 +01:00
github-actions[bot] 9580ad3934 chore: release v1.4.39 2026-03-06 10:48:15 +00:00
b33c0810d5 fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 11:47:44 +01:00
charlesgauthereau e3e84e0d18 Merge branch 'main' into dev
# Conflicts:
#	.github/workflows/auto-release.yml
2026-03-06 11:47:26 +01:00
charlesgauthereau 55cb4a7144 fix 2026-03-06 11:46:33 +01:00
8fbe639aa1 fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 11:45:11 +01:00
charlesgauthereau 9bc042e2cd Merge branch 'main' into dev
# Conflicts:
#	.github/workflows/auto-release.yml
2026-03-06 11:44:17 +01:00
charlesgauthereau 67302c6a17 fix 2026-03-06 11:43:48 +01:00
github-actions[bot] d7e2ec3b08 chore: release v1.4.38 2026-03-06 10:25:41 +00:00
fdaca0e8a7 dev (#171)
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 11:25:13 +01:00
charlesgauthereau 3af5f89606 Merge branch 'main' into dev
# Conflicts:
#	.github/workflows/auto-release.yml
2026-03-06 11:24:39 +01:00
charlesgauthereau 3ccccfc5e5 fix 2026-03-06 11:24:12 +01:00
github-actions[bot] 7959f903bb chore: release v1.4.37 2026-03-06 10:22:43 +00:00
59eb91f28f fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 11:22:16 +01:00
charlesgauthereau 9aecfb22a5 Merge branch 'main' into dev
# Conflicts:
#	.github/workflows/auto-release.yml
2026-03-06 11:21:31 +01:00
charlesgauthereau ba4476a3a7 fix 2026-03-06 11:21:11 +01:00
64724d2a92 fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 10:53:49 +01:00
charlesgauthereau be58ebdb7d Merge branch 'main' into dev 2026-03-06 10:53:05 +01:00
charlesgauthereau 746a9f5eb6 fix 2026-03-06 10:49:04 +01:00
github-actions[bot] 91a6d672fa chore: release v1.4.36 2026-03-06 09:42:31 +00:00
50809476fa fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 10:40:27 +01:00
charlesgauthereau 483f9cf192 Merge branch 'main' into dev
# Conflicts:
#	.github/workflows/auto-release.yml
2026-03-06 10:39:45 +01:00
charlesgauthereau f1c7dcda45 fix 2026-03-06 10:39:28 +01:00
282ffb543c fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 10:34:13 +01:00
charlesgauthereau c840196a14 fix 2026-03-06 10:33:38 +01:00
charlesgauthereau 7d76037a43 Merge branch 'main' into dev 2026-03-06 10:33:18 +01:00
68fea480ae fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 10:30:09 +01:00
charlesgauthereau b063e5c276 fix 2026-03-06 10:29:27 +01:00
charlesgauthereau f2d98505f1 Merge branch 'main' into dev 2026-03-06 10:29:14 +01:00
ec64ea8229 fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 10:25:44 +01:00
charlesgauthereau 59dc181625 Merge branch 'main' into dev
# Conflicts:
#	.github/workflows/auto-release.yml
2026-03-06 10:25:16 +01:00
charlesgauthereau 79387b83f6 fix 2026-03-06 10:24:58 +01:00
github-actions[bot] b13433e554 chore: release v1.4.37 2026-03-06 09:03:53 +00:00
d9fd382cb2 fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 10:03:23 +01:00
charlesgauthereau baa5e300c8 Merge branch 'main' into dev
# Conflicts:
#	.github/workflows/auto-release.yml
2026-03-06 10:02:53 +01:00
charlesgauthereau 51747773ba fix 2026-03-06 10:02:32 +01:00
github-actions[bot] 4d324d885d chore: release v1.4.36 2026-03-06 08:59:45 +00:00
af1a437523 fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 09:59:15 +01:00
charlesgauthereau dd8663591e Merge branch 'main' into dev
# Conflicts:
#	.github/workflows/auto-release.yml
2026-03-06 09:58:42 +01:00
charlesgauthereau de4f31f255 fix 2026-03-06 09:58:11 +01:00
charlesgauthereau 53c1bc9e59 fix 2026-03-06 09:56:13 +01:00
github-actions[bot] 77901ae1bc chore: release v1.4.35 2026-03-06 08:55:03 +00:00
1aeff542e5 fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 09:54:33 +01:00
charlesgauthereau c05469d704 Merge branch 'main' into dev
# Conflicts:
#	.github/workflows/auto-release.yml
2026-03-06 09:53:43 +01:00
charlesgauthereau 1e6bb542f9 fix 2026-03-06 09:53:23 +01:00
github-actions[bot] b81ad079ee chore: release v1.4.34 2026-03-06 08:49:01 +00:00
044edcda07 fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 09:48:32 +01:00
charlesgauthereau 7eb85d2a99 Merge branch 'main' into dev
# Conflicts:
#	.github/workflows/auto-release.yml
2026-03-06 09:47:23 +01:00
charlesgauthereau 7d4d6cd9df fix 2026-03-06 09:47:02 +01:00
github-actions[bot] 3f7eb4528e chore: release v1.4.33 2026-03-06 08:37:28 +00:00
0008f70552 fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 09:36:59 +01:00
charlesgauthereau ff48517b43 fix 2026-03-06 09:36:31 +01:00
github-actions[bot] 2695715180 chore: release v1.4.32 2026-03-06 08:34:59 +00:00
b3be1a561c fix: workflow
* fix

* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 09:34:31 +01:00
charlesgauthereau cab7a7f8bb Merge branch 'main' into dev 2026-03-06 09:33:51 +01:00
charlesgauthereau f6da5961c3 fix 2026-03-06 09:33:23 +01:00
github-actions[bot] d297f1a27b chore: release v1.4.31 2026-03-06 08:31:07 +00:00
b513509b01 fix: workflow
* fix

* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 09:30:39 +01:00
charlesgauthereau bfa3f7e04e Merge branch 'main' into dev
# Conflicts:
#	.github/workflows/auto-release.yml
2026-03-06 09:30:01 +01:00
charlesgauthereau 539b516e45 fix 2026-03-06 09:29:35 +01:00
github-actions[bot] 25744efc66 chore: release v1.4.30 2026-03-06 08:25:40 +00:00
21e37366fb fix: workflow
* fix

* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 09:25:07 +01:00
charlesgauthereau 6002ad44e1 Merge branch 'main' into dev
# Conflicts:
#	.github/workflows/auto-release.yml
2026-03-06 09:24:36 +01:00
charlesgauthereau a431270718 fix 2026-03-06 09:24:09 +01:00
github-actions[bot] 3f6a47c39d chore: release v1.4.29 2026-03-06 08:22:18 +00:00
4dace0a32b fix: workflow
* fix

* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 09:21:47 +01:00
charlesgauthereau efb5318fc5 Merge branch 'main' into dev
# Conflicts:
#	.github/workflows/auto-release.yml
2026-03-06 09:21:11 +01:00
charlesgauthereau a5e83f3133 fix 2026-03-06 09:20:51 +01:00
github-actions[bot] 7b6ca12744 chore: release v1.4.28 2026-03-06 08:18:49 +00:00
da1b5a969c fix: workflow
* fix

* fix

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
2026-03-06 09:18:19 +01:00
charlesgauthereau 5d790231ea Merge branch 'main' into dev
# Conflicts:
#	.github/workflows/auto-release.yml
2026-03-06 09:17:28 +01:00
charlesgauthereau ad363ce38c fix 2026-03-06 09:16:58 +01:00
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
charlesgauthereau 13d3cfb290 Merge branch 'main' into dev 2026-03-06 09:11:05 +01:00
charlesgauthereau c657bdfb63 fix 2026-03-06 09:10:47 +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
Théo LAGACHE 3bf731055f add: oidc,passkey,disable email/password,sign-up 2026-02-13 16:48:13 +01:00
207 changed files with 47468 additions and 5536 deletions
+40 -8
View File
@@ -1,12 +1,11 @@
# Environnement
# Environment
NODE_ENV=development
# Base de données
# Database
DATABASE_URL=postgresql://devuser:changeme@localhost:5433/devdb?schema=public
# Projet
# Project
PROJECT_NAME="Portabase"
PROJECT_DESCRIPTION="Portabase is a powerful database manager"
PROJECT_URL=http://localhost:8887
PROJECT_SECRET=
@@ -21,11 +20,44 @@ SMTP_FROM=
#Port 465 -> secure: true
#By default : true
# Google
AUTH_GOOGLE_ID=
AUTH_GOOGLE_SECRET=
AUTH_GOOGLE_METHOD=
# 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="* * * * *"
TRUSTED_DOMAINS="http://localhost:8887, http://localhost:3055, http://localhost:3056"
+1 -1
View File
@@ -60,7 +60,7 @@ representative at an online or offline event.
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
contact@soluce-technologies.com.
contact@portabase.io.
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
+3 -3
View File
@@ -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! 🙌
---
---
+1 -1
View File
@@ -18,7 +18,7 @@ We take security seriously and aim to support the following versions of the proj
If you discover a security vulnerability in this project, we appreciate your help in disclosing it responsibly.
1. **Contact Us**
Please report the vulnerability by emailing **[contact@soluce-technologies.com](mailto:contact@soluce-technologies.com)**. Include the following details:
Please report the vulnerability by emailing **[contact@portabase.io](mailto:contact@portabase.io)**. Include the following details:
- A detailed description of the issue.
- Steps to reproduce the vulnerability (if applicable).
- Any potential impacts or risks.
+73
View File
@@ -0,0 +1,73 @@
name: Discord Notification
on:
workflow_call:
inputs:
release_tag:
required: true
type: string
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 "${{ inputs.release_tag }}" -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="${{ inputs.release_tag }}"; 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"
+67 -47
View File
@@ -3,10 +3,16 @@ name: Docker Publish
on:
workflow_call:
inputs:
version:
required: true
type: string
ref:
required: true
type: string
image_name:
required: false
type: string
default: 'portabase/portabase'
default: "portabase/portabase"
add_latest:
required: false
type: boolean
@@ -14,11 +20,11 @@ on:
target:
required: false
type: string
default: 'prod'
default: "prod"
dockerfile:
required: false
type: string
default: './docker/dockerfile/Dockerfile'
default: "./docker/dockerfile/Dockerfile"
secrets:
DOCKER_USERNAME:
required: true
@@ -26,10 +32,9 @@ on:
required: true
jobs:
build:
name: Build and push Docker images
runs-on: ${{ matrix.platform == 'linux/amd64' && 'ubuntu-latest' || matrix.platform == 'linux/arm64' && 'ubuntu-24.04-arm' }}
name: Build architectures
runs-on: ${{ matrix.platform == 'linux/amd64' && 'ubuntu-latest' || 'ubuntu-24.04-arm' }}
strategy:
fail-fast: false
matrix:
@@ -37,23 +42,26 @@ jobs:
steps:
- name: Checkout
uses: actions/checkout@v4
with:
ref: ${{ inputs.ref }}
fetch-depth: 0
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Set image tag
id: set-tags
- name: Prepare Image Tags
id: prep
run: |
REF_NAME=${GITHUB_REF#refs/tags/}
if [ "${{ matrix.platform }}" = "linux/amd64" ]; then
IMAGE="${{ inputs.image_name }}:$REF_NAME-amd64"
else
IMAGE="${{ inputs.image_name }}:$REF_NAME-arm64"
fi
echo "image=$IMAGE" >> $GITHUB_OUTPUT
ARCH=${{ matrix.platform == 'linux/amd64' && 'amd64' || 'arm64' }}
echo "image=${{ inputs.image_name }}:${{inputs.version}}-$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
@@ -62,61 +70,73 @@ jobs:
file: ${{ inputs.dockerfile }}
platforms: ${{ matrix.platform }}
push: true
tags: ${{ steps.set-tags.outputs.image }}
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: Prepare artifact name
id: artifact
run: |
platform=${{ matrix.platform }}
echo "safe_platform=${platform//\//-}" >> $GITHUB_OUTPUT
echo "${{ steps.set-tags.outputs.image }}" > image.txt
- name: Save image name for manifest
run: echo "${{ steps.prep.outputs.image }}" > image.txt
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
- uses: actions/upload-artifact@v4
with:
name: image-${{ steps.artifact.outputs.safe_platform }}
name: image-${{ steps.prep.outputs.safe_platform }}
path: image.txt
if-no-files-found: warn
compression-level: 6
overwrite: false
include-hidden-files: false
retention-days: 1
create-manifest:
name: Create multi-arch Docker manifest (Portabase)
name: Create multi-arch manifest
runs-on: ubuntu-latest
needs: build
steps:
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131
- uses: actions/download-artifact@v4
with:
name: image-linux-amd64
path: /tmp/digests/amd64
- uses: actions/download-artifact@37930b1c2abaa49bbe596cd826c3c89aef350131
- uses: actions/download-artifact@v4
with:
name: image-linux-arm64
path: /tmp/digests/arm64
- name: Login to Docker
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Generate semantic tags
id: tags
run: |
VERSION="${{ inputs.version }}"
IFS='.' read -r MAJOR MINOR PATCH <<< "$VERSION"
echo "VERSION_TAG=$VERSION" >> $GITHUB_OUTPUT
if [ -n "$MINOR" ]; then
echo "MINOR_TAG=$MAJOR.$MINOR" >> $GITHUB_OUTPUT
fi
if [ -n "$MAJOR" ]; then
echo "MAJOR_TAG=$MAJOR" >> $GITHUB_OUTPUT
fi
if [ "${{ inputs.add_latest }}" = "true" ]; then
echo "LATEST_TAG=latest" >> $GITHUB_OUTPUT
fi
- name: Extract Docker metadata
id: meta
uses: docker/metadata-action@v5
with:
images: ${{ inputs.image_name }}
tags: |
type=raw,value=${{ steps.tags.outputs.VERSION_TAG }}
type=raw,value=${{ steps.tags.outputs.MINOR_TAG }}
type=raw,value=${{ steps.tags.outputs.MAJOR_TAG }}
type=raw,value=${{ steps.tags.outputs.LATEST_TAG }}
- name: Create and push manifest list
working-directory: /tmp/digests
run: |
DOCKER_IMAGES="$(cat amd64/image.txt) $(cat arm64/image.txt)"
REF_NAME=${GITHUB_REF#refs/tags/}
MANIFEST_IMAGE="${{ inputs.image_name }}:$REF_NAME"
docker buildx imagetools create $DOCKER_IMAGES -t $MANIFEST_IMAGE
docker buildx imagetools inspect $MANIFEST_IMAGE
if [ "${{ inputs.add_latest }}" = "true" ]; then
docker buildx imagetools create $DOCKER_IMAGES -t ${{ inputs.image_name }}:latest
docker buildx imagetools inspect ${{ inputs.image_name }}:latest
fi
TAG_ARGS=$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON")
echo $TAG_ARGS
docker buildx imagetools create $TAG_ARGS $DOCKER_IMAGES
+77
View File
@@ -0,0 +1,77 @@
name: End-to-end testing
on:
pull_request:
branches: [main]
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: 22
cache: 'pnpm'
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Build Docker image
uses: docker/build-push-action@v6
with:
context: .
file: ./docker/dockerfile/Dockerfile
push: false
load: true
tags: portabase/portabase:test
- name: Run app container
run: |
docker run -d --name myapp \
-p 8887:80 \
-e NODE_ENV=production \
-e PROJECT_URL=http://localhost:8887 \
-e PROJECT_SECRET=80af5e4c875dfded3a6ba511c6ed8b0160cad723f848ee0851403ed6141f6ba1 \
portabase/portabase:test
- name: Wait for app port to be listening
run: |
for i in {1..40}; do
if curl -fsS http://localhost:8887/ >/dev/null; then
echo "App is responding on port 8887"
exit 0
fi
echo "Waiting for app readiness... ($i/40)"
sleep 2
done
echo "Timeout: app never became ready"
docker logs myapp
exit 1
- name: Install Playwright browsers
run: pnpm exec playwright install --with-deps chromium firefox webkit
- name: Run Playwright tests
run: pnpm exec playwright test --project chromium
env:
PROJECT_URL: http://localhost:8887
- name: Upload report if failed
if: failure()
uses: actions/upload-artifact@v4
with:
name: playwright-report
path: playwright-report/
retention-days: 7
- name: Cleanup
if: always()
run: docker stop myapp && docker rm myapp || true
-128
View File
@@ -1,128 +0,0 @@
name: GitHub Release
on:
workflow_call:
inputs:
prerelease:
required: false
type: boolean
default: false
make_latest:
required: false
type: boolean
default: false
discord_title:
required: true
type: string
discord_color:
required: true
type: number
discord_footer:
required: true
type: string
secrets:
DISCORD_WEBHOOK:
required: true
GH_TOKEN:
required: true
jobs:
create-release:
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Check out the repo
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Build Changelog
id: build_changelog
uses: mikepenz/release-changelog-builder-action@v5
with:
mode: "COMMIT"
configurationJson: |
{
"template": "#{{CHANGELOG}}",
"categories": [
{
"title": "## Feature",
"labels": ["feat", "feature"]
},
{
"title": "## Fix",
"labels": ["fix", "bug"]
},
{
"title": "## Other",
"labels": []
}
],
"label_extractor": [
{
"pattern": "^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test){1}(\([\w\-\.]+\))?(!)?: ([\w ])+([\s\S]*)",
"on_property": "title",
"target": "$1"
}
]
}
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
- name: Create GitHub Release
uses: softprops/action-gh-release@v2
with:
generate_release_notes: false
body: ${{ steps.build_changelog.outputs.changelog }}
prerelease: ${{ inputs.prerelease }}
make_latest: ${{ inputs.make_latest }}
env:
GITHUB_TOKEN: ${{ secrets.GH_TOKEN }}
- name: Send Discord Notification
env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
GH_TOKEN: ${{ secrets.GH_TOKEN }}
run: |
RELEASE_INFO=$(gh release view "${{ github.ref_name }}" -R ${{ github.repository }} --json name,url,body,author)
RELEASE_TITLE=$(echo "$RELEASE_INFO" | jq -r .name)
if [ -z "$RELEASE_TITLE" ] || [ "$RELEASE_TITLE" = "null" ]; then RELEASE_TITLE="${{ github.ref_name }}"; fi
RELEASE_URL=$(echo "$RELEASE_INFO" | jq -r .url)
RELEASE_BODY=$(echo "$RELEASE_INFO" | jq -r .body)
AUTHOR_NAME="Portabase"
AUTHOR_ICON="https://github.com/Portabase.png"
PAYLOAD=$(jq -n \
--arg title "$RELEASE_TITLE" \
--arg description "$RELEASE_BODY" \
--arg url "$RELEASE_URL" \
--arg author "$AUTHOR_NAME" \
--arg icon "$AUTHOR_ICON" \
--arg discord_title "${{ inputs.discord_title }}" \
--arg discord_footer "${{ inputs.discord_footer }}" \
--argjson discord_color ${{ inputs.discord_color }} \
'{
content: $discord_title,
embeds: [{
title: $title,
url: $url,
description: $description,
color: $discord_color,
author: {
name: $author,
icon_url: $icon
},
footer: {
text: $discord_footer
}
}]
}'
)
curl -H "Content-Type: application/json" \
-d "$PAYLOAD" \
"$DISCORD_WEBHOOK"
+37
View File
@@ -0,0 +1,37 @@
name: Publish Helm Chart
on:
workflow_call:
inputs:
version:
required: true
type: string
secrets:
GH_TOKEN:
required: true
jobs:
publish-helm:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Set up Helm
uses: azure/setup-helm@v4
- name: Package Helm chart
run: |
mkdir -p ./helm-packages
helm package helm \
--version ${{ inputs.version }} \
--app-version ${{ inputs.version }} \
--destination ./helm-packages
- name: Authenticate to GitHub Packages
run: |
echo "${{ secrets.GH_TOKEN }}" | helm registry login ghcr.io -u ${{ github.actor }} --password-stdin
- name: Push Helm chart to GitHub Packages (OCI)
run: |
helm push ./helm-packages/portabase-${{ inputs.version }}.tgz oci://ghcr.io/portabase/charts
+8 -9
View File
@@ -3,29 +3,28 @@ name: Publish Docker image for release candidate
on:
push:
tags:
- '*-rc*'
- "*.*.*-rc*"
- "!*-*-rc*"
permissions:
contents: write
contents: read
packages: write
jobs:
docker_publish:
uses: ./.github/workflows/docker.yml
with:
add_latest: false
add_latest: true
secrets:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME_2 }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD_2 }}
github_release:
discord_notification:
needs: docker_publish
uses: ./.github/workflows/github.yml
uses: ./.github/workflows/discord.yml
with:
prerelease: true
make_latest: false
discord_title: "||@release-dashboard|| New release candidate published"
discord_color: 16776960
discord_title: "New Release Candidate: ${{ github.ref_name }}"
discord_color: 2044800
discord_footer: "Portabase"
secrets:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
+123 -13
View File
@@ -1,32 +1,142 @@
name: Publish Docker image for release
name: Auto Release & Publish
on:
push:
tags:
- '*.*.*'
- '!*-*'
pull_request:
types: [ closed ]
branches:
- main
permissions:
contents: write
packages: write
jobs:
docker_publish:
check-skip:
runs-on: ubuntu-latest
outputs:
skip: ${{ steps.set-skip.outputs.skip }}
steps:
- name: Determine if release should be skipped
id: set-skip
run: |
TITLE="${{ github.event.pull_request.title }}"
echo "PR title: $TITLE"
if [[ "$TITLE" == *"[skip-release]"* ]]; then
echo "PR title contains [skip-release], skipping release jobs."
echo "skip=true" >> $GITHUB_OUTPUT
else
echo "skip=false" >> $GITHUB_OUTPUT
fi
create-release:
needs: check-skip
if: ${{ needs.check-skip.outputs.skip == 'false' }}
runs-on: ubuntu-latest
outputs:
draft_tag: ${{ steps.release_step.outputs.draft_tag }}
version: ${{ steps.release_step.outputs.version }}
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: main
- 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]'
git config --global user.email 'github-actions[bot]@users.noreply.github.com'
- name: Run release-it
id: release_step
run: |
git pull origin main
VERSION=$(pnpm exec release-it --ci --release-version)
echo $VERSION
echo "version=$VERSION" >> $GITHUB_OUTPUT
OUTPUT=$(pnpm exec release-it --ci)
echo "$OUTPUT"
DRAFT_TAG=$(echo "$OUTPUT" | grep -oE 'untagged-[a-z0-9]+')
echo $DRAFT_TAG
echo "draft_tag=$DRAFT_TAG" >> $GITHUB_OUTPUT
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
publish-docker:
needs: create-release
if: ${{ needs.create-release.result == 'success' }}
uses: ./.github/workflows/docker.yml
with:
version: ${{ needs.create-release.outputs.version }}
ref: ${{ needs.create-release.outputs.version }}
add_latest: true
secrets:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME_2 }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD_2 }}
github_release:
needs: docker_publish
uses: ./.github/workflows/github.yml
publish-helm:
needs: create-release
if: ${{ needs.create-release.result == 'success' }}
uses: ./.github/workflows/helm.yml
with:
prerelease: false
make_latest: true
discord_title: "||@release-dashboard|| New release published"
discord_color: 5814783
version: ${{ needs.create-release.outputs.version }}
secrets:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
finalize-release:
needs:
- create-release
- publish-docker
- publish-helm
runs-on: ubuntu-latest
outputs:
release_tag: ${{ steps.publish_release_step.outputs.release_tag }}
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Publish GitHub Release
id: publish_release_step
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
OUTPUT=$(gh release edit ${{ needs.create-release.outputs.draft_tag }} --draft=false)
echo "$OUTPUT"
RELEASE_TAG=$(echo "$OUTPUT" | sed -E 's|.*/releases/tag/||')
echo "release_tag=$RELEASE_TAG" >> $GITHUB_OUTPUT
notify-discord:
needs:
- publish-docker
- publish-helm
- create-release
- finalize-release
uses: ./.github/workflows/discord.yml
with:
release_tag: ${{ needs.create-release.outputs.version }}
discord_title: "New Release"
discord_color: 3066993
discord_footer: "Portabase"
secrets:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
+6 -6
View File
@@ -5,27 +5,27 @@ on:
branches: [ main ]
jobs:
sca-deps: # Dependency & container scan
sca-deps:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: aquasecurity/trivy-action@0.20.0
- uses: aquasecurity/trivy-action@0.35.0
with:
scan-type: 'fs'
format: 'table'
severity: 'CRITICAL,HIGH'
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
+10 -1
View File
@@ -44,4 +44,13 @@ private/keys/*
/.env
certificates
seeds/pocket-id/*.db
certificates
# Playwright
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
/playwright/.auth/
+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"
]
+57
View File
@@ -0,0 +1,57 @@
{
"github": {
"release": true,
"draft": true,
"tokenRef": "GITHUB_TOKEN"
},
"git": {
"commit": true,
"commitMessage": "chore: release ${version}",
"requireCleanWorkingDir": true,
"tag": true,
"tagName": "${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"
}
}
}
}
+9 -4
View File
@@ -1,6 +1,6 @@
cff-version: 1.2.0
title: Portabase
message: "If you use this software, please cite it as below."
message: If you use this software, please cite it as below.
type: software
authors:
- family-names: Gauthereau
@@ -9,7 +9,12 @@ authors:
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."
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
@@ -26,5 +31,5 @@ keywords:
- web-ui
- agent
license: Apache-2.0
version: 1.2.6
date-released: "2026-02-15"
version: 1.9.5
date-released: '2026-03-02'
+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"
+22 -7
View File
@@ -12,18 +12,20 @@
[![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)
[![Docker Pulls](https://img.shields.io/docker/pulls/portabase/portabase?color=brightgreen)](https://hub.docker.com/r/portabase/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/)
[![Redis](https://img.shields.io/badge/Redis-DC382D?style=flat&logo=Redis&logoColor=white)](https://redis.io/)
[![Valkey](https://img.shields.io/badge/Valkey-6284fc?style=flat&logo=Valkey&logoColor=white)](https://valkey.io/)
[![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]
@@ -34,14 +36,14 @@
<strong>
<a href="https://portabase.io">Website</a> •
<a href="https://portabase.io/docs">Documentation</a> •
<a href="https://www.youtube.com/watch?v=hvLbX5LN1UE">Demo</a> •
<a href="https://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/f5ec7204-79c0-43ac-ba17-f2ed682c39d6)
![portabase-dashboard](https://github.com/user-attachments/assets/8f2c69d6-f1f9-4b80-b51c-01f6f13b9b62)
</div>
@@ -51,13 +53,27 @@
You have 4 ways to install Portabase:
- Automated CLI (recommended) - [details](https://portabase.io/docs/dashboard/setup#cli)
- Docker Compose setup - [details](https://portabase.io/docs/dashboard/setup#docker)
- Kubernetes with Helm (soon)
- 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 [details](https://portabase.io/docs/dashboard/setup#helm)
- Development setup - [details](https://portabase.io/docs/dashboard/setup#development)
**Ensure Docker is installed on your machine before getting started.**
## Supported databases
| Engine | Support | Supported Versions | Restore |
|:-------------------|:----------|:----------------------------------|:--------|
| **PostgreSQL** | ✅ Stable | 12, 13, 14, 15, 16, 17 et 18 | Yes |
| **MySQL** | ✅ Stable | 5.7, 8 et 9 | Yes |
| **MariaDB** | ✅ Stable | 10 et 11 | Yes |
| **MongoDB** | ✅ Stable | 4, 5, 6, 7 et 8 | Yes |
| **SQLite** | ✅ Stable | 3.x | Yes |
| **Redis** | ✅ Stable | 2.8+ | No |
| **Valkey** | ✅ Stable | 7.2+ | No |
| **MSSQL Server** | ❌ Ongoing | - | Yes |
See the [Database Servers documentation](https://portabase.io/docs/agent/db) for version-specific backup and restore details.
## Contributors
@@ -89,4 +105,3 @@ Distributed under the Apache License. See `LICENSE.txt` for more details.
[ShadcnUI-url]: https://ui.shadcn.com/
[Docker-url]: https://www.docker.com/
+11 -7
View File
@@ -1,10 +1,15 @@
import {CardContent, CardHeader} from "@/components/ui/card";
import { CardContent, CardHeader } from "@/components/ui/card";
import {TooltipProvider} from "@/components/ui/tooltip";
import {ForgotPasswordForm} from "@/components/wrappers/auth/login/forgot-password-form/forgot-password-form";
import {CardAuth} from "@/features/layout/card-auth";
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>
@@ -12,12 +17,11 @@ export default async function RoutePage(props: { searchParams: Promise<{ callbac
<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>
<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/>
<ForgotPasswordForm />
</CardContent>
</CardAuth>
</TooltipProvider>
+30 -27
View File
@@ -1,32 +1,35 @@
import React 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 { redirect } from "next/navigation";
import { currentUser } from "@/lib/auth/current-user";
import { AuthLogoSection } from "@/components/wrappers/auth/auth-logo-section";
import { Heart } from "lucide-react";
export default async function Layout({children}: { children: React.ReactNode }) {
export default async function Layout({
children,
}: {
children: React.ReactNode;
}) {
const user = await currentUser();
const user = await currentUser();
if (user && !user.banned && user.role !== "pending") {
redirect("/dashboard/home");
}
if (user && !user.banned && user.role !== "pending") {
redirect("/dashboard/home");
}
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" /> by <span className="font-medium text-foreground">Portabase</span>
</p>
<p>v{env.NEXT_PUBLIC_PROJECT_VERSION}</p>
</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>
</footer>
</div>
);
}
+49 -42
View File
@@ -1,52 +1,59 @@
import {LoginForm} from "@/components/wrappers/auth/login/login-form/login-form";
import {Metadata} from "next";
import {SUPPORTED_PROVIDERS} from "../../../portabase.config";
import {SocialAuthButtons} from "@/components/wrappers/auth/social-buttons";
import {TooltipProvider} from "@/components/ui/tooltip";
import {CardContent, CardHeader} from "@/components/ui/card";
import { 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 { Separator } from "@/components/ui/separator";
import { CardAuth } from "@/features/layout/card-auth";
import { env } from "@/env.mjs";
export const metadata: Metadata = {
title: "Login",
title: "Login",
};
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"} />
)}
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">
<LoginForm/>
{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>
)}
{SUPPORTED_PROVIDERS.filter((p) => !p.isManual && p.isActive).length > 0 && (
<>
<div className="relative my-4 flex items-center justify-center overflow-hidden">
<Separator/>
<div className="px-2 text-center text-sm">OR</div>
<Separator/>
</div>
<SocialAuthButtons providers={SUPPORTED_PROVIDERS}/>
</>
)}
{SUPPORTED_PROVIDERS.filter((p) => !p.isManual && p.isActive).length >
0 && <SocialAuthButtons providers={SUPPORTED_PROVIDERS} />}
<div className="mt-4 text-center text-sm">
Don&apos;t have an account ?{" "}
<Link href="/register" className="underline">
Sign up
</Link>
</div>
</CardContent>
</CardAuth>
</TooltipProvider>
)
{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>
);
}
+19 -10
View File
@@ -1,15 +1,24 @@
import {PageParams} from "@/types/next";
import {RegisterForm} from "@/components/wrappers/auth/register/register-form/register-form";
import {Metadata} from "next";
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",
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>
);
}
+5 -1
View File
@@ -4,9 +4,13 @@ import { ResetPasswordForm } from "@/components/wrappers/auth/login/reset-passwo
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 { 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;
@@ -1,30 +0,0 @@
import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {db} from "@/db";
import {SettingsEmailSection} from "@/components/wrappers/dashboard/admin/settings/email/settings-email-section";
import {notFound} from "next/navigation";
export default async function RoutePage(props: PageParams<{}>) {
const settings = await db.query.setting.findFirst({
where: (fields, {eq}) => eq(fields.name, "system"),
});
if (!settings) {
notFound()
}
return (
<Page>
<PageHeader className="flex flex-col">
<div className="flex justify-between">
<PageTitle className="mb-3">System email</PageTitle>
</div>
</PageHeader>
<PageContent className="flex flex-col gap-5">
<SettingsEmailSection settings={settings}/>
</PageContent>
</Page>
);
}
@@ -1,29 +0,0 @@
import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {SettingsStorageSection} from "@/components/wrappers/dashboard/admin/settings/storage/settings-storage-section";
import {db} from "@/db";
import {notFound} from "next/navigation";
export default async function RoutePage(props: PageParams<{}>) {
const settings = await db.query.setting.findFirst({
where: (fields, {eq}) => eq(fields.name, "system"),
});
if (!settings) {
notFound()
}
return (
<Page>
<PageHeader className="flex flex-col">
<div className="flex justify-between">
<PageTitle className="mb-3">System storage</PageTitle>
</div>
</PageHeader>
<PageContent className="flex flex-col gap-5">
<SettingsStorageSection settings={settings}/>
</PageContent>
</Page>
);
}
@@ -6,6 +6,7 @@ import {SettingsTabs} from "@/components/wrappers/dashboard/admin/settings/setti
import {desc, isNull} from "drizzle-orm";
import * as drizzleDb from "@/db";
import {StorageChannelWith} from "@/db/schema/12_storage-channel";
import {NotificationChannelWith} from "@/db/schema/09_notification-channel";
export default async function RoutePage(props: PageParams<{}>) {
@@ -13,6 +14,8 @@ export default async function RoutePage(props: PageParams<{}>) {
where: (fields, {eq}) => eq(fields.name, "system"),
});
console.log(settings)
const storageChannels = await db.query.storageChannel.findMany({
with: {
organizations: true
@@ -21,7 +24,16 @@ export default async function RoutePage(props: PageParams<{}>) {
orderBy: desc(drizzleDb.schemas.storageChannel.createdAt)
}) as StorageChannelWith[]
if (!settings || !storageChannels ) {
const notificationChannels = await db.query.notificationChannel.findMany({
with: {
organizations: true
},
where: isNull(drizzleDb.schemas.notificationChannel.organizationId),
orderBy: desc(drizzleDb.schemas.notificationChannel.createdAt)
}) as NotificationChannelWith[]
if (!settings || !storageChannels || !notificationChannels ) {
notFound()
}
@@ -33,7 +45,7 @@ export default async function RoutePage(props: PageParams<{}>) {
</div>
</PageHeader>
<PageContent className="flex flex-col gap-5">
<SettingsTabs storageChannels={storageChannels} settings={settings} />
<SettingsTabs storageChannels={storageChannels} notificationChannels={notificationChannels} settings={settings} />
</PageContent>
</Page>
);
@@ -4,6 +4,7 @@ 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<{}>) {
@@ -21,6 +22,9 @@ export default async function RoutePage(props: PageParams<{}>) {
},
});
const credentialProvider = SUPPORTED_PROVIDERS.find(p => p.id === 'credential');
const isPasswordAuthEnabled = credentialProvider?.isActive || false;
return (
<Page>
<PageHeader className="flex flex-col">
@@ -32,7 +36,7 @@ export default async function RoutePage(props: PageParams<{}>) {
</div>
</PageHeader>
<PageContent className="flex flex-col gap-5">
<AdminUserList users={users}/>
<AdminUserList users={users} isPasswordAuthEnabled={isPasswordAuthEnabled}/>
</PageContent>
</Page>
);
@@ -1,61 +1,69 @@
import {PageParams} from "@/types/next";
import {Page, PageContent, PageDescription, PageTitle} from "@/features/layout/page";
import {db} from "@/db";
import { PageParams } from "@/types/next";
import {
Page,
PageContent,
PageDescription,
PageTitle,
} from "@/features/layout/page";
import { db } from "@/db";
import * as drizzleDb from "@/db";
import {eq} from "drizzle-orm";
import {notFound} from "next/navigation";
import {ButtonDeleteAgent} from "@/components/wrappers/dashboard/agent/button-delete-agent/button-delete-agent";
import {capitalizeFirstLetter} from "@/utils/text";
import {generateEdgeKey} from "@/utils/edge_key";
import {getServerUrl} from "@/utils/get-server-url";
import {AgentContentPage} from "@/components/wrappers/dashboard/agent/agent-content";
import {AgentDialog} from "@/features/agents/components/agent.dialog";
import {AgentType} from "@/features/agents/agents.schema";
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 }>) {
export default async function RoutePage(
props: PageParams<{ agentId: string }>,
) {
const { agentId } = await props.params;
const {agentId} = await props.params
const agent = await db.query.agent.findFirst({
where: eq(drizzleDb.schemas.agent.id, agentId),
with: {
databases: true,
},
});
const agent = await db.query.agent.findFirst({
where: eq(drizzleDb.schemas.agent.id, agentId),
with: {
databases: true
}
})
if (!agent) {
notFound();
}
if (!agent) {
notFound()
}
const edgeKey = await generateEdgeKey(getServerUrl(), agent.id);
const edgeKey = await generateEdgeKey(getServerUrl(), agent.id);
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>
<div className="flex items-center gap-2">
<ButtonDeleteAgent agentId={agentId} text={"Delete Agent"}/>
</div>
</div>
</PageTitle>
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>
<div className="flex items-center gap-2">
<ButtonDeleteAgent agentId={agentId} text={"Delete Agent"} />
</div>
</div>
</PageTitle>
</div>
{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>
)
}
{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>
);
}
@@ -35,12 +35,10 @@ export default async function RoutePage(props: PageParams<{}>) {
<PageHeader>
<PageTitle>Notification channels</PageTitle>
<PageActions>
{/*<NotifierAddEditModal adminView={false}/>*/}
<ChannelAddEditModal kind="notification" adminView={false}/>
</PageActions>
</PageHeader>
<PageContent>
{/*<NotificationChannelsSection organizations={organizations} notificationChannels={notificationChannels}/>*/}
<ChannelsSection kind="notification" organizations={organizations}
channels={notificationChannels}/>
</PageContent>
@@ -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>
);
}
@@ -8,6 +8,7 @@ import {getOrganizationProjectDatabases} from "@/lib/services";
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";
import {getHealthLast12hLogs} from "@/db/services/healthcheck";
export default async function RoutePage(props: PageParams<{
projectId: string;
@@ -83,6 +84,8 @@ export default async function RoutePage(props: PageParams<{
notFound();
}
const databaseHealthLogs = dbItem ? await getHealthLast12hLogs({ id: dbItem.id }) : []
const successRate = totalBackups > 0 ? (successfulBackups / totalBackups) * 100 : null;
@@ -95,6 +98,7 @@ export default async function RoutePage(props: PageParams<{
activeMember={activeMember}
settings={settings}
database={dbItem}
databaseHealthLogs={databaseHealthLogs}
isAlreadyRestore={isAlreadyRestore}
restorations={restorations}
backups={backups}
@@ -1,89 +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 {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 {Button} from "@/components/ui/button";
import {GearIcon} from "@radix-ui/react-icons";
import {db} from "@/db";
import {isNull} from "drizzle-orm";
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 { 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";
import { eq } from "drizzle-orm";
export const metadata: Metadata = {
title: "Settings",
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 || !activeMember || !user) {
notFound();
}
if (!organization || !activeMember || !user) {
notFound();
}
const notificationChannels = await getOrganizationChannels(organization.id)
const storageChannels = await getOrganizationStorageChannels(organization.id)
const permissions = computeOrganizationPermissions(activeMember);
const notificationChannels = await getOrganizationChannels(organization.id);
const storageChannels = await getOrganizationStorageChannels(organization.id);
const permissions = computeOrganizationPermissions(activeMember);
const users = await db.query.user.findMany({
where: (fields) => isNull(fields.deletedAt)
});
const users = await db.query.user.findMany({
where: (fields) => isNull(fields.deletedAt),
});
const organizationWithMembers = await db.query.organization.findFirst({
where: eq(drizzleDb.schemas.organization.id, organization.id),
const organizationWithMembers = await db.query.organization.findFirst({
where: eq(drizzleDb.schemas.organization.id, organization.id),
with: {
members: {
with: {
members: {
with: {
user: true
}
}
}
});
user: true,
},
},
},
});
if (!organizationWithMembers) notFound();
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>
)
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>
);
}
+22 -23
View File
@@ -1,27 +1,26 @@
import React, {ReactNode} 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 {ThemeMetaUpdater} from "@/features/browser/theme-meta-updater";
import {BackupModalProvider} from "@/components/wrappers/dashboard/database/backup/backup-modal-context";
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: 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">
<ThemeMetaUpdater/>
<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>
);
}
+14 -12
View File
@@ -1,7 +1,10 @@
import { NextResponse } from "next/server";
import { eq } from "drizzle-orm";
import { db } from "@/db";
import {NextResponse} from "next/server";
import {eq} from "drizzle-orm";
import {db} from "@/db";
import * as drizzleDb from "@/db";
import {logger} from "@/lib/logger";
const log = logger.child({module: "api/agent/backup/helpers"});
export function withAgentCheck(handler: Function) {
return async (request: Request, context: { params: Promise<{ agentId: string }> }) => {
@@ -14,23 +17,22 @@ export function withAgentCheck(handler: Function) {
if (!agent) {
return NextResponse.json(
{ error: "Agent not found" },
{ status: 404 }
{error: "Agent not found"},
{status: 404}
);
}
return handler(request, { ...context, agent });
return handler(request, {...context, agent});
} catch (err) {
console.error("Error in agent middleware:", err);
log.error({error: err, name: "withAgentCheck"}, "Error in agent middleware");
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
{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),
@@ -43,8 +45,8 @@ export async function getDatabaseOrThrow(generatedId: string) {
if (!database) {
throw NextResponse.json(
{ error: "Database associated with generatedId not found" },
{ status: 404 }
{error: "Database associated with generatedId not found"},
{status: 404}
);
}
+7 -6
View File
@@ -8,6 +8,9 @@ import {withUpdatedAt} from "@/db/utils";
import {eventEmitter} from "@/features/shared/event";
import {sendNotificationsBackupRestore} from "@/features/notifications/helpers";
import {EventKind} from "@/features/notifications/types";
import {logger} from "@/lib/logger";
const log = logger.child({module: "api/agent/backup/route"});
export type BodyPost = {
method: "manual" | "automatic"
@@ -77,7 +80,6 @@ export const POST = withAgentCheck(async (request: Request, {params, agent}: {
}
}
eventEmitter.emit('modification', {update: true});
return NextResponse.json(
@@ -88,7 +90,7 @@ export const POST = withAgentCheck(async (request: Request, {params, agent}: {
{status: 200}
);
} catch (error) {
console.error("Error in POST for INIT backup:", error);
log.error({error: error}, "Error in POST for INIT backup");
return NextResponse.json(
{error: "Internal server error"},
{status: 500}
@@ -102,6 +104,7 @@ export const PATCH = withAgentCheck(async (request: Request, {params, agent}: {
}) => {
try {
const body: BodyPatch = await request.json();
log.info({data: body}, "Body from PATH in backup route");
const status = body.status
const backupId = body.backupId
@@ -131,7 +134,7 @@ export const PATCH = withAgentCheck(async (request: Request, {params, agent}: {
eventEmitter.emit('modification', {update: true});
await sendNotificationsBackupRestore(database, `${status}_backup` as EventKind);
await sendNotificationsBackupRestore(database, status == "failed" ? "error_backup" : "success_backup" as EventKind);
return NextResponse.json(
{
@@ -141,9 +144,7 @@ export const PATCH = withAgentCheck(async (request: Request, {params, agent}: {
{status: 200}
);
} catch (error) {
console.error("Error in PATCH backup:", error);
log.error({error: error}, "Error in PATCH backup")
return NextResponse.json(
{error: "Internal server error"},
{status: 500}
@@ -5,6 +5,9 @@ import {db} from "@/db";
import {getDatabaseOrThrow, withAgentCheck} from "../../helpers";
import {isUuidv4} from "@/utils/verify-uuid";
import {eventEmitter} from "@/features/shared/event";
import {logger} from "@/lib/logger";
const log = logger.child({module: "api/agent/backup/upload/init"});
export type Body = {
generatedId: string
@@ -18,7 +21,7 @@ export const POST = withAgentCheck(async (request: Request, {params, agent}: {
try {
const body: Body = await request.json();
console.log("body", body);
log.info({data: body}, "Body for backup upload init");
const generatedId = body.generatedId;
const storageChannelId = body.storageChannelId;
@@ -66,7 +69,7 @@ export const POST = withAgentCheck(async (request: Request, {params, agent}: {
{status: 200}
);
} catch (error) {
console.error("Error in POST for INIT backup:", error);
log.error({error: error}, "Error in POST for INIT backup");
return NextResponse.json(
{error: "Internal server error"},
{status: 500}
@@ -5,6 +5,9 @@ import {db as dbClient, db} from "@/db";
import {withUpdatedAt} from "@/db/utils";
import {getDatabaseOrThrow, withAgentCheck} from "../../helpers";
import {eventEmitter} from "@/features/shared/event";
import {logger} from "@/lib/logger";
const log = logger.child({module: "api/agent/backup/upload/status"});
export type Body = {
generatedId: string
@@ -27,8 +30,7 @@ export const PATCH = withAgentCheck(async (request: Request, {params, agent}: {
const backupStorageId = body.backupStorageId;
const backupId = body.backupId;
console.log("body", body);
log.info({data: body}, "Body for backup upload status");
const database = await getDatabaseOrThrow(generatedId);
@@ -85,7 +87,7 @@ export const PATCH = withAgentCheck(async (request: Request, {params, agent}: {
{status: 200}
);
} catch (error) {
console.error("Error in POST for INIT backup:", error);
log.error({error: error},"Error in POST for INIT backup");
return NextResponse.json(
{error: "Internal server error"},
{status: 500}
+7 -7
View File
@@ -4,7 +4,9 @@ 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";
import {logger} from "@/lib/logger";
const log = logger.child({module: "api/agent/restore"});
export type BodyResultRestore = {
generatedId: string
@@ -63,18 +65,16 @@ export async function POST(
.set({status: body.status as RestorationStatus})
.where(eq(drizzleDb.schemas.restoration.id, restoration.id));
await sendNotificationsBackupRestore(database, body.status == "error" ? "error_restore" : "success_restore");
await sendNotificationsBackupRestore(database, body.status == "failed" ? "error_restore" : "success_restore");
const response = {
message: true,
details: "Restoration successfully updated"
status: true,
message: "Restoration successfully updated"
}
return Response.json(response, {status: 200})
} catch (error) {
console.error('Error in POST handler:', error);
log.error({error: error}, "Error in POST handler")
return NextResponse.json(
{error: 'Internal server error'},
{status: 500}
+34 -7
View File
@@ -2,7 +2,7 @@ import {NextResponse} from "next/server";
import {Body} from "./route";
import {isUuidv4} from "@/utils/verify-uuid";
import {Agent} from "@/db/schema/08_agent";
import {Database, DatabaseWith} from "@/db/schema/07_database";
import {DatabaseWith} from "@/db/schema/07_database";
import * as drizzleDb from "@/db";
import {db, db as dbClient} from "@/db";
import {and, eq, inArray} from "drizzle-orm";
@@ -11,6 +11,9 @@ 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";
import {logger} from "@/lib/logger";
const log = logger.child({module: "api/agent/status/helpers"});
export async function handleDatabases(body: Body, agent: Agent, lastContact: Date, settings: Setting) {
const databasesResponse = [];
@@ -56,7 +59,7 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
}
if (!dbmsEnumSchema.safeParse(db.dbms).success) {
console.log(`Database type not available: ${db.dbms}`);
log.error({name: "handleDatabases"},`Database type not available: ${db.dbms}`);
continue;
}
@@ -67,12 +70,24 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
name: db.name,
dbms: db.dbms as EDbmsSchema,
agentDatabaseId: db.generatedId,
lastContact: lastContact,
lastContact: db.pingStatus ? lastContact : null,
healthErrorCount: null
})
.returning();
if (databaseCreated) {
await dbClient
.insert(drizzleDb.schemas.healthcheckLog)
.values({
kind: "database",
status: db.pingStatus ? "success" : "failed",
objectId: databaseCreated.id,
date: lastContact
})
const storages = await getDatabaseStorageChannels(databaseCreated.id)
databasesResponse.push(formatDatabase(databaseCreated, backupAction, restoreAction, urlBackup, storages, null));
@@ -84,11 +99,24 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
.set(withUpdatedAt({
name: db.name,
agentId: agent.id,
lastContact: lastContact
dbms: db.dbms as EDbmsSchema,
lastContact: db.pingStatus ? lastContact : existingDatabase.lastContact,
healthErrorCount: db.pingStatus ? null : existingDatabase.healthErrorCount,
}))
.where(eq(drizzleDb.schemas.database.id, existingDatabase.id))
.returning();
await dbClient
.insert(drizzleDb.schemas.healthcheckLog)
.values({
kind: "database",
status: db.pingStatus ? "success" : "failed",
objectId: databaseUpdated.id,
date: lastContact
})
const activeBackup = await dbClient.query.backup.findFirst({
where: and(
eq(drizzleDb.schemas.backup.databaseId, databaseUpdated.id),
@@ -159,11 +187,11 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
.where(eq(drizzleDb.schemas.restoration.id, restoration.id));
const errorMessage = "Failed to get backup URL";
console.error("Restoration failed: ", errorMessage);
log.error({error: errorMessage, name: "handleDatabases"}, "Restoration failed");
continue;
}
} catch (err) {
console.error("Restoration crashed unexpectedly:", err);
log.error({error: err, name: "handleDatabases"}, "Restoration crashed unexpectedly");
await dbClient
.update(drizzleDb.schemas.restoration)
.set({status: "failed"})
@@ -180,7 +208,6 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
databasesResponse.push(formatDatabase(databaseUpdated, backupAction, restoreAction, urlBackup, storages, urlMeta));
}
}
return databasesResponse;
}
+19 -8
View File
@@ -6,13 +6,17 @@ 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";
import {logger} from "@/lib/logger";
const log = logger.child({module: "api/agent/status/route"});
export type databaseAgent = {
name: string,
dbms: EDbmsSchema,
generatedId: string
pingStatus: boolean
}
export type Body = {
@@ -27,14 +31,14 @@ export async function POST(
) {
try {
const agentId = (await params).agentId
console.log(agentId)
log.info(`Agent ID: ${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.error(message)
log.error({error: message}, "An error occurred")
return NextResponse.json(
{error: "agentId is not a valid uuid"},
{status: 500}
@@ -55,17 +59,25 @@ export async function POST(
return NextResponse.json({error: "An error occured"}, {status: 404})
}
const databasesResponse = await handleDatabases(body, agent, lastContact, settings)
await db
.update(drizzleDb.schemas.agent)
.set(withUpdatedAt({
version: body.version,
lastContact: lastContact
lastContact: lastContact,
healthErrorCount: null
}))
.where(eq(drizzleDb.schemas.agent.id, agentId));
await db
.insert(drizzleDb.schemas.healthcheckLog)
.values({
kind: "agent",
status: "success",
objectId: agentId,
date: lastContact
})
const response = {
agent: {
@@ -75,10 +87,9 @@ export async function POST(
databases: databasesResponse
}
return Response.json(response)
} catch (error) {
console.error('Error in POST handler:', error);
log.error({error: error}, "Error in POST handler")
return NextResponse.json(
{error: 'Internal server error'},
{status: 500}
+6 -6
View File
@@ -1,9 +1,9 @@
import {NextResponse} from "next/server";
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,
});
return NextResponse.json({
PROJECT_URL: process.env.PROJECT_URL,
PROJECT_NAME: process.env.PROJECT_NAME,
PROJECT_DESCRIPTION: process.env.PROJECT_DESCRIPTION,
});
}
+6 -3
View File
@@ -2,6 +2,9 @@ import {auth} from "@/lib/auth/auth";
import {headers} from "next/headers";
import {NextResponse} from "next/server";
import {eventEmitter} from "@/features/shared/event";
import {logger} from "@/lib/logger";
const log = logger.child({module: "api/events"});
export async function GET(request: Request) {
@@ -16,9 +19,9 @@ export async function GET(request: Request) {
return new Response(
new ReadableStream({
start(controller) {
console.log('Stream started');
log.info("Stream started");
const handleModification = (data: any) => {
console.log('Modification event triggered:', data);
log.info({data: data},"Modification event triggered");
controller.enqueue(`event: modification\n`);
controller.enqueue(`data: ${JSON.stringify(data)}\n\n`);
};
@@ -26,7 +29,7 @@ export async function GET(request: Request) {
eventEmitter.on('modification', handleModification);
request.signal.addEventListener('abort', () => {
console.log('Client disconnected');
log.info("Client disconnected");
controller.close();
eventEmitter.off('modification', handleModification);
});
+4 -1
View File
@@ -3,6 +3,9 @@ import path from "path";
import type {StorageInput} from "@/features/storages/types";
import {dispatchStorage} from "@/features/storages/dispatch";
import {Readable} from "node:stream";
import {logger} from "@/lib/logger";
const log = logger.child({module: "api/files/backups"});
export async function GET(
request: Request,
@@ -30,7 +33,7 @@ export async function GET(
}
};
console.debug(input);
log.info({input: input}, "Dispatch Storage");
const result = await dispatchStorage(input, undefined, storageId);
+5 -5
View File
@@ -1,12 +1,12 @@
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";
import {logger} from "@/lib/logger";
const log = logger.child({module: "api/files/images"});
export async function GET(
req: Request,
@@ -56,7 +56,7 @@ export async function GET(
const result = await dispatchStorage(input, undefined, storageId);
if (!result.file || !(result.file instanceof Readable)) {
console.error(`An error occurred while getting file :`, result);
log.error({error: result}, `An error occurred while getting file`);
return NextResponse.json(
{error: "Invalid file payload"},
{status: 500}
@@ -82,7 +82,7 @@ export async function GET(
});
} catch (err) {
console.error("Error streaming image:", err);
log.error({error: err}, `Error streaming image`);
return NextResponse.json({error: "Error fetching file"}, {status: 500});
}
}
+5 -5
View File
@@ -2,7 +2,9 @@ import {NextResponse} from "next/server";
import fs from "fs";
import path from "path";
import {env} from "@/env.mjs";
import {logger} from "@/lib/logger";
const log = logger.child({module: "api/tus/hooks"});
export async function POST(request: Request) {
try {
@@ -13,7 +15,7 @@ export async function POST(request: Request) {
const uploadOffset = headers["Upload-Offset"]?.[0];
const status = headers["X-Status"]?.[0];
console.log(`Upload ID : ${event.Upload.ID} (${uploadOffset}/${uploadLength})`);
log.info(`Upload ID : ${event.Upload.ID} (${uploadOffset}/${uploadLength})`);
if (status === "success") {
if (
@@ -29,9 +31,7 @@ export async function POST(request: Request) {
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 uploadDir = path.join(env.PRIVATE_PATH!, "/uploads/");
const oldFilePath = path.join(uploadDir, "tmp", id);
const newFilePath = path.join(uploadDir, filePath);
@@ -74,7 +74,7 @@ export async function POST(request: Request) {
}
return NextResponse.json({});
} catch (error) {
console.error("Hook error:", error);
log.error({error: error},"TUS Hook error");
return NextResponse.json({error: "Internal server error"}, {status: 500});
}
}
+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>
)
}
+33 -29
View File
@@ -1,38 +1,42 @@
import React from "react";
import type {Metadata} from "next";
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 {author, geistMono, poppins} from "@/fonts/fonts";
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 title = process.env.PROJECT_NAME ?? "App Title";
const title = process.env.PROJECT_NAME ?? "Portabase";
export const metadata: Metadata = {
title: {
default: title,
template: `%s - ${title}`
},
description: process.env.PROJECT_DESCRIPTION ?? undefined,
title: {
default: title,
template: `%s - ${title}`,
},
description: process.env.PROJECT_DESCRIPTION ?? undefined,
};
export default async function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
children,
}: Readonly<{
children: React.ReactNode;
}>) {
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>
);
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>
);
}
+14 -10
View File
@@ -1,15 +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>
<BackButton>Go home</BackButton>
</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>
);
}
+19 -21
View File
@@ -1,30 +1,28 @@
"use client";
import {PropsWithChildren, Suspense} from "react";
import {ThemeProvider} from "@/features/theme/theme-provider";
import {Toaster} from "@/components/ui/sonner";
import {QueryClient, QueryClientProvider} from "@tanstack/react-query";
import {ThemeMetaUpdaterRoot} from "@/features/browser/theme-meta-updater-root";
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { type PropsWithChildren, Suspense } from "react";
import { Toaster } from "@/components/ui/sonner";
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
>
<ThemeMetaUpdaterRoot/>
<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>
);
};
+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:
+1 -1
View File
@@ -4,7 +4,7 @@ services:
# context: .
# dockerfile: docker/dockerfile/Dockerfile
# target: prod
image: portabase/portabase:latest
image: portabase/portabase:1.7.1
ports:
- '8887:80'
environment:
+2 -1
View File
@@ -12,7 +12,7 @@ services:
- POSTGRES_USER=devuser
- POSTGRES_PASSWORD=changeme
healthcheck:
test: [ "CMD-SHELL", "pg_isready -U devuser -d devdb" ]
test: ["CMD-SHELL", "pg_isready -U devuser -d devdb"]
interval: 10s
timeout: 5s
retries: 5
@@ -30,6 +30,7 @@ services:
- "localhost:host-gateway"
volumes:
- ./private/uploads/tmp:/data/uploads/tmp
user: "1000:1000"
volumes:
postgres-data:
-3
View File
@@ -1,4 +1,3 @@
#FROM node:22-bullseye AS base
FROM --platform=$BUILDPLATFORM node:22-bullseye AS base
RUN apt-get update && apt-get install -y \
@@ -112,8 +111,6 @@ RUN chmod +x /usr/local/bin/tusd
COPY docker/nginx/nginx.conf /etc/nginx/nginx.conf
#RUN mkdir -p .next /app/private/uploads \
# && chown -R nextjs:nodejs .next /app/private /app/public
RUN mkdir -p .next /data/private/uploads \
&& chown -R nextjs:nodejs .next /data/private /app/public
+135
View File
@@ -0,0 +1,135 @@
import {test, expect, Page} from '@playwright/test';
type UserCredentials = {
username: string
email: string
password: string
}
const users: Record<string, UserCredentials> = {
admin: {username: 'Admin', email: 'admin@example.com', password: 'testPASS123456!'},
normal: {username: 'John Doe', email: 'john.doe@example.com', password: 'testPASS123456!'},
}
async function register(page: Page, name: string, email: string, password: string, confirmPassword: string) {
await page.locator('input[name="name"]').fill(name)
await page.locator('input[name="email"]').fill(email)
await page.locator('input[name="password"]').fill(password)
await page.locator('input[name="confirmPassword"]').fill(confirmPassword)
await page.click('button[type="submit"]')
}
async function login(page: Page, email: string, password: string) {
await page.locator('input[name="email"]').fill(email)
await page.locator('input[name="password"]').fill(password)
await page.locator('button:has-text("Login")').click()
}
async function logout() {
//TODO
}
test.describe.serial('User signup and login flows', () => {
test('Redirect to login if not connected', async ({page}) => {
await page.goto('/dashboard/projects');
await expect(page).toHaveURL("login?redirect=%2Fdashboard%2Fprojects");
});
test('Password too short', async ({page}) => {
await page.goto('')
await page.click('text=Sign up')
await expect(page).toHaveURL('/register')
let password = '123456'
await register(page, users["admin"].username, users["admin"].email, password, password)
const toast = page.locator('text=Must have at least 8 character')
await expect(toast).toBeVisible()
})
test('Password too simple', async ({page}) => {
await page.goto('')
await page.click('text=Sign up')
await expect(page).toHaveURL('/register')
let password = '12345678'
await register(page, users["admin"].username, users["admin"].email, password, password)
const toast = page.locator('text=Your password must contain at least one uppercase letter, one lowercase letter, one number, and one special character.')
await expect(toast).toBeVisible()
})
test('Password and confirm password mismatch', async ({page}) => {
await page.goto('/')
await page.click('text=Sign up')
await expect(page).toHaveURL('/register')
let password = 'testPASS123456!'
let confirmPassword = 'testPASS123456!!'
await register(page, users["admin"].username, users["admin"].email, password, confirmPassword)
const toast = page.locator('text=The passwords did not match')
await expect(toast).toBeVisible()
})
test('Successful register for admin', async ({page}) => {
await page.goto('/')
await page.click('text=Sign up')
await expect(page).toHaveURL('/register')
await register(page, users["admin"].username, users["admin"].email, users["admin"].password, users["admin"].password)
await expect(page).toHaveURL('/login')
})
test('User already exists.', async ({page}) => {
await page.goto('/')
await page.click('text=Sign up')
await expect(page).toHaveURL('/register')
await register(page, users["admin"].username, users["admin"].email, users["admin"].password, users["admin"].password)
const toast = page.locator('text=User already exists. Use another email.')
await expect(toast).toBeVisible()
})
test('Successful register for normal', async ({page}) => {
await page.goto('/')
await page.click('text=Sign up')
await expect(page).toHaveURL('/register')
await register(page, users["normal"].username, users["normal"].email, users["normal"].password, users["normal"].password)
await expect(page).toHaveURL('login')
})
test('Failed login because account not active', async ({page}) => {
await page.goto('/login')
await login(page, users["normal"].email, users["normal"].password)
const toast = page.locator('text=Your account is not active.')
await expect(toast).toBeVisible()
})
test('Successful login', async ({page}) => {
await page.goto('login')
await login(page, users["admin"].email, users["admin"].password)
await expect(page).toHaveURL('/dashboard/home')
})
})
// test('Successful logout', async ({page}) => {
// await logout()
// await expect(page).toHaveURL('/login')
// })
+17
View File
@@ -0,0 +1,17 @@
.DS_Store
.git/
.gitignore
.bzr/
.bzrignore
.hg/
.hgignore
.svn/
*.swp
*.bak
*.tmp
*.orig
*~
.project
.idea/
*.tmproj
.vscode/
+28
View File
@@ -0,0 +1,28 @@
apiVersion: v2
name: portabase
description: Helm chart for Portabase
type: application
version: 0.0.0
appVersion: "latest"
keywords:
- postgresql
- mariadb
- mongodb
- mysql
- sqlite
- backup
- database
- restore
home: https://github.com/Portabase/portabase
sources:
- https://github.com/Portabase/portabase
- https://github.com/Portabase/portabase/tree/main/helm
maintainers:
- name: Charles Gauthereau
url: https://github.com/RambokDev
- name: Killian Larcher
url: https://github.com/KillianLarcher
icon: https://raw.githubusercontent.com/Portabase/portabase/main/.github/assets/logo.png
+32
View File
@@ -0,0 +1,32 @@
# Development Notes
## Check that Kubernetes is reachable locally
```bash
kubectl get nodes
```
## Install the local Portabase Helm chart
```bash
helm install portabase . \
--set project.secret=$(openssl rand -hex 32)
```
## Check the pods
```bash
kubectl get pods
```
## Check the services
```bash
kubectl get svc
```
## If the service type is ClusterIP, expose it locally using port forwarding:
```bash
kubectl port-forward svc/portabase 8887:80
```
+33
View File
@@ -0,0 +1,33 @@
apiVersion: apps/v1
kind: Deployment
metadata:
name: portabase
spec:
replicas: 1
selector:
matchLabels:
app: portabase
template:
metadata:
labels:
app: portabase
spec:
containers:
- name: portabase
image: "{{ .Values.image.repository }}:{{ .Values.image.tag }}"
ports:
- containerPort: 80
env:
- name: TZ
value: {{ .Values.timezone }}
- name: PROJECT_URL
value: {{ .Values.project.url }}
- name: PROJECT_SECRET
value: {{ .Values.project.secret }}
volumeMounts:
- name: data
mountPath: /data
volumes:
- name: data
persistentVolumeClaim:
claimName: portabase-data
+10
View File
@@ -0,0 +1,10 @@
apiVersion: v1
kind: PersistentVolumeClaim
metadata:
name: portabase-data
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: {{ .Values.persistence.size }}
+12
View File
@@ -0,0 +1,12 @@
apiVersion: v1
kind: Service
metadata:
name: portabase
spec:
type: {{ .Values.service.type }}
selector:
app: portabase
ports:
- port: {{ .Values.service.port }}
targetPort: 80
nodePort: {{ .Values.service.nodePort }}
+47
View File
@@ -0,0 +1,47 @@
image:
repository: portabase/portabase
tag: latest
pullPolicy: Always
replicaCount: 1
service:
type: ClusterIP
port: 80
targetPort: 80
resources:
requests:
memory: "1Gi"
cpu: "1"
limits:
memory: "1Gi"
cpu: "1"
timezone: Europe/Paris
project:
url: http://localhost:8887
secret: "change_me_generate_secure_secret"
persistence:
enabled: true
storageClassName: ""
accessMode: ReadWriteOnce
size: 10Gi
mountPath: /data
ingress:
enabled: false
className: nginx
hosts:
- host: portabase.example.com
paths:
- path: /
pathType: Prefix
tls: []
updateStrategy:
type: RollingUpdate
rollingUpdate:
partition: 0
+3
View File
@@ -43,6 +43,9 @@ const nextConfig: NextConfig = {
typescript: {
ignoreBuildErrors: true,
},
logging: {
browserToTerminal: false,
},
experimental: {
serverActions: {
bodySizeLimit: "10gb",
+37 -30
View File
@@ -1,6 +1,6 @@
{
"name": "portabase",
"version": "1.2.6",
"version": "1.9.5",
"private": true,
"scripts": {
"dev": "next dev --turbopack -p 8887",
@@ -11,9 +11,12 @@
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:drop": "drizzle-kit drop",
"auth:generate": "npx @better-auth/cli generate --config ./src/lib/auth/auth.ts"
"auth:generate": "npx @better-auth/cli generate --config ./src/lib/auth/auth.ts",
"release": "release-it"
},
"dependencies": {
"@better-auth/passkey": "^1.5.6",
"@better-auth/sso": "^1.5.6",
"@hookform/resolvers": "^5.2.2",
"@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.15",
@@ -44,46 +47,46 @@
"@radix-ui/react-toggle-group": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.8",
"@react-email/components": "^0.0.41",
"@t3-oss/env-nextjs": "^0.13.10",
"@tanstack/react-query": "^5.90.21",
"@t3-oss/env-nextjs": "^0.13.11",
"@tanstack/react-query": "^5.95.2",
"@tanstack/react-table": "^8.21.3",
"@types/express": "^5.0.6",
"@types/nodemailer": "^6.4.22",
"@types/nodemailer": "^6.4.23",
"@types/ws": "^8.18.1",
"@zenstackhq/runtime": "2.14.2",
"argon2": "^0.43.1",
"bcrypt": "^6.0.0",
"better-auth": "1.4.5",
"better-auth": "1.5.6",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
"date-fns": "^4.1.0",
"dockerode": "^4.0.9",
"dockerode": "^4.0.10",
"dotenv": "^16.6.1",
"drizzle-orm": "^0.43.1",
"drizzle-zod": "^0.7.1",
"embla-carousel-react": "^8.6.0",
"express": "^5.2.1",
"googleapis": "^170.1.0",
"input-otp": "^1.4.2",
"lucide-react": "^0.553.0",
"minio": "^8.0.6",
"motion": "^12.34.0",
"next": "16.1.5",
"minio": "^8.0.7",
"motion": "^12.38.0",
"next": "16.2.1",
"next-safe-action": "^7.10.8",
"next-themes": "^0.4.6",
"node-cron": "^4.2.1",
"node-forge": "^1.3.3",
"node-forge": "^1.4.0",
"nodemailer": "^7.0.13",
"npm-check-updates": "^18.3.1",
"pg": "^8.18.0",
"pg": "^8.20.0",
"pino": "^10.3.1",
"pino-pretty": "^13.1.3",
"prettier": "^3.8.1",
"react": "^19.2.4",
"react-day-picker": "9.7.0",
"react-dom": "^19.2.4",
"react-dropzone": "^14.4.1",
"react-email": "^4.3.2",
"react-hook-form": "^7.71.1",
"react-hook-form": "^7.72.0",
"react-qr-code": "^2.0.18",
"react-resizable-panels": "^3.0.6",
"react-twc": "^1.5.1",
@@ -93,39 +96,43 @@
"socket.io": "^4.8.3",
"socket.io-client": "^4.8.3",
"sonner": "^2.0.7",
"swiper": "^12.1.0",
"tailwind-merge": "^3.4.0",
"swiper": "^12.1.3",
"tailwind-merge": "^3.5.0",
"uuid": "^11.1.0",
"vaul": "^1.1.2",
"ws": "^8.19.0",
"ws": "^8.20.0",
"zod": "^3.25.76"
},
"devDependencies": {
"@iconify/react": "^6.0.2",
"@playwright/test": "1.58.2",
"@react-email/preview-server": "4.3.2",
"@react-email/render": "^2.0.4",
"@tailwindcss/postcss": "^4.1.18",
"@release-it/bumper": "^7.0.5",
"@release-it/conventional-changelog": "^10.0.6",
"@tailwindcss/postcss": "^4.2.2",
"@types/eslint-plugin-tailwindcss": "^3.17.0",
"@types/node": "^22.19.11",
"@types/node": "^22.19.15",
"@types/node-forge": "^1.3.14",
"@types/pg": "^8.16.0",
"@types/pg": "^8.20.0",
"@types/react": "^19.2.14",
"@types/react-dom": "^19.2.3",
"@zenstackhq/openapi": "^2.22.1",
"@zenstackhq/tanstack-query": "^2.22.2",
"baseline-browser-mapping": "^2.9.19",
"drizzle-kit": "^0.31.9",
"esbuild": "^0.27.3",
"eslint": "^9.39.2",
"eslint-config-next": "^16.1.6",
"baseline-browser-mapping": "^2.10.12",
"drizzle-kit": "^0.31.10",
"esbuild": "^0.27.4",
"eslint": "^9.39.4",
"eslint-config-next": "^16.2.1",
"eslint-plugin-tailwindcss": "^3.18.2",
"framer-motion": "^12.34.0",
"postcss": "^8.5.6",
"tailwindcss": "^4.1.18",
"framer-motion": "^12.38.0",
"postcss": "^8.5.8",
"release-it": "^19.2.4",
"tailwindcss": "^4.2.2",
"tsx": "^4.21.0",
"tw-animate-css": "^1.4.0",
"typescript": "^5.9.3",
"zenstack": "2.14.2"
},
"packageManager": "pnpm@10.29.2+sha512.bef43fa759d91fd2da4b319a5a0d13ef7a45bb985a3d7342058470f9d2051a3ba8674e629672654686ef9443ad13a82da2beb9eeb3e0221c87b8154fff9d74b8"
"packageManager": "pnpm@10.33.0"
}
+79
View File
@@ -0,0 +1,79 @@
import { defineConfig, devices } from '@playwright/test';
/**
* Read environment variables from file.
* https://github.com/motdotla/dotenv
*/
// import dotenv from 'dotenv';
// import path from 'path';
// dotenv.config({ path: path.resolve(__dirname, '.env') });
/**
* See https://playwright.dev/docs/test-configuration.
*/
export default defineConfig({
testDir: './e2e',
/* Run tests in files in parallel */
fullyParallel: true,
/* Fail the build on CI if you accidentally left test.only in the source code. */
forbidOnly: !!process.env.CI,
/* Retry on CI only */
retries: process.env.CI ? 2 : 0,
/* Opt out of parallel tests on CI. */
workers: process.env.CI ? 1 : undefined,
/* Reporter to use. See https://playwright.dev/docs/test-reporters */
reporter: 'html',
/* Shared settings for all the projects below. See https://playwright.dev/docs/api/class-testoptions. */
use: {
/* Base URL to use in actions like `await page.goto('')`. */
baseURL: process.env.PROJECT_URL,
/* Collect trace when retrying the failed test. See https://playwright.dev/docs/trace-viewer */
trace: 'on-first-retry',
},
/* Configure projects for major browsers */
projects: [
{
name: 'chromium',
use: { ...devices['Desktop Chrome'] },
},
{
name: 'firefox',
use: { ...devices['Desktop Firefox'] },
},
{
name: 'webkit',
use: { ...devices['Desktop Safari'] },
},
/* Test against mobile viewports. */
{
name: 'Mobile Chrome',
use: { ...devices['Pixel 5'] },
},
{
name: 'Mobile Safari',
use: { ...devices['iPhone 12'] },
},
/* Test against branded browsers. */
// {
// name: 'Microsoft Edge',
// use: { ...devices['Desktop Edge'], channel: 'msedge' },
// },
// {
// name: 'Google Chrome',
// use: { ...devices['Desktop Chrome'], channel: 'chrome' },
// },
],
/* Run your local dev server before starting the tests */
// webServer: {
// command: 'docker compose up -d && make up',
// url: 'http://localhost:8887',
// reuseExistingServer: !process.env.CI,
// },
});
+3252 -1523
View File
File diff suppressed because it is too large Load Diff
+1 -45
View File
@@ -1,17 +1,5 @@
export interface AuthProviderConfig {
id: "google" | "github" | "credential";
isActive: boolean;
icon: string;
isManual?: boolean;
credentials?: {
clientId: string;
clientSecret: string;
};
}
export const PORTABASE_DEFAULT_SETTINGS = {
SECURITY: {
CSP: {
@@ -64,7 +52,7 @@ export const PORTABASE_DEFAULT_SETTINGS = {
FORM_ACTION: ["'self'"],
FRAME_ANCESTORS: ["'none'"],
BLOCK_ALL_MIXED_CONTENT: false,
UPGRADE_INSECURE_REQUESTS: true,
UPGRADE_INSECURE_REQUESTS: process.env.PROJECT_URL?.startsWith("https://") || false,
},
PERMISSIONS_POLICY: {
CAMERA: ["()"],
@@ -76,35 +64,3 @@ export const PORTABASE_DEFAULT_SETTINGS = {
},
};
export const SUPPORTED_PROVIDERS: AuthProviderConfig[] = [
{
id: "google",
icon: "hugeicons:chrome",
isActive: Boolean(process.env.AUTH_GOOGLE_METHOD),
// isManual: true,
credentials: {
clientId: process.env.AUTH_GOOGLE_ID || "",
clientSecret: process.env.AUTH_GOOGLE_SECRET || "",
},
},
{
id: "github",
icon: "iconoir:github",
isActive: Boolean(process.env.AUTH_GITHUB_METHOD),
credentials: {
clientId: process.env.AUTH_GITHUB_ID || "",
clientSecret: process.env.AUTH_GITHUB_SECRET || "",
},
},
{
id: "credential",
isActive: true,
icon: "proicons:key",
isManual: true,
credentials: {
clientId: "",
clientSecret: "",
},
},
];
+67 -61
View File
@@ -1,75 +1,81 @@
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 { NextRequest, NextResponse } from "next/server";
import { loggingMiddleware } from "@/middleware/loggingMiddleware";
import { errorHandler } from "@/middleware/errorHandler";
import { auth } from "@/lib/auth/auth";
import { headers } from "next/headers";
export async function proxy(request: NextRequest) {
const url = request.nextUrl.clone();
const redirectUrl = encodeURIComponent(request.nextUrl.pathname)
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) {
await auth.api.signOut({headers: await headers()});
return NextResponse.redirect(new URL("/login?error=banned", request.url));
}
if (session.user.role === "pending") {
await auth.api.signOut({headers: await headers()});
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();
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) {
await auth.api.signOut({ headers: await headers() });
return NextResponse.redirect(new URL("/login?error=banned", request.url));
}
if (session.user.role === "pending") {
await auth.api.signOut({ headers: await headers() });
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();
}
if (url.pathname.startsWith("/api/auth")) {
return NextResponse.next();
}
if (url.pathname.startsWith("/api/auth")) {
return NextResponse.next();
}
if (url.pathname.startsWith("/api")) {
const routeExists = checkRouteExists(url.pathname);
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);
if (url.pathname.startsWith("/api")) {
const routeExists = checkRouteExists(url.pathname);
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 checkRouteExists(pathname: string) {
const routePatterns = [
/^\/api\/agent\/[^/]+\/status\/?$/,
/^\/api\/agent\/[^/]+\/backup\/?$/,
/^\/api\/agent\/[^/]+\/backup\/upload\/init\/?$/,
/^\/api\/agent\/[^/]+\/backup\/upload\/status\/?$/,
/^\/api\/agent\/[^/]+\/restore\/?$/,
/^\/api\/files\/images\/[^/]+\/?$/,
/^\/api\/files\/backups\/?$/,
/^\/api\/tus\/hooks\/?$/,
/^\/api\/events\/?$/,
/^\/api\/config\/?$/,
/^\/api\/google\/drive\/callback\/?$/,
];
return routePatterns.some((pattern) => pattern.test(pathname));
const routePatterns = [
/^\/api\/agent\/[^/]+\/status\/?$/,
/^\/api\/agent\/[^/]+\/backup\/?$/,
/^\/api\/agent\/[^/]+\/backup\/upload\/init\/?$/,
/^\/api\/agent\/[^/]+\/backup\/upload\/status\/?$/,
/^\/api\/agent\/[^/]+\/restore\/?$/,
/^\/api\/files\/images\/[^/]+\/?$/,
/^\/api\/files\/backups\/?$/,
/^\/api\/tus\/hooks\/?$/,
/^\/api\/events\/?$/,
/^\/api\/config\/?$/,
/^\/api\/google\/drive\/callback\/?$/,
];
return routePatterns.some((pattern) => pattern.test(pathname));
}
export const config = {
matcher: [
"/api/:path*",
"/dashboard/:path*",
"/dashboard",
]
matcher: ["/api/:path*", "/dashboard/:path*", "/dashboard"],
};
Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

-95
View File
@@ -1,95 +0,0 @@
#!/bin/bash
set -e
if [ -z "$1" ]; then
echo "Usage: ./release <version>"
echo "Example: ./release v1.0.0"
exit 1
fi
VERSION=$1
CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD)
if [ "$CURRENT_BRANCH" = "main" ]; then
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Error: On 'main' branch, only release tags (X.Y.Z) are allowed."
exit 1
fi
else
if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+-rc\.[0-9]+$ ]]; then
echo "Error: On branch '$CURRENT_BRANCH', only RC tags matching X.Y.Z-rc.W are allowed (e.g., 1.0.0-rc.1)."
exit 1
fi
fi
CLEAN_VERSION=${VERSION#v}
CURRENT_DATE=$(date +%Y-%m-%d)
echo "Preparing release $VERSION..."
# package.json
if [ -f package.json ]; then
echo "Updating package.json..."
if sed --version >/dev/null 2>&1; then
sed -i "s/\"version\": \".*\"/\"version\": \"$CLEAN_VERSION\"/" package.json
else
sed -i '' "s/\"version\": \".*\"/\"version\": \"$CLEAN_VERSION\"/" package.json
fi
fi
# pyproject.toml
if [ -f pyproject.toml ]; then
echo "Updating pyproject.toml..."
if sed --version >/dev/null 2>&1; then
sed -i "s/^version = \".*\"/version = \"$CLEAN_VERSION\"/" pyproject.toml
else
sed -i '' "s/^version = \".*\"/version = \"$CLEAN_VERSION\"/" pyproject.toml
fi
fi
# Cargo.toml
if [ -f Cargo.toml ]; then
echo "Updating Cargo.toml..."
if sed --version >/dev/null 2>&1; then
sed -i "s/^version = \".*\"/version = \"$CLEAN_VERSION\"/" Cargo.toml
else
sed -i '' "s/^version = \".*\"/version = \"$CLEAN_VERSION\"/" Cargo.toml
fi
fi
# CITATION.cff
if [ -f CITATION.cff ]; then
echo "Updating CITATION.cff..."
if sed --version >/dev/null 2>&1; then
sed -i "s/^version: .*/version: $CLEAN_VERSION/" CITATION.cff
sed -i "s/^date-released: .*/date-released: \"$CURRENT_DATE\"/" CITATION.cff
else
sed -i '' "s/^version: .*/version: $CLEAN_VERSION/" CITATION.cff
sed -i '' "s/^date-released: .*/date-released: \"$CURRENT_DATE\"/" CITATION.cff
fi
fi
git add .
if ! git diff-index --quiet HEAD --; then
echo "Committing changes..."
git commit -m "chore(release): $VERSION"
else
echo "No changes to commit. Proceeding to tag..."
fi
if git rev-parse "$VERSION" >/dev/null 2>&1; then
echo "Tag $VERSION already exists. Aborting."
exit 1
fi
echo "Creating tag $VERSION..."
git tag -a "$VERSION" -m "Release $VERSION"
echo "Pushing changes and tags to remote..."
git push
git push origin "$VERSION"
echo "Successfully released $VERSION!"
File diff suppressed because it is too large Load Diff
Binary file not shown.
-6
View File
@@ -1,6 +0,0 @@
import express from "express";
import router from "../src/features/api/router";
export function mountApi(app: express.Express) {
app.use("/services/v1", router);
}
-52
View File
@@ -1,52 +0,0 @@
import next from "next";
import express from "express";
import {mountApi} from "./api";
import http from "http";
const port = Number(process.env.PORT) || 8887;
const dev = process.env.NODE_ENV !== "production";
async function start() {
const nextApp = next({
dev,
hostname: "0.0.0.0",
turbopack: dev,
port
});
const handle = nextApp.getRequestHandler();
await nextApp.prepare();
const app = express();
app.use((req, res, next) => {
req.setTimeout(0);
res.setTimeout(0);
next();
});
mountApi(app);
app.use((req, res, next) => {
if (req.path.startsWith('/services/v1')) return next();
handle(req, res).catch((err) => {
console.error('Next.js App Router error:', err);
res.status(500).send('Internal Server Error');
});
});
const server = http.createServer(app);
server.setTimeout(0);
server.headersTimeout = 0;
server.requestTimeout = 0;
server.keepAliveTimeout = 0;
server.listen(port, "0.0.0.0", () => {
console.log(`NEXT APP → http://localhost:${port}`);
console.log(`API → http://localhost:${port}/services/v1`);
});
}
start();
+10 -4
View File
@@ -6,7 +6,7 @@ import {env} from "@/env.mjs";
interface EmailCreateUserProps {
email: string;
password: string;
password?: string;
}
export const EmailCreateUser = ({email, password}: EmailCreateUserProps) => {
@@ -23,9 +23,15 @@ export const EmailCreateUser = ({email, password}: EmailCreateUserProps) => {
<strong>Email: </strong>{email}
</Text>
<Text className="text-[14px] text-black leading-[24px]">
<strong>Default password: </strong>{password}
</Text>
{password ? (
<Text className="text-[14px] text-black leading-[24px]">
<strong>Default password: </strong>{password}
</Text>
) : (
<Text className="text-[14px] text-black leading-[24px]">
You can log in using one of the single sign-on (SSO) providers configured by your administrator.
</Text>
)}
<Section className="mt-[32px] mb-[32px] text-center">
<Button
+19 -6
View File
@@ -208,13 +208,26 @@ type UseZodFormProps<Z extends ZodSchema> = Exclude<
schema: Z;
};
// const useZodForm = <Z extends ZodSchema>({
// schema,
// ...formProps
// }: UseZodFormProps<Z>) =>
// useForm({
// ...formProps,
// // @ts-ignore
// resolver: zodResolver(schema),
// });
const useZodForm = <Z extends ZodSchema>({
schema,
...formProps
}: UseZodFormProps<Z>) =>
useForm({
...formProps,
resolver: zodResolver(schema),
schema,
...formProps
}: UseZodFormProps<Z>): UseFormReturn<TypeOf<Z>> =>
useForm<TypeOf<Z>>({
...formProps,
// @ts-ignore
resolver: zodResolver(schema),
});
export {
@@ -1,121 +1,133 @@
"use client";
import {useEffect, useState} from "react";
import {useMutation} from "@tanstack/react-query";
import {toast} from "sonner";
import {FormControl, FormField, FormItem, FormLabel, FormMessage, useZodForm} from "@/components/ui/form";
import {Input} from "@/components/ui/input";
import {Form} from "@/components/ui/form";
import {LoginSchema, LoginType} from "@/components/wrappers/auth/login/login-form/login-form.schema";
import {signIn} from "@/lib/auth/auth-client";
import {ButtonWithLoading} from "@/components/wrappers/common/button/button-with-loading";
import { useMutation } from "@tanstack/react-query";
import Link from "next/link";
import {useRouter} from "next/navigation";
import {PasswordInput} from "@/components/ui/password-input";
import { useRouter, useSearchParams } from "next/navigation";
import { useEffect } from "react";
import { toast } from "sonner";
import {
Form,
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
useZodForm,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { PasswordInput } from "@/components/ui/password-input";
import {
LoginSchema,
type LoginType,
} from "@/components/wrappers/auth/login/login-form/login-form.schema";
import { ButtonWithLoading } from "@/components/wrappers/common/button/button-with-loading";
import { signIn } from "@/lib/auth/auth-client";
export type loginFormProps = {
defaultValues?: LoginType;
defaultValues?: LoginType;
isPasskeyEnabled?: boolean;
};
export const LoginForm = (props: loginFormProps) => {
const { isPasskeyEnabled = false } = props;
const url = useSearchParams();
const [urlParams, setUrlParams] = useState<URLSearchParams>();
const form = useZodForm({
schema: LoginSchema,
});
useEffect(() => {
const urlParams = new URLSearchParams(window.location.search);
setUrlParams(urlParams);
const error = urlParams.get("error");
if (error?.includes("pending")) {
toast.error("Your account is not active.");
urlParams.delete("error");
window.history.replaceState({}, document.title, window.location.pathname + "?" + urlParams.toString());
}
if (error?.includes("invalid_or_expired_token")) {
toast.error("Password reset invalid token.");
urlParams.delete("error");
window.history.replaceState({}, document.title, window.location.pathname + "?" + urlParams.toString());
}
}, []);
const router = useRouter();
const form = useZodForm({
schema: LoginSchema,
});
const router = useRouter();
const mutation = useMutation({
mutationFn: async (values: LoginType) => {
await signIn.email(
{
password: values.password,
email: values.email,
callbackURL: urlParams?.get("redirect") ?? "/dashboard",
},
{
onSuccess: (context) => {
if (context.data.twoFactorRedirect) {
//@ts-ignore
router.push("/guard?redirect=" + encodeURIComponent(context.data.callbackURL || "/dashboard"));
}
toast.success("Login success");
},
onError: (error) => {
toast.error(error.error.message);
},
}
);
const mutation = useMutation({
mutationFn: async (values: LoginType) => {
await signIn.email(
{
password: values.password,
email: values.email,
callbackURL: url?.get("redirect") ?? "/dashboard",
},
});
{
onSuccess: (context) => {
if (context.data.twoFactorRedirect) {
router.push(
"/guard?redirect=" +
encodeURIComponent(context.data.callbackURL || "/dashboard"),
);
}
return (
<Form
form={form}
className="flex flex-col gap-4 mb-1"
onSubmit={async (values) => {
await mutation.mutateAsync(values);
}}
>
<FormField
control={form.control}
name="email"
defaultValue=""
render={({field}) => (
<FormItem>
<FormLabel>Email Address</FormLabel>
<FormControl>
<Input autoComplete="email" autoFocus placeholder="example@portabase.io" {...field} />
</FormControl>
<FormMessage/>
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
defaultValue=""
render={({field}) => (
<FormItem>
<div className="flex items-center justify-between">
<FormLabel>Password</FormLabel>
<div className="text-center text-sm">
<Link href={"/forgot-password"} className="hover:underline ml-1">
Forgot your password ?
</Link>
</div>
</div>
<FormControl>
<PasswordInput autoComplete="current-password webauthn"
placeholder={"Enter your password"} {...field} />
</FormControl>
<FormMessage/>
</FormItem>
)}
/>
<ButtonWithLoading className="mt-2 h-11" isPending={mutation.isPending}>
Login
</ButtonWithLoading>
</Form>
);
toast.success("Login success");
},
onError: (error) => {
toast.error(error.error.message);
},
},
);
},
});
return (
<Form
form={form}
className="flex flex-col gap-4 mb-1"
onSubmit={async (values) => {
await mutation.mutateAsync(values);
}}
>
<FormField
control={form.control}
name="email"
defaultValue=""
render={({ field }) => (
<FormItem>
<FormLabel>Email Address</FormLabel>
<FormControl>
<Input
autoComplete="email"
autoFocus
placeholder="example@portabase.io"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
defaultValue=""
render={({ field }) => (
<FormItem>
<div className="flex items-center justify-between">
<FormLabel>Password</FormLabel>
<div className="text-center text-sm">
<Link
href={"/forgot-password"}
className="hover:underline ml-1"
>
Forgot your password ?
</Link>
</div>
</div>
<FormControl>
<PasswordInput
autoComplete={
isPasskeyEnabled
? "current-password webauthn"
: "current-password"
}
placeholder={"Enter your password"}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<ButtonWithLoading className="mt-2 h-11" isPending={mutation.isPending}>
Login
</ButtonWithLoading>
</Form>
);
};
@@ -1,156 +1,169 @@
"use client";
import {useRouter} from "next/navigation";
import {useMutation} from "@tanstack/react-query";
import {toast} from "sonner";
import {CardContent, CardHeader} from "@/components/ui/card";
import {Form} from "@/components/ui/form";
import {TooltipProvider, TooltipTrigger, Tooltip, TooltipContent} from "@/components/ui/tooltip";
import {RegisterSchema, RegisterType} from "@/components/wrappers/auth/register/register-form/register-form.schema";
import {signUp} from "@/lib/auth/auth-client";
import {useZodForm} from "@/components/ui/form";
import {CardAuth} from "@/features/layout/card-auth";
import {FormField} from "@/components/ui/form";
import {FormItem} from "@/components/ui/form";
import {FormLabel} from "@/components/ui/form";
import {FormControl} from "@/components/ui/form";
import {FormMessage} from "@/components/ui/form";
import {Input} from "@/components/ui/input";
import {PasswordInput} from "@/components/ui/password-input";
import {Button} from "@/components/ui/button";
import { useRouter } from "next/navigation";
import { useMutation } from "@tanstack/react-query";
import { toast } from "sonner";
import { CardContent, CardHeader } from "@/components/ui/card";
import { Form } from "@/components/ui/form";
import {
TooltipProvider,
TooltipTrigger,
Tooltip,
TooltipContent,
} from "@/components/ui/tooltip";
import {
RegisterSchema,
RegisterType,
} from "@/components/wrappers/auth/register/register-form/register-form.schema";
import { signUp } from "@/lib/auth/auth-client";
import { useZodForm } from "@/components/ui/form";
import { CardAuth } from "@/features/layout/card-auth";
import { FormField } from "@/components/ui/form";
import { FormItem } from "@/components/ui/form";
import { FormLabel } from "@/components/ui/form";
import { FormControl } from "@/components/ui/form";
import { FormMessage } from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { PasswordInput } from "@/components/ui/password-input";
import { Button } from "@/components/ui/button";
import Link from "next/link";
import {Info} from "lucide-react";
import { Info } from "lucide-react";
export type registerFormProps = {
defaultValues?: RegisterType;
defaultValues?: RegisterType;
};
export const RegisterForm = (props: registerFormProps) => {
const form = useZodForm({
schema: RegisterSchema,
});
const form = useZodForm({
schema: RegisterSchema,
});
const router = useRouter();
const mutation = useMutation({
mutationFn: async (values: RegisterType) => {
// @ts-ignore
await signUp.email(values, {
onSuccess: () => {
toast.success(`Account successfully created`);
router.refresh();
router.push(`/login`);
},
onError: (error) => {
toast.error(error.error.message);
},
});
const router = useRouter();
const mutation = useMutation({
mutationFn: async (values: RegisterType) => {
await signUp.email(values, {
onSuccess: () => {
toast.success(`Account successfully created`);
router.refresh();
router.push(`/login`);
},
});
onError: (error) => {
toast.error(error.error.message);
},
});
},
});
return (
<TooltipProvider>
<CardAuth>
<CardHeader>
<div className="grid gap-2 text-center mb-2">
<h1 className="text-3xl font-bold">Create an account</h1>
<p className="text-balance text-muted-foreground">Enter your informations below to register</p>
</div>
</CardHeader>
<CardContent>
<Form
form={form}
className="flex flex-col gap-4"
onSubmit={async (values) => {
await mutation.mutateAsync(values);
}}
>
<FormField
control={form.control}
name="name"
defaultValue=""
render={({field}) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="Your name" {...field} />
</FormControl>
<FormMessage/>
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
defaultValue=""
render={({field}) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="example@portabase.io" {...field} />
</FormControl>
<FormMessage/>
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
defaultValue=""
render={({field}) => (
<FormItem>
<FormLabel className="flex">
Password
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info className="ml-3" size="15"/>
</TooltipTrigger>
<TooltipContent>
<p>
Min. 8 characters, 1 uppercase (A-Z), 1 lowercase (a-z), 1
number (0-9), 1 special character (!, @,
etc.)
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</FormLabel>
<FormControl>
<PasswordInput placeholder="Your password" {...field} />
</FormControl>
<FormMessage/>
</FormItem>
)}
/>
<FormField
control={form.control}
name="confirmPassword"
defaultValue=""
render={({field}) => (
<FormItem>
<FormLabel>Password Confirmation</FormLabel>
<FormControl>
<PasswordInput placeholder="Conform your password" {...field} />
</FormControl>
<FormMessage/>
</FormItem>
)}
/>
<Button type="submit" className="h-11" disabled={mutation.isPending}>
Sign up
</Button>
<div className="mt-4 text-center text-sm">
Already have an account ?{" "}
<Link href="/login" className="underline">
Sign in
</Link>
</div>
</Form>
</CardContent>
</CardAuth>
</TooltipProvider>
);
return (
<TooltipProvider>
<CardAuth>
<CardHeader>
<div className="grid gap-2 text-center mb-2">
<h1 className="text-3xl font-bold">Create an account</h1>
<p className="text-balance text-muted-foreground">
Enter your informations below to register
</p>
</div>
</CardHeader>
<CardContent>
<Form
form={form}
className="flex flex-col gap-4"
onSubmit={async (values) => {
await mutation.mutateAsync(values);
}}
>
<FormField
control={form.control}
name="name"
defaultValue=""
render={({ field }) => (
<FormItem>
<FormLabel>Name</FormLabel>
<FormControl>
<Input placeholder="Your name" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="email"
defaultValue=""
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="example@portabase.io" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
defaultValue=""
render={({ field }) => (
<FormItem>
<FormLabel className="flex">
Password
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<Info size="15" />
</TooltipTrigger>
<TooltipContent>
<p>
Min. 8 characters, 1 uppercase (A-Z), 1 lowercase
(a-z), 1 number (0-9), 1 special character (!, @,
etc.)
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</FormLabel>
<FormControl>
<PasswordInput placeholder="Your password" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="confirmPassword"
defaultValue=""
render={({ field }) => (
<FormItem>
<FormLabel>Password Confirmation</FormLabel>
<FormControl>
<PasswordInput
placeholder="Conform your password"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<Button
type="submit"
className="h-11"
disabled={mutation.isPending}
>
Sign up
</Button>
<div className="mt-4 text-center text-sm">
Already have an account ?{" "}
<Link href="/login" className="underline">
Sign in
</Link>
</div>
</Form>
</CardContent>
</CardAuth>
</TooltipProvider>
);
};
+55 -35
View File
@@ -1,29 +1,54 @@
"use client";
import React, { useState } from "react";
import { useState } from "react";
import { Button } from "@/components/ui/button";
import { authClient } from "@/lib/auth/auth-client";
import { authClient, passkey } from "@/lib/auth/auth-client";
import { Loader2 } from "lucide-react";
import { toast } from "sonner";
import {AuthProviderConfig} from "../../../../portabase.config";
import {Icon} from "@iconify/react";
import { Icon } from "@iconify/react";
import { AuthProviderConfig } from "@/lib/auth/config";
import Image from "next/image";
import { useRouter } from "next/navigation";
export function SocialAuthButtons({ providers }: { providers: AuthProviderConfig[] }) {
const socialProviders = providers.filter(p => p.isActive && !p.isManual);
const socialProviders = providers.filter((p) => p.isActive && p.type !== "credential");
const router = useRouter();
const [isLoading, setIsLoading] = useState<string | null>(null);
const handleSocialSignIn = async (providerId: string) => {
setIsLoading(providerId);
const handleSocialSignIn = async (provider: AuthProviderConfig) => {
setIsLoading(provider.id);
try {
const { error } = await authClient.signIn.social({
provider: providerId as "google" | "github",
callbackURL: "/dashboard",
});
if (error) {
toast.error("An error occurred while signing in with the provider. Please try again.");
let result;
if (provider.id === "passkey") {
result = await authClient.signIn.passkey({
fetchOptions: {
onSuccess() {
router.push("/dashboard");
},
onError() {
toast.error("An error occurred during passkey authentication. Please try again.");
},
},
});
} else if (provider.type === "sso") {
result = await authClient.signIn.sso({
providerId: provider.id,
providerType: "oidc",
callbackURL: "/dashboard",
});
console.log(result);
} else {
result = await authClient.signIn.social({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
provider: provider.id as any,
callbackURL: "/dashboard",
});
}
if (result?.error) {
toast.error("An error occurred while signing in with the provider. Please try again.");
} else if (provider.id !== "passkey") {
toast.success("Redirecting to provider...");
}
} catch (err) {
@@ -38,29 +63,24 @@ export function SocialAuthButtons({ providers }: { providers: AuthProviderConfig
return (
<div className="flex flex-col gap-2 w-full">
{socialProviders.map((provider) => (
<Button key={provider.id} variant="outline" className="w-full gap-2" onClick={() => handleSocialSignIn(provider.id)} disabled={!!isLoading}>
{isLoading === provider.id ? <Loader2 className="h-4 w-4 animate-spin" /> :
<Icon icon={provider.icon} className="h-4 w-4"/>
}
<span>{PROVIDERS_TEXT[provider.id].title}</span>
<Button key={provider.id} variant="outline" className="w-full gap-2" onClick={() => handleSocialSignIn(provider)} disabled={!!isLoading}>
{isLoading === provider.id ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : provider.icon.startsWith("/") || provider.icon.startsWith("http") ? (
<Image
src={provider.icon}
alt={provider.id || "icon"}
width={16}
height={16}
className="h-4 w-4"
unoptimized={provider.icon.startsWith("http")}
/>
) : (
<Icon icon={provider.icon} className="h-4 w-4" />
)}
<span>{provider.title || provider.name}</span>
</Button>
))}
</div>
);
}
const PROVIDERS_TEXT = {
credential: {
title: "Password",
description: "Use your email address and password to sign in."
},
google: {
title: "Google",
description: "Sign in with your Google account."
},
github: {
title: "GitHub",
description: "Sign in with your GitHub account."
}
}
@@ -1,161 +1,198 @@
"use client"
import {Button, ButtonVariantsProps} from "@/components/ui/button";
import {ReactNode, useState} from "react";
import {Loader2} from "lucide-react";
import {Popover, PopoverContent, PopoverTrigger} from "@/components/ui/popover";
import {cn} from "@/lib/utils";
import {Tooltip, TooltipContent, TooltipProvider, TooltipTrigger} from "@/components/ui/tooltip";
"use client";
import { Button, ButtonVariantsProps } from "@/components/ui/button";
import { ReactNode, useState } from "react";
import { Loader2 } from "lucide-react";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import { cn } from "@/lib/utils";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
export type ButtonWithConfirmProps = {
title: string;
description: string;
button?: {
main: {
className?: string;
type?: "button" | "submit" | "reset" | undefined;
text?: string;
icon?: any;
variant?: ButtonVariantsProps["variant"];
size?: ButtonVariantsProps["size"];
disabled?: boolean;
tooltipText?: string;
};
confirm: {
className?: string;
text: string;
icon?: any;
variant?: ButtonVariantsProps["variant"];
size?: ButtonVariantsProps["size"];
onClick?: () => void;
};
cancel: {
className?: string;
text: string;
icon?: any;
variant?: ButtonVariantsProps["variant"];
size?: ButtonVariantsProps["size"];
onClick?: () => void;
};
title: string;
description: string;
button?: {
main: {
className?: string;
type?: "button" | "submit" | "reset" | undefined;
text?: string;
icon?: any;
variant?: ButtonVariantsProps["variant"];
size?: ButtonVariantsProps["size"];
disabled?: boolean;
tooltipText?: string;
};
children?: ReactNode;
onConfirm?: (e: React.MouseEvent) => void;
onCancel?: (e: React.MouseEvent) => void;
confirmButtonText?: string;
cancelButtonText?: string;
isPending?: boolean;
confirm: {
className?: string;
text: string;
icon?: any;
variant?: ButtonVariantsProps["variant"];
size?: ButtonVariantsProps["size"];
onClick?: () => void;
};
cancel: {
className?: string;
text: string;
icon?: any;
variant?: ButtonVariantsProps["variant"];
size?: ButtonVariantsProps["size"];
onClick?: () => void;
};
};
children?: ReactNode;
onConfirm?: (e: React.MouseEvent) => void;
onCancel?: (e: React.MouseEvent) => void;
confirmButtonText?: string;
cancelButtonText?: string;
isPending?: boolean;
};
export const ButtonWithConfirm = (props: ButtonWithConfirmProps) => {
const [isConfirming, setIsConfirming] = useState(false);
const [isConfirming, setIsConfirming] = useState(false);
const isLegacy = !!props.button;
const isDisabled = isLegacy ? !!props.button?.main.disabled : false;
const isLegacy = !!props.button;
const isDisabled = isLegacy ? !!props.button?.main.disabled : false;
const handleConfirm = (e: React.MouseEvent) => {
const handleConfirm = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
if (isLegacy) {
props.button?.confirm.onClick?.();
} else {
props.onConfirm?.(e);
}
setIsConfirming(false);
};
const handleCancel = (e: React.MouseEvent) => {
e.preventDefault();
e.stopPropagation();
if (isLegacy) {
props.button?.cancel.onClick?.();
} else {
props.onCancel?.(e);
}
setIsConfirming(false);
};
const triggerContent = isLegacy ? (
<Button
type={props.button?.main.type}
disabled={isDisabled}
variant={props.button?.main.variant ?? "default"}
size={props.button?.main.size ?? "default"}
className={props.button?.main.className}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
if (isLegacy) {
props.button?.confirm.onClick?.();
} else {
props.onConfirm?.(e);
}
setIsConfirming(false);
};
const handleCancel = (e: React.MouseEvent) => {
if (!isDisabled) setIsConfirming(true);
}}
>
{props.isPending && <Loader2 className="animate-spin mr-4" size={16} />}
{props.button?.main.icon}
{props.button?.main.text && <span>{props.button.main.text}</span>}
</Button>
) : (
<div
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
if (isLegacy) {
props.button?.cancel.onClick?.();
} else {
props.onCancel?.(e);
}
setIsConfirming(false);
};
setIsConfirming(true);
}}
>
{props.children}
</div>
);
const triggerContent = isLegacy ? (
<Button
type={props.button?.main.type}
disabled={isDisabled}
variant={props.button?.main.variant ?? "default"}
size={props.button?.main.size ?? "default"}
className={props.button?.main.className}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
if (!isDisabled) setIsConfirming(true);
}}
>
{props.isPending && <Loader2 className="animate-spin mr-4" size={16}/>}
{props.button?.main.icon}
{props.button?.main.text && <span>{props.button.main.text}</span>}
</Button>
const withTooltip =
isLegacy && props.button?.main.tooltipText && isDisabled ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div>{triggerContent}</div>
</TooltipTrigger>
<TooltipContent>
<p>{props.button?.main.tooltipText}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
) : (
<div onClick={(e) => {
e.preventDefault();
e.stopPropagation();
setIsConfirming(true);
}}>
{props.children}
</div>
triggerContent
);
const withTooltip = isLegacy && props.button?.main.tooltipText && isDisabled ? (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<div>{triggerContent}</div>
</TooltipTrigger>
<TooltipContent>
<p>{props.button?.main.tooltipText}</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
) : triggerContent;
return (
<Popover open={isConfirming} onOpenChange={setIsConfirming}>
<PopoverTrigger asChild>
{withTooltip}
</PopoverTrigger>
<PopoverContent
className="w-80"
onPointerDownOutside={(e) => e.preventDefault()}
onInteractOutside={(e) => e.preventDefault()}
onClick={(e) => e.stopPropagation()}
onMouseMove={(e) => e.stopPropagation()}
onMouseEnter={(e) => e.stopPropagation()}
onMouseLeave={(e) => e.stopPropagation()}
onOpenAutoFocus={(e) => e.preventDefault()}
return (
<Popover open={isConfirming} onOpenChange={setIsConfirming}>
<PopoverTrigger asChild>{withTooltip}</PopoverTrigger>
<PopoverContent
className="w-80"
onPointerDownOutside={(e) => e.preventDefault()}
onInteractOutside={(e) => e.preventDefault()}
onClick={(e) => e.stopPropagation()}
onMouseMove={(e) => e.stopPropagation()}
onMouseEnter={(e) => e.stopPropagation()}
onMouseLeave={(e) => e.stopPropagation()}
onOpenAutoFocus={(e) => e.preventDefault()}
>
<div className="grid gap-4">
<div className="space-y-2">
<h4 className="font-medium leading-none">{props.title}</h4>
<p className="text-sm text-muted-foreground">{props.description}</p>
</div>
<div className="grid gap-2">
<Button
onClick={handleConfirm}
variant={
isLegacy
? (props.button?.confirm.variant ?? "default")
: "default"
}
size={
isLegacy ? (props.button?.confirm.size ?? "default") : "default"
}
className={cn(
isLegacy ? props.button?.confirm.className : "",
"w-full",
)}
>
<div className="grid gap-4">
<div className="space-y-2">
<h4 className="font-medium leading-none">{props.title}</h4>
<p className="text-sm text-muted-foreground">{props.description}</p>
</div>
<div className="grid gap-2">
<Button
onClick={handleConfirm}
variant={isLegacy ? (props.button?.confirm.variant ?? "default") : "default"}
size={isLegacy ? (props.button?.main.size ?? "default") : "default"}
className={cn(isLegacy ? props.button?.main.className : "", "w-full")}
>
{props.isPending && <Loader2 className="animate-spin mr-4" size={16}/>}
{isLegacy && props.button?.confirm.icon}
<span>{isLegacy ? props.button?.confirm.text : (props.confirmButtonText ?? "Confirm")}</span>
</Button>
<Button
variant={isLegacy ? (props.button?.cancel.variant ?? "outline") : "outline"}
onClick={handleCancel}
className={cn(isLegacy ? props.button?.main.className : "", "w-full")}
size={isLegacy ? (props.button?.main.size ?? "default") : "default"}
>
{isLegacy ? (props.button?.cancel.text ?? "Cancel") : (props.cancelButtonText ?? "Cancel")}
</Button>
</div>
</div>
</PopoverContent>
</Popover>
);
};
{props.isPending && (
<Loader2 className="animate-spin mr-4" size={16} />
)}
{isLegacy && props.button?.confirm.icon}
<span>
{isLegacy
? props.button?.confirm.text
: (props.confirmButtonText ?? "Confirm")}
</span>
</Button>
<Button
variant={
isLegacy
? (props.button?.cancel.variant ?? "outline")
: "outline"
}
onClick={handleCancel}
className={cn(
isLegacy ? props.button?.cancel.className : "",
"w-full",
)}
size={
isLegacy ? (props.button?.cancel.size ?? "default") : "default"
}
>
{isLegacy
? (props.button?.cancel.text ?? "Cancel")
: (props.cancelButtonText ?? "Cancel")}
</Button>
</div>
</div>
</PopoverContent>
</Popover>
);
};
@@ -0,0 +1,94 @@
"use client";
import { useSearchParams } from "next/navigation";
import type React from "react";
import { useEffect } from "react";
import { toast } from "sonner";
export const ErrorLayout = ({ children }: { children: React.ReactNode }) => {
const url = useSearchParams();
useEffect(() => {
const error = url.get("error");
if (!error) return;
const errorMessages: Record<string, string> = {
pending: "Your account is not active.",
invalid_or_expired_token: "Password reset invalid token.",
account_already_linked_to_different_user:
"An account already exists with the same email address but different sign-in credentials. Sign in using a provider associated with this email address.",
USER_NOT_FOUND: "User not found",
FAILED_TO_CREATE_USER: "Failed to create user",
FAILED_TO_CREATE_SESSION: "Failed to create session",
FAILED_TO_UPDATE_USER: "Failed to update user",
FAILED_TO_GET_SESSION: "Failed to get session",
INVALID_PASSWORD: "Invalid password",
INVALID_EMAIL: "Invalid email",
INVALID_EMAIL_OR_PASSWORD: "Invalid email or password",
INVALID_USER: "Invalid user",
SOCIAL_ACCOUNT_ALREADY_LINKED: "Social account already linked",
PROVIDER_NOT_FOUND: "Provider not found",
INVALID_TOKEN: "Invalid token",
TOKEN_EXPIRED: "Token expired",
ID_TOKEN_NOT_SUPPORTED: "id_token not supported",
FAILED_TO_GET_USER_INFO: "Failed to get user info",
USER_EMAIL_NOT_FOUND: "User email not found",
EMAIL_NOT_VERIFIED: "Email not verified",
PASSWORD_TOO_SHORT: "Password too short",
PASSWORD_TOO_LONG: "Password too long",
USER_ALREADY_EXISTS: "User already exists.",
USER_ALREADY_EXISTS_USE_ANOTHER_EMAIL:
"User already exists. Use another email.",
EMAIL_CAN_NOT_BE_UPDATED: "Email can not be updated",
CREDENTIAL_ACCOUNT_NOT_FOUND: "Credential account not found",
SESSION_EXPIRED:
"Session expired. Re-authenticate to perform this action.",
FAILED_TO_UNLINK_LAST_ACCOUNT: "You can't unlink your last account",
ACCOUNT_NOT_FOUND: "Account not found",
USER_ALREADY_HAS_PASSWORD:
"User already has a password. Provide that to delete the account.",
CROSS_SITE_NAVIGATION_LOGIN_BLOCKED:
"Cross-site navigation login blocked. This request appears to be a CSRF attack.",
VERIFICATION_EMAIL_NOT_ENABLED: "Verification email isn't enabled",
EMAIL_ALREADY_VERIFIED: "Email is already verified",
EMAIL_MISMATCH: "Email mismatch",
SESSION_NOT_FRESH: "Session is not fresh",
LINKED_ACCOUNT_ALREADY_EXISTS: "Linked account already exists",
INVALID_ORIGIN: "Invalid origin",
INVALID_CALLBACK_URL: "Invalid callbackURL",
INVALID_REDIRECT_URL: "Invalid redirectURL",
INVALID_ERROR_CALLBACK_URL: "Invalid errorCallbackURL",
INVALID_NEW_USER_CALLBACK_URL: "Invalid newUserCallbackURL",
MISSING_OR_NULL_ORIGIN: "Missing or null Origin",
CALLBACK_URL_REQUIRED: "callbackURL is required",
FAILED_TO_CREATE_VERIFICATION: "Unable to create verification",
FIELD_NOT_ALLOWED: "Field not allowed to be set",
ASYNC_VALIDATION_NOT_SUPPORTED: "Async validation is not supported",
VALIDATION_ERROR: "Validation Error",
MISSING_FIELD: "Field is required",
METHOD_NOT_ALLOWED_DEFER_SESSION_REQUIRED:
"POST method requires deferSessionRefresh to be enabled in session config",
BODY_MUST_BE_AN_OBJECT: "Body must be an object",
PASSWORD_ALREADY_SET: "User already has a password set",
Sign_up_is_disabled: "Sign up is disabled by the administrator",
};
const match = Object.entries(errorMessages).find(([key]) =>
error.includes(key),
);
if (!match) return;
toast.error(match[1]);
const params = new URLSearchParams(url.toString());
params.delete("error");
const newUrl =
window.location.pathname +
(params.toString() ? `?${params.toString()}` : "");
window.history.replaceState({}, document.title, newUrl);
}, [url]);
return <>{children}</>;
};
@@ -1,26 +1,69 @@
import {z} from "zod";
import {SlackChannelConfigSchema} from "./providers/notifications/forms/slack.schema";
import {SmtpChannelConfigSchema} from "./providers/notifications/forms/smtp.schema";
import {DiscordChannelConfigSchema} from "./providers/notifications/forms/discord.schema";
import {TelegramChannelConfigSchema} from "./providers/notifications/forms/telegram.schema";
import {GotifyChannelConfigSchema} from "./providers/notifications/forms/gotify.schema";
import {NtfyChannelConfigSchema} from "./providers/notifications/forms/ntfy.schema";
import {WebhookChannelConfigSchema} from "./providers/notifications/forms/webhook.schema";
import {S3ChannelConfigSchema} from "./providers/storages/forms/s3.schema";
import {GoogleDriveChannelConfigSchema} from "./providers/storages/forms/google-drive.schema";
import {LocalChannelConfigSchema} from "./providers/storages/forms/local.schema";
export const ChannelFormSchema = z.object({
const BaseChannelFormSchema = z.object({
name: z
.string()
.min(5, "Name must be at least 5 characters long")
.max(40, "Name must be at most 40 characters long"),
config: z.record(z.union([z.string(), z.number(), z.boolean(), z.null(), z.undefined()])).optional(),
enabled: z.boolean().default(true),
});
export const NotificationChannelFormSchema = z.discriminatedUnion("provider", [
BaseChannelFormSchema.extend({
provider: z.literal("slack"),
config: SlackChannelConfigSchema,
}),
BaseChannelFormSchema.extend({
provider: z.literal("smtp"),
config: SmtpChannelConfigSchema,
}),
BaseChannelFormSchema.extend({
provider: z.literal("discord"),
config: DiscordChannelConfigSchema,
}),
BaseChannelFormSchema.extend({
provider: z.literal("telegram"),
config: TelegramChannelConfigSchema,
}),
BaseChannelFormSchema.extend({
provider: z.literal("gotify"),
config: GotifyChannelConfigSchema,
}),
BaseChannelFormSchema.extend({
provider: z.literal("ntfy"),
config: NtfyChannelConfigSchema,
}),
BaseChannelFormSchema.extend({
provider: z.literal("webhook"),
config: WebhookChannelConfigSchema,
}),
]);
export const NotificationChannelFormSchema = ChannelFormSchema.extend({
provider: z.enum(
["slack", "smtp", "discord", "telegram", "gotify", "ntfy", "webhook"],
{required_error: "Provider is required"}
),
});
export const StorageChannelFormSchema = ChannelFormSchema.extend({
provider: z.enum(["local", "s3", "google-drive"], {required_error: "Provider is required"}),
});
export const StorageChannelFormSchema = z.discriminatedUnion("provider", [
BaseChannelFormSchema.extend({
provider: z.literal("s3"),
config: S3ChannelConfigSchema,
}),
BaseChannelFormSchema.extend({
provider: z.literal("google-drive"),
config: GoogleDriveChannelConfigSchema,
}),
BaseChannelFormSchema.extend({
provider: z.literal("local"),
config: LocalChannelConfigSchema
})
]);
export type NotificationChannelFormType = z.infer<typeof NotificationChannelFormSchema>;
@@ -224,7 +224,7 @@ export const ChannelForm = ({onSuccessAction, organization, defaultValues, kind}
<div className="flex justify-between mt-4">
<div>
{kind == "storage" && (
{(!isCreate || kind == "storage") && (
<ChannelTestButton
kind={kind}
organizationId={organization?.id}
@@ -34,7 +34,7 @@ export const ChannelTestButton = ({channel, organizationId, kind}: NotifierTestC
message: `We are testing channel ${channel.name}`,
level: 'info',
};
const result = await dispatchNotification(payload, undefined, channel, organizationId);
const result = await dispatchNotification(payload, undefined, channel.id, organizationId);
if (result.success) {
toast.success(result.message);
@@ -51,7 +51,8 @@ export const ChannelTestButton = ({channel, organizationId, kind}: NotifierTestC
toast.success("Successfully connected to storage channel");
} else {
console.error(result);
toast.error("An error occurred while testing the storage channel, check your configuration");
const responseText = result.response ? `(${result.response})` : "";
toast.error(`An error occurred while testing the storage channel, check your configuration ${responseText}`);
}
} else {
toast.error("Not yet supported");
@@ -1,30 +1,38 @@
import {UseFormReturn} from "react-hook-form";
import {FormControl, FormField, FormItem, FormLabel, FormMessage} from "@/components/ui/form";
import {Input} from "@/components/ui/input";
import {Separator} from "@/components/ui/separator";
import { UseFormReturn } from "react-hook-form";
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Separator } from "@/components/ui/separator";
import { PasswordInput } from "@/components/ui/password-input";
type NotifierDiscordFormProps = {
form: UseFormReturn<any, any, any>
}
form: UseFormReturn<any, any, any>;
};
export const NotifierDiscordForm = ({form}: NotifierDiscordFormProps) => {
return (
<>
<Separator className="my-1"/>
<FormField
control={form.control}
name="config.discordWebhook"
render={({field}) => (
<FormItem>
<FormLabel>Discord Webhook URL</FormLabel>
<FormControl>
<Input {...field} placeholder="https://discord.com/api/webhooks/..."/>
</FormControl>
<FormMessage/>
</FormItem>
)}
/>
</>
)
}
export const NotifierDiscordForm = ({ form }: NotifierDiscordFormProps) => {
return (
<>
<Separator className="my-1" />
<FormField
control={form.control}
name="config.discordWebhook"
render={({ field }) => (
<FormItem>
<FormLabel>Discord Webhook URL</FormLabel>
<FormControl>
<PasswordInput
{...field}
placeholder="https://discord.com/api/webhooks/..."
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</>
);
};
@@ -0,0 +1,5 @@
import {z} from "zod";
export const DiscordChannelConfigSchema = z.object({
discordWebhook: z.string().url("Must be a valid URL"),
});
@@ -1,43 +1,49 @@
import {UseFormReturn} from "react-hook-form";
import {FormControl, FormField, FormItem, FormLabel, FormMessage} from "@/components/ui/form";
import {Input} from "@/components/ui/input";
import {Separator} from "@/components/ui/separator";
import type { UseFormReturn } from "react-hook-form";
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Separator } from "@/components/ui/separator";
import { PasswordInput } from "@/components/ui/password-input";
type NotifierGotifyFormProps = {
form: UseFormReturn<any, any, any>
}
form: UseFormReturn<any, any, any>;
};
export const NotifierGotifyForm = ({form}: NotifierGotifyFormProps) => {
return (
<>
<Separator className="my-1"/>
<FormField
control={form.control}
name="config.gotifyServerUrl"
render={({field}) => (
<FormItem>
<FormLabel>Gotify Server URL</FormLabel>
<FormControl>
<Input {...field} placeholder="https://gotify.example.com"/>
</FormControl>
<FormMessage/>
</FormItem>
)}
/>
<FormField
control={form.control}
name="config.gotifyAppToken"
render={({field}) => (
<FormItem>
<FormLabel>Application Token</FormLabel>
<FormControl>
<Input {...field} placeholder="A1b2C3d4E5f6G7h"/>
</FormControl>
<FormMessage/>
</FormItem>
)}
/>
</>
)
}
export const NotifierGotifyForm = ({ form }: NotifierGotifyFormProps) => {
return (
<>
<Separator className="my-1" />
<FormField
control={form.control}
name="config.gotifyServerUrl"
render={({ field }) => (
<FormItem>
<FormLabel>Gotify Server URL</FormLabel>
<FormControl>
<Input {...field} placeholder="https://gotify.example.com" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="config.gotifyAppToken"
render={({ field }) => (
<FormItem>
<FormLabel>Application Token</FormLabel>
<FormControl>
<PasswordInput {...field} placeholder="A1b2C3d4E5f6G7h" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</>
);
};
@@ -0,0 +1,6 @@
import {z} from "zod";
export const GotifyChannelConfigSchema = z.object({
gotifyServerUrl: z.string().url("Must be a valid URL"),
gotifyAppToken: z.string().min(1, "App token is required"),
});
@@ -2,6 +2,7 @@ import {UseFormReturn} from "react-hook-form";
import {FormControl, FormField, FormItem, FormLabel, FormMessage} from "@/components/ui/form";
import {Input} from "@/components/ui/input";
import {Separator} from "@/components/ui/separator";
import {PasswordInput} from "@/components/ui/password-input";
type NotifierNtfyFormProps = {
form: UseFormReturn<any, any, any>
@@ -48,7 +49,7 @@ export const NotifierNtfyForm = ({form}: NotifierNtfyFormProps) => {
<FormItem>
<FormLabel>Access Token (Optional)</FormLabel>
<FormControl>
<Input {...field} placeholder="tk_..."/>
<PasswordInput {...field} placeholder="tk_..."/>
</FormControl>
<p className="text-xs text-muted-foreground">Only required for protected topics or self-hosted instances with auth.</p>
<FormMessage/>
@@ -79,7 +80,7 @@ export const NotifierNtfyForm = ({form}: NotifierNtfyFormProps) => {
<FormItem>
<FormLabel>Basic Auth Password (Optional)</FormLabel>
<FormControl>
<Input {...field} type="password" placeholder="password"/>
<PasswordInput placeholder="password" {...field} />
</FormControl>
<FormMessage/>
</FormItem>
@@ -0,0 +1,9 @@
import {z} from "zod";
export const NtfyChannelConfigSchema = z.object({
ntfyTopic: z.string().min(1, "Topic is required"),
ntfyServerUrl: z.string().url("Must be a valid URL").optional().or(z.literal('')),
ntfyToken: z.string().optional(),
ntfyUsername: z.string().optional(),
ntfyPassword: z.string().optional(),
});
@@ -1,31 +1,38 @@
import {UseFormReturn} from "react-hook-form";
import {FormControl, FormField, FormItem, FormLabel, FormMessage} from "@/components/ui/form";
import {Input} from "@/components/ui/input";
import {Separator} from "@/components/ui/separator";
import { UseFormReturn } from "react-hook-form";
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Separator } from "@/components/ui/separator";
import { PasswordInput } from "@/components/ui/password-input";
type NotifierSmtpFormProps = {
form: UseFormReturn<any, any, any>
}
form: UseFormReturn<any, any, any>;
};
export const NotifierSlackForm = ({form}: NotifierSmtpFormProps) => {
return(
<>
<Separator className="my-1"/>
<FormField
control={form.control}
name="config.slackWebhook"
render={({field}) => (
<FormItem>
<FormLabel>Slack Webhook URL</FormLabel>
<FormControl>
<Input {...field} placeholder="https://hooks.slack.com/services/..."/>
</FormControl>
<FormMessage/>
</FormItem>
)}
/>
</>
)
}
export const NotifierSlackForm = ({ form }: NotifierSmtpFormProps) => {
return (
<>
<Separator className="my-1" />
<FormField
control={form.control}
name="config.slackWebhook"
render={({ field }) => (
<FormItem>
<FormLabel>Slack Webhook URL</FormLabel>
<FormControl>
<PasswordInput
{...field}
placeholder="https://hooks.slack.com/services/..."
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</>
);
};
@@ -0,0 +1,5 @@
import {z} from "zod";
export const SlackChannelConfigSchema = z.object({
slackWebhook: z.string().url("Must be a valid URL"),
});
@@ -0,0 +1,10 @@
import {z} from "zod";
export const SmtpChannelConfigSchema = z.object({
host: z.string().min(1, "Host is required"),
port: z.coerce.number().min(1, "Port is required"),
user: z.string().min(1, "User is required"),
password: z.string().min(1, "Password is required"),
from: z.string().min(1, "From is required"),
to: z.string().min(1, "To is required"),
});
@@ -1,48 +1,84 @@
import {UseFormReturn} from "react-hook-form";
import {FormControl, FormField, FormItem, FormLabel, FormMessage} from "@/components/ui/form";
import {Input} from "@/components/ui/input";
import {Separator} from "@/components/ui/separator";
import { UseFormReturn } from "react-hook-form";
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Separator } from "@/components/ui/separator";
import { PasswordInput } from "@/components/ui/password-input";
type NotifierTelegramFormProps = {
form: UseFormReturn<any, any, any>
}
form: UseFormReturn<any, any, any>;
};
export const NotifierTelegramForm = ({form}: NotifierTelegramFormProps) => {
return (
<>
<Separator className="my-1"/>
<FormField
control={form.control}
name="config.telegramBotToken"
render={({field}) => (
<FormItem>
<FormLabel>Telegram Bot Token</FormLabel>
<FormControl>
<Input {...field} placeholder="123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"/>
</FormControl>
<FormMessage/>
</FormItem>
)}
/>
<FormField
control={form.control}
name="config.telegramChatId"
render={({field}) => (
<FormItem>
<FormLabel>Telegram Chat ID</FormLabel>
<FormControl>
<Input {...field} placeholder="-100123456789 or 123456789"/>
</FormControl>
<p className="text-xs text-muted-foreground">
You must start the conversation with the bot first (<strong>/start</strong>). <br/>
For groups, add the bot to the group. <br/>
You can use <a href="https://t.me/userinfobot" target="_blank" rel="noopener noreferrer" className="underline">@userinfobot</a> to find your ID.
</p>
<FormMessage/>
</FormItem>
)}
/>
</>
)
}
export const NotifierTelegramForm = ({ form }: NotifierTelegramFormProps) => {
return (
<>
<Separator className="my-1" />
<FormField
control={form.control}
name="config.telegramBotToken"
render={({ field }) => (
<FormItem>
<FormLabel>Telegram Bot Token</FormLabel>
<FormControl>
<PasswordInput
{...field}
placeholder="123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11"
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="config.telegramChatId"
render={({ field }) => (
<FormItem>
<FormLabel>Telegram Chat ID</FormLabel>
<FormControl>
<Input {...field} placeholder="-100123456789 or 123456789" />
</FormControl>
<p className="text-xs text-muted-foreground">
You must start the conversation with the bot first (
<strong>/start</strong>). <br />
For groups, add the bot to the group. <br />
You can use{" "}
<a
href="https://t.me/userinfobot"
target="_blank"
rel="noopener noreferrer"
className="underline"
>
@userinfobot
</a>{" "}
to find your ID.
</p>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="config.telegramTopicId"
render={({ field }) => (
<FormItem>
<FormLabel>Telegram Topic ID</FormLabel>
<FormControl>
<Input {...field} placeholder="123456" />
</FormControl>
<p className="text-xs text-muted-foreground">
Unique identifier for the target message thread (topic) of the
forum; for forum supergroups only
</p>
<FormMessage />
</FormItem>
)}
/>
</>
);
};
@@ -0,0 +1,20 @@
import { z } from "zod";
export const TelegramChannelConfigSchema = z
.object({
telegramBotToken: z.string().min(1, "Bot token is required"),
telegramChatId: z.string().min(1, "Chat ID is required"),
telegramTopicId: z.string().optional(),
})
.refine(
(data) => {
if (data.telegramTopicId && !data.telegramChatId) {
return false;
}
return true;
},
{
message: "Chat ID is required when a Topic ID is provided",
path: ["telegramChatId"],
},
);
@@ -1,65 +1,72 @@
import {UseFormReturn} from "react-hook-form";
import {FormControl, FormField, FormItem, FormLabel, FormMessage} from "@/components/ui/form";
import {Input} from "@/components/ui/input";
import {Separator} from "@/components/ui/separator";
import { UseFormReturn } from "react-hook-form";
import {
FormControl,
FormField,
FormItem,
FormLabel,
FormMessage,
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { Separator } from "@/components/ui/separator";
import { PasswordInput } from "@/components/ui/password-input";
type NotifierWebhookFormProps = {
form: UseFormReturn<any, any, any>
}
form: UseFormReturn<any, any, any>;
};
export const NotifierWebhookForm = ({form}: NotifierWebhookFormProps) => {
return (
<>
<Separator className="my-1"/>
<FormField
control={form.control}
name="config.webhookUrl"
render={({field}) => (
<FormItem>
<FormLabel>Webhook URL</FormLabel>
<FormControl>
<Input {...field} placeholder="https://example.com/api/webhook"/>
</FormControl>
<FormMessage/>
</FormItem>
)}
/>
<div className="flex gap-4">
<div className="flex-1">
<FormField
control={form.control}
name="config.webhookSecretHeader"
render={({field}) => (
<FormItem>
<FormLabel>Header Name (Optional)</FormLabel>
<FormControl>
<Input {...field} placeholder="X-Webhook-Secret"/>
</FormControl>
<FormMessage/>
</FormItem>
)}
/>
</div>
<div className="flex-1">
<FormField
control={form.control}
name="config.webhookSecret"
render={({field}) => (
<FormItem>
<FormLabel>Secret Value (Optional)</FormLabel>
<FormControl>
<Input {...field} placeholder="Secret value..."/>
</FormControl>
<FormMessage/>
</FormItem>
)}
/>
</div>
</div>
<p className="text-xs text-muted-foreground mt-2">
If provided, the secret will be sent in the specified header (defaults to <code>X-Webhook-Secret</code>).
</p>
</>
)
}
export const NotifierWebhookForm = ({ form }: NotifierWebhookFormProps) => {
return (
<>
<Separator className="my-1" />
<FormField
control={form.control}
name="config.webhookUrl"
render={({ field }) => (
<FormItem>
<FormLabel>Webhook URL</FormLabel>
<FormControl>
<Input {...field} placeholder="https://example.com/api/webhook" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<div className="flex gap-4">
<div className="flex-1">
<FormField
control={form.control}
name="config.webhookSecretHeader"
render={({ field }) => (
<FormItem>
<FormLabel>Header Name (Optional)</FormLabel>
<FormControl>
<Input {...field} placeholder="X-Webhook-Secret" />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
<div className="flex-1">
<FormField
control={form.control}
name="config.webhookSecret"
render={({ field }) => (
<FormItem>
<FormLabel>Secret Value (Optional)</FormLabel>
<FormControl>
<PasswordInput {...field} placeholder="Secret value..." />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
</div>
<p className="text-xs text-muted-foreground mt-2">
If provided, the secret will be sent in the specified header (defaults
to <code>X-Webhook-Secret</code>).
</p>
</>
);
};
@@ -0,0 +1,7 @@
import {z} from "zod";
export const WebhookChannelConfigSchema = z.object({
webhookUrl: z.string().url("Must be a valid URL"),
webhookSecretHeader: z.string().optional(),
webhookSecret: z.string().optional(),
});
@@ -0,0 +1,8 @@
import {z} from "zod";
export const GoogleDriveChannelConfigSchema = z.object({
clientId: z.string().min(1, "Client ID is required"),
clientSecret: z.string().min(1, "Client Secret is required"),
folderId: z.string().min(1, "Folder ID is required"),
refreshToken: z.string().optional(),
});

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