Compare commits

..
479 Commits
Author SHA1 Message Date
github-actions[bot] 40d3228db6 chore: release 1.20.2 2026-06-25 12:57:03 +00:00
Charles GTEandGitHub 383564f988 Merge pull request #335 from Portabase/fix/backup-size-restoration
fix: size in restoration agent helper
2026-06-25 14:56:06 +02:00
Charles GTE fb2f7791fd fix: size in restoration agent helper 2026-06-25 14:42:42 +02:00
github-actions[bot] 80a3f8642a chore: release 1.20.1 2026-06-24 06:47:12 +00:00
Charles GTEandGitHub 18bdbafa75 Merge pull request #333 from Portabase/fix/retention-failed-backups
fix: clean up failed and ongoing backups
2026-06-24 08:46:22 +02:00
charles-gauthereau e31245b492 fix: clean up failed and ongoing backups 2026-06-24 08:32:40 +02:00
github-actions[bot] c7d10efefe chore: release 1.20.0 2026-06-21 14:40:11 +00:00
Charles GTEandGitHub 408d6d76ef Merge pull request #327 from Portabase/dev
Dev
2026-06-21 16:39:06 +02:00
Charles GTEandGitHub bf0ed69b60 Merge pull request #328 from jjingi/feature/Integrate-Azure-Blob
Feature/integrate azure blob
2026-06-21 15:49:52 +02:00
Charles GTE afd9a8eff2 chore: refactoring 2026-06-21 15:47:23 +02:00
Charles GTE baab060628 fix: azurite and refactoring 2026-06-20 19:35:40 +02:00
Charles GTE 3018515abf Merge branch 'dev' into feature/Integrate-Azure-Blob
# Conflicts:
#	src/db/migrations/meta/0063_snapshot.json
#	src/db/migrations/meta/_journal.json
2026-06-20 14:56:41 +02:00
Charles GTE 402d3f3d9d fix: README.md 2026-06-20 09:59:30 +02:00
Charles GTE e193a300b1 fix: README.md 2026-06-20 09:45:02 +02:00
Charles GTE 0ed3be16f5 fix: README.md 2026-06-20 09:23:31 +02:00
jingi c34d8c5532 fix: coderabbit review fixed(typo, url validation, and unique ping key 2026-06-19 14:45:24 -07:00
Charles GTE aaa0c1d9a3 fix: README.md 2026-06-19 23:13:34 +02:00
jingi 1dfd0c2e56 fix: changed lowercase blob provder to uppercase to match StoragePrivderKind 2026-06-19 14:06:15 -07:00
jingi 63a94143e2 chore: regenrate pnpm-lock.yaml after rebase 2026-06-19 13:54:44 -07:00
jingi 243d724fb2 feat: add blob to db migration and register in channel helpers 2026-06-19 13:53:58 -07:00
jingi d6235adccd fix: add blob to db schema 2026-06-19 13:51:12 -07:00
jingi ae0f9a770d fix: add form for azure blob 2026-06-19 13:51:12 -07:00
jingi 305d55d972 feat: add Blobl.form.tsx file 2026-06-19 13:51:12 -07:00
jingi aa14367f65 feat: add blob variant to SotrageChannelFormSchema 2026-06-19 13:51:12 -07:00
jingi 8f797df033 feat: add auzre blob on index.ts handler 2026-06-19 13:51:12 -07:00
jingi d680eeb40e fix: rename blob.schema.ts to Blob.schema.ts 2026-06-19 13:51:12 -07:00
jingi 6ede27667f feat: implement Azure Blob storage (getBlobClient, upload, get, delete, ping, copy) 2026-06-19 13:51:12 -07:00
jingi 187d100230 feat: add Zod schema for Azure Blob storage config 2026-06-19 13:51:12 -07:00
jingi 4f8df0cef6 feat: add azure storage dependency 2026-06-19 13:50:19 -07:00
jingi 76a7c2ce43 feat: add blob to storage types 2026-06-19 13:48:42 -07:00
Charles GTE 4911e49c6b fix: README.md 2026-06-19 22:45:04 +02:00
Charles GTE f2f5f767b4 fix: README.md 2026-06-19 22:22:20 +02:00
Charles GTEandGitHub f209989f11 Merge pull request #325 from pcrosthwaite/feature/pushover-notifications
Feature/pushover notifications
2026-06-19 19:22:33 +02:00
charles-gauthereau d651568921 fix: schema and form select 2026-06-19 19:20:37 +02:00
Peter Crosthwaite 27348a3341 Updated to use AbortController pattern 2026-06-19 22:04:03 +10:00
Peter Crosthwaite ceba848bdd pushover request fixes
Implemented truncation on data elements to comply with pushover restrictions.
Added a timeout for a request to the pushover api endpoint
2026-06-19 21:22:07 +10:00
Peter Crosthwaite 1a36b8b6ef Merge main into feature/pushover-notifications, add Microsoft Teams provider
Both branches added a migration at index 0062. Resolved by keeping
upstream's teams migration at 0062 and promoting pushover to 0063.
Both providers are now included in the enum, types, handlers, and forms.
2026-06-19 18:27:45 +10:00
github-actions[bot] 61bc4a286d chore: release 1.19.0 2026-06-19 08:01:10 +00:00
Charles GTEandGitHub f5b3ad7381 Merge pull request #324 from Portabase/dev
Dev
2026-06-19 10:00:13 +02:00
Peter Crosthwaite a96c739d90 Replace manual pushover migration with drizzle-generated version 2026-06-19 17:49:50 +10:00
Peter Crosthwaite aaeaeabc81 Removed duplicated migration 2026-06-19 17:44:42 +10:00
charles-gauthereau 0cf7c45488 fix: README.md and removed the e2E folder as it is in a dedicated project now 2026-06-19 09:44:41 +02:00
Peter Crosthwaite db6eb221d8 Updated failed response to display in activity log 2026-06-19 17:24:44 +10:00
MalilavGitHubkillian-larcherCharles GTEgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>github-actions[bot] <github-actions[bot]@users.noreply.github.com>Michael KutýKillian Larchergithub-actions[bot] <github-actions[bot]@users.noreply.github.com>Shawn M
130c23dd5b feat: Add Microsoft Teams as notification provider (issue 260) (#313)
* feat: add Microsoft Teams notification provider

* feat: Add Microsoft Teams as notification provider

* fix: Update database migration

* fix: Remove duplicate file 0058 db migration

* add Microsoft Teams notification provider E2E tests

* fix: externalize e2e testings and profile name trim (#318)

* fix: externalize e2e testings

* fix: externalize e2e tests.

* refactor: improve e2e workflow.

* chore: upgrade workflow action version.

* fix: createEnv by adding emptyStringAsUndefined (#317)

* chore: release 1.18.2

---------

Co-authored-by: killian-larcher <killian.larcher@soluce-technologies.com>
Co-authored-by: Charles GTE <charles.gauthereau@soluce-technologies.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* fix(layout): guard undefined user.name in avatar fallbacks (#320)

* fix(layout): guard undefined user.name in avatar fallbacks

* fix: AUTH_DEFAULT_USER as zod email

---------

Co-authored-by: charles-gauthereau <charles.gauthereau@soluce-technologies.com>

* fix: profile name trim

---------

Co-authored-by: killian-larcher <killian.larcher@soluce-technologies.com>
Co-authored-by: Charles GTE <charles.gauthereau@soluce-technologies.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Michael Kutý <6du1ro.n@gmail.com>
Co-authored-by: charles-gauthereau <charles.gauthereau@soluce-technologies.com>

* chore: release 1.18.3

* chore: Update README.md (#323)

* chore: Update README.md

added unraid link

* chore: Update README.md

fix markdown

* chore: release 1.18.4

* fix: teams.ts with timeout

---------

Co-authored-by: Killian Larcher <98161034+KillianLarcher@users.noreply.github.com>
Co-authored-by: killian-larcher <killian.larcher@soluce-technologies.com>
Co-authored-by: Charles GTE <charles.gauthereau@soluce-technologies.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Michael Kutý <6du1ro.n@gmail.com>
Co-authored-by: charles-gauthereau <charles.gauthereau@soluce-technologies.com>
Co-authored-by: Shawn M <84647313+hyperion02@users.noreply.github.com>
2026-06-19 07:42:50 +02:00
Peter Crosthwaite 6075872881 Added support for pushover notifications 2026-06-19 15:37:05 +10:00
github-actions[bot] 7b65d5648f chore: release 1.18.4 2026-06-16 19:12:17 +00:00
Shawn MandGitHub eb4a31e5f4 chore: Update README.md (#323)
* chore: Update README.md

added unraid link

* chore: Update README.md

fix markdown
2026-06-16 21:11:25 +02:00
github-actions[bot] 0d2f2957b3 chore: release 1.18.3 2026-06-11 13:53:57 +00:00
Killian LarcherGitHubkillian-larcherCharles GTEgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>charles-gauthereaugithub-actions[bot] <github-actions[bot]@users.noreply.github.com>Michael Kutý
51bf9cf679 fix: externalize e2e testings and profile name trim (#318)
* fix: externalize e2e testings

* fix: externalize e2e tests.

* refactor: improve e2e workflow.

* chore: upgrade workflow action version.

* fix: createEnv by adding emptyStringAsUndefined (#317)

* chore: release 1.18.2

---------

Co-authored-by: killian-larcher <killian.larcher@soluce-technologies.com>
Co-authored-by: Charles GTE <charlesgte31@gmail.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>

* fix(layout): guard undefined user.name in avatar fallbacks (#320)

* fix(layout): guard undefined user.name in avatar fallbacks

* fix: AUTH_DEFAULT_USER as zod email

---------

Co-authored-by: charles-gauthereau <charles.gauthereau@soluce-technologies.com>

* fix: profile name trim

---------

Co-authored-by: killian-larcher <killian.larcher@soluce-technologies.com>
Co-authored-by: Charles GTE <charlesgte31@gmail.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
Co-authored-by: Michael Kutý <6du1ro.n@gmail.com>
Co-authored-by: charles-gauthereau <charles.gauthereau@soluce-technologies.com>
2026-06-11 15:53:07 +02:00
charles-gauthereau 777a8bba3c fix: profile name trim 2026-06-11 15:28:48 +02:00
09eba5c0e8 fix(layout): guard undefined user.name in avatar fallbacks (#320)
* fix(layout): guard undefined user.name in avatar fallbacks

* fix: AUTH_DEFAULT_USER as zod email

---------

Co-authored-by: charles-gauthereau <charles.gauthereau@soluce-technologies.com>
2026-06-11 15:20:06 +02:00
killian-larcher b9924f511f Merge branch 'main' into dev 2026-06-10 18:21:21 +02:00
Killian LarcherGitHubkillian-larcherCharles GTEgithub-actions[bot] <github-actions[bot]@users.noreply.github.com>
32da8b5fd0 feat: externalize e2e testings [skip-release] (#312)
* feat: externalize e2e tests.

* refactor: improve e2e workflow.

* chore: upgrade workflow action version.

* fix: createEnv by adding emptyStringAsUndefined (#317)

* chore: release 1.18.2

---------

Co-authored-by: killian-larcher <killian.larcher@optimusai.fr>
Co-authored-by: Charles GTE <charlesgte31@gmail.com>
Co-authored-by: github-actions[bot] <github-actions[bot]@users.noreply.github.com>
2026-06-10 18:08:58 +02:00
github-actions[bot] 87f8a50326 chore: release 1.18.2 2026-06-10 15:50:59 +00:00
Charles GTEandGitHub b70a8c8a16 fix: createEnv by adding emptyStringAsUndefined (#317) 2026-06-10 17:50:04 +02:00
github-actions[bot] ebdef2d302 chore: release 1.18.1 2026-06-09 18:07:45 +00:00
Charles GTEandGitHub a6bb50c5e2 Merge pull request #314 from Portabase/dev
Dev
2026-06-09 19:55:38 +02:00
Charles GTEandGitHub 50eb48fcf9 Merge pull request #311 from GaboritJunior/fix/tusd-behind-proxy
fix: Enable --behind-proxy for tusd
2026-06-09 19:44:03 +02:00
Charles GTE 6622565b14 fix: echo log in app-prod-entrypoint.sh and TUSD_BEHIND_PROXY in .env.example 2026-06-09 19:42:24 +02:00
Junior GABORITGitHubcoderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
f001de1e97 Update docker/nginx/nginx.conf
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>
2026-06-08 11:38:04 +02:00
Junior GABORIT 6711fab413 [docker/nginx/nginx.conf] Apply coderabbitai change 2026-06-08 11:22:20 +02:00
Junior GABORIT e2f28a84bc [K8S] Add TUSD_BEHIND_PROXY env var 2026-06-08 10:08:41 +02:00
Junior GABORIT d1941c2f3d [docker/entrypoints] Update tusd lauch command 2026-06-08 10:07:14 +02:00
Junior GABORIT f35e6abb05 [docker/Dockerfile] Add TUSD_BEHIND_PROXY env var 2026-06-08 10:05:48 +02:00
Junior GABORIT 0598f78a34 [docker/nginx/nginx.conf] Update X-Forwarded-Proto value 2026-06-08 10:05:18 +02:00
github-actions[bot] f30b55ba47 chore: release 1.18.0 2026-06-07 15:42:38 +00:00
Charles GTEandGitHub 751f29e340 Merge pull request #309 from Portabase/feat/backup-restore-logs
feat: backup-restore-logs
2026-06-07 17:41:43 +02:00
charles-gauthereau 2e629f8423 fix: refactoring and ordering data on agent and project page 2026-06-07 17:33:47 +02:00
Charles GTE ae431f5b85 fix: pnpm 2026-06-07 11:22:22 +02:00
Charles GTE f5f697ceb0 fix: refactoring 2026-06-07 11:18:45 +02:00
Charles GTE 02a8f0baf9 fix: also add for restoration 2026-06-07 11:15:21 +02:00
Charles GTE 2932149b92 feat: backup and restore logs modal view 2026-06-06 22:52:21 +02:00
Charles GTE e1e4d67114 feat: backup and restore logs modal view 2026-06-06 22:46:36 +02:00
Charles GTE 1e3f112d9c Merge branch 'main' into dev 2026-06-05 21:32:28 +02:00
github-actions[bot] e3600949b0 chore: release 1.17.4 2026-06-04 07:09:53 +00:00
Charles GTEandGitHub 856c80cc20 fix: release.yml (#307) 2026-06-04 09:09:06 +02:00
Charles GTE 8c77796f91 Merge branch 'main' into dev 2026-06-04 08:58:07 +02:00
github-actions[bot] dbeaaa4bac chore: release 1.17.3 2026-06-04 06:44:52 +00:00
charles-gauthereau f9986e1b7f Merge branch 'main' into dev 2026-06-02 21:21:06 +02:00
github-actions[bot] 895008ba1d chore: release 1.17.2 2026-06-02 16:37:29 +00:00
Charles GTEandGitHub 09791ae86e fix: demo-banner (#305) 2026-06-02 18:36:35 +02:00
github-actions[bot] 564dee512f chore: release 1.17.1 2026-06-02 07:18:07 +00:00
Charles GTEandGitHub 56a9922057 fix: refactoring the api keys section and add documentation link. (#304) 2026-06-02 09:17:21 +02:00
github-actions[bot] 44f0112620 chore: release 1.17.0 2026-06-01 21:07:33 +00:00
Charles GTEandGitHub 2d7a5ce264 feat: add mcp server (#303)
* feat: added the mcp server

* feat: added the mcp server

* fix: api for database not linked to any projects

* fix: added variable to disable MCP

* chore: refactoring
2026-06-01 23:06:21 +02:00
charles-gauthereau 48f8d00d2e Merge branch 'main' into dev 2026-05-29 18:29:03 +02:00
github-actions[bot] 7afcf0f76b chore: release 1.16.2 2026-05-29 06:23:23 +00:00
a7b28ce84d fix: Discord Notification (#302)
Co-authored-by: charles-gauthereau <charles.gauthereau@soluce-technologies.com>
2026-05-29 08:22:41 +02:00
github-actions[bot] 4ef82ac3a8 chore: release 1.16.1 2026-05-29 06:07:05 +00:00
f86c826d1b chore: env variables (#301)
Co-authored-by: charles-gauthereau <charles.gauthereau@soluce-technologies.com>
2026-05-29 08:06:17 +02:00
github-actions[bot] 3de7ef1a26 chore: release 1.16.0 2026-05-29 05:39:20 +00:00
Charles GTEandGitHub 1713780528 fieat: Merge pull request #300 from Portabase/feat/rest-api
feat: rest-api
2026-05-29 07:38:18 +02:00
charles-gauthereau 6db39b829e fix: refactoring 2026-05-28 22:35:09 +02:00
charles-gauthereau 174ff0886c fix: added OPENAPI_ENABLED & API_ENABLED variables 2026-05-28 22:08:49 +02:00
charles-gauthereau d49d8f52c4 fix: hydration errors 2026-05-28 20:50:05 +02:00
charles-gauthereau c9552e448a fix: refactoring 2026-05-28 20:44:27 +02:00
charles-gauthereau 47d3059d78 fix: some bugs 2026-05-28 20:12:59 +02:00
charles-gauthereau c3bafef6d8 fix: openapi schemas 2026-05-28 19:05:57 +02:00
charles-gauthereau 1fbbc935c1 fix: add cdn.jsdelivr.net to style-src CSP for Swagger UI 2026-05-28 11:20:04 +02:00
charles-gauthereau ad0e6be10d fix: pin swagger-ui CDN assets with SRI and add document-level security default 2026-05-28 11:15:04 +02:00
charles-gauthereau 5735d47889 feat: serve Swagger UI at GET /api/v1/docs 2026-05-28 11:00:47 +02:00
charles-gauthereau 294bdfa296 feat: serve OpenAPI spec JSON at GET /api/v1/openapi 2026-05-28 11:00:04 +02:00
charles-gauthereau 83eb012cb5 feat: add buildSpec() to assemble /api/v1 OpenAPI document 2026-05-28 10:59:25 +02:00
charles-gauthereau 51d165fe84 feat: register database routes in OpenAPI spec 2026-05-28 10:58:26 +02:00
charles-gauthereau 3b7eb6c606 feat: register agent routes in OpenAPI spec 2026-05-28 10:56:29 +02:00
charles-gauthereau 333146c590 feat: register apiKeyAuth security scheme for OpenAPI spec 2026-05-28 10:55:17 +02:00
charles-gauthereau 6adb310f0c feat: add zod-to-openapi bootstrap for /api/v1 OpenAPI spec 2026-05-28 10:53:39 +02:00
charles-gauthereau a64fe1c08c chore: add @asteasolutions/zod-to-openapi for OpenAPI spec generation 2026-05-28 10:51:58 +02:00
charles-gauthereau 40cdb3d0ab fix: some security issues 2026-05-28 10:18:28 +02:00
charles-gauthereau 7a97721abf fix: agent route to add json parser 2026-05-28 08:16:34 +02:00
charles-gauthereau c1b71e7772 fix: endpoints 2026-05-27 22:02:22 +02:00
charles-gauthereau 56517226b6 fix: endpoints 2026-05-25 18:31:00 +02:00
charles-gauthereau 337d270ba6 fix: correct rate-limit response shape in withApiKey middleware 2026-05-25 16:17:18 +02:00
charles-gauthereau 373ab315a3 feat: register api/v1 routes in proxy route guard 2026-05-25 15:36:33 +02:00
charles-gauthereau 685e1fc300 fix: apply soft-delete filters consistently across api/v1 routes
Four missed cases where deleted/archived resources could leak through:
- acl.ts: exclude deleted databases from getAccessibleDatabaseIds
- databases/route.ts: exclude deleted databases in list endpoint
- agents/route.ts: re-apply isArchived guard on final fetch (ACL ids stale)
- databases/[id]/status/route.ts: exclude deleted restorations
2026-05-25 15:29:33 +02:00
charles-gauthereau 274aded8fd fix(api-v1): add soft-delete filters and remove any types in database operation routes 2026-05-25 15:18:55 +02:00
charles-gauthereau 39b6b5e116 feat(api-v1): add GET /databases/:id/status 2026-05-25 15:14:41 +02:00
charles-gauthereau 3f52d8c76e feat(api-v1): add POST /databases/:id/restore 2026-05-25 15:14:39 +02:00
charles-gauthereau 11f814777b feat(api-v1): add GET /databases/:id/backup/:backupId 2026-05-25 15:14:38 +02:00
charles-gauthereau dd66ecdb78 feat(api-v1): add GET and POST /databases/:id/backup 2026-05-25 15:14:36 +02:00
charles-gauthereau ec90a34262 fix(api-v1): return 404 for archived agents in resolveAgentAccess 2026-05-25 15:12:45 +02:00
charles-gauthereau 5a2ce4968d feat(api-v1): add GET /databases/:id 2026-05-25 15:09:44 +02:00
charles-gauthereau 03659b4a15 feat(api-v1): add GET /databases 2026-05-25 15:09:32 +02:00
charles-gauthereau b032c96a7c feat(api-v1): add GET /agents/:id and DELETE /agents/:id 2026-05-25 15:09:26 +02:00
charles-gauthereau d887d44bd4 fix(api-v1): transaction safety and isArchived consistency in agents route 2026-05-25 15:08:11 +02:00
charles-gauthereau 7115ce8484 feat(api-v1): add GET /agents and POST /agents 2026-05-25 15:03:21 +02:00
charles-gauthereau 96cc9822d0 fix(api-v1): handle agent.organizationId FK path and nullable isArchived in ACL helpers 2026-05-25 15:01:25 +02:00
charles-gauthereau 68fb74f441 feat(api-v1): add ACL helpers 2026-05-25 14:57:20 +02:00
charles-gauthereau 06ed4f3517 fix(api-v1): strengthen middleware type safety and remove redundant import 2026-05-25 14:55:53 +02:00
charles-gauthereau 3a4dd10b4c feat(api-v1): add withApiKey middleware 2026-05-25 14:46:20 +02:00
charles-gauthereau 0316fdc0a3 feat: added the api key list, delete, create in profile modal. 2026-05-25 13:04:42 +02:00
charles-gauthereau 72ecd7f054 feat: added the api key list, delete, create in profile modal. 2026-05-25 11:39:30 +02:00
Charles GTE 8db995010f Merge branch 'main' into dev 2026-05-24 17:13:16 +02:00
github-actions[bot] 539fec4dbc chore: release 1.15.5 2026-05-24 15:09:58 +00:00
Charles GTEandGitHub 3fa0f9b76d fix: feature-base-refactoring (#297)
* docs: add AGENTS.md with feature-based architecture conventions

Defines the target src/ structure, feature list, naming conventions
(kebab-case, file type patterns), import direction rules, and
feature boundary guidelines for the planned big-bang refactor.

* docs: add features/channel/ to architecture conventions

Generic channel UI (card, modal, form) goes to features/channel/.
Provider-specific sub-folders: channel/notifications/ and channel/storages/.

* refactor: move event emitter to lib/, project service to db/services/, dedup use-mobile hook

* fix: remove unused auth import in db/services/project.ts

* refactor: migrate wrappers/common to components/common (flat)

* fix: remove dead commented-out code from button-with-loading.tsx

* refactor: merge features/browser into features/theme, flatten upload/updates, migrate statistics

* refactor: migrate wrappers/auth to features/auth

Move all auth components and forms from src/components/wrappers/auth/
into src/features/auth/ with flat kebab-case naming. Handle extra
reset-password-form.tsx and differing reset-password-schema.ts by
copying them as reset-password-page-form.tsx and reset-password-page.schema.ts.

* refactor: remove dead auth files and fix zPassword schema

Delete reset-password-section.tsx and reset-password-page-form.tsx (zero consumers).
Fix reset-password-page.schema.ts to import zPassword from @/lib/zod.

* refactor: migrate wrappers/dashboard/common to features/layout, rename Header.tsx to header.tsx

* fix: decouple layout from theme/updates cross-feature imports

Use slot props (actions, updateNotification) so app/ composes
ModeToggle and UpdateNotification into Header and AppSidebar.
Delete dead side-bar-logo.tsx (zero consumers).

* refactor: merge features/keys into features/agents, flatten sub-dirs, migrate agent wrappers

- Merge src/features/keys/keys.action.ts into src/features/agents/
- Flatten src/features/agents/components/ and hooks/ into feature root
- Migrate all src/components/wrappers/dashboard/agent/ files into src/features/agents/
- Update all import paths across app/ and src/ accordingly
- Fix broken code-snippet import in agent-key-modal.tsx (was pointing to non-existent wrappers path)

* fix: move github-releases to lib/, remove dead code and unused imports

Move GitHub release fetch logic from features/updates/github.ts to
lib/github-releases.ts so both features/updates and features/agents
can import it without cross-feature imports.
Remove commented-out impersonation block from agent-database-card.tsx.
Remove unused imports (CopyButton, Terminal, Key, Info).

* refactor: flatten features/projects/components, migrate project wrappers

Move project.dialog/form out of sub-dir, migrate project-card, project-database-card, and delete-project button/action from wrappers into features/projects/. Update all import paths across src/ and app/. Leave wrappers/dashboard/projects/database/ untouched for Task 10.

* fix: extract DatabaseCard to components/common, remove cross-feature import

DatabaseCard was imported by both features/projects and features/agents,
violating the no-cross-feature-import rule. Extract to components/common/
database-card.tsx so both features import from the shared layer.
Also tighten extendedProps type and remove commented-out maxCount prop.

* refactor: migrate all database/backup/restore/health UI to features/database

Consolidates ~40 files from 5 source locations (features/dashboard/backup,
features/dashboard/restore, wrappers/dashboard/backup, wrappers/dashboard/database,
wrappers/dashboard/health, wrappers/dashboard/projects/database) into the new
flat features/database directory with kebab-case filenames and updated @/ imports.

* fix: remove dead restore-form files and console.log from health-grid

Delete restore-form.tsx (186 lines all commented-out, zero consumers)
and restore-form.schema.ts (only a commented import).
Remove console.log(logs) from getOldestLog helper in health-grid.tsx.

Cross-feature imports (database→storages for dispatchStorage/storeBackupFiles)
are accepted as known architectural coupling — storage dispatch is called from
database server actions at runtime and cannot route through app/ props.

* refactor: migrate wrappers/admin/users to features/users, fix table-colums typo

* fix: fix relative imports, string interpolation, and console.error in users feature

Replace relative imports with @/ aliases in user-actions-cell.tsx and
admin-user-edit-modal.tsx. Fix string interpolation bug in dialog title
(was regular string, now template literal). Remove console.error from
admin-user-change-role-modal.tsx.

* refactor: migrate wrappers/profile to features/profile, fix profile-apperance typo

* fix: remove console.error calls and ts-ignore in profile feature

Remove console.error from profile-account.tsx, 2fa-form.tsx (x2).
Replace @ts-ignore on fileInput.onchange with typed cast in avatar-with-upload.tsx.

* refactor: rename features/organization to features/organizations, migrate all org wrappers

Moves 37 org-related files from features/organization/, features/dashboard/,
hooks/, wrappers/admin/organizations/, wrappers/admin/channels/organization/,
and wrappers/dashboard/organization/ into a flat features/organizations/ feature
directory. Updates all import paths codebase-wide. Fixes @ts-ignore and removes
unused @better-fetch/fetch import in admin-organization-form.

* fix: remove console.error, @ts-ignore, and unused destructures in features/organizations

* refactor: migrate wrappers/organization/migration to features/migration

Move all 5 migration files to src/features/migration/, update internal
imports to @/features/migration/ absolute paths, remove console.log,
and delete src/components/wrappers/dashboard/organization/ entirely.

* refactor: migrate wrappers/admin/settings to features/settings

Move all 14 settings files (email, notification, storage, s3) from
wrappers/dashboard/admin/settings/ into features/settings/, rewriting
imports, replacing @ts-ignore with @ts-expect-error, and renaming error
params to _error where applicable.

* refactor: rename notifications files to kebab convention, migrate notification logs

- Rename dispatch.ts/helpers.ts/types.ts to notifications.*.ts
- Migrate notification logs components from wrappers/dashboard/admin/notifications/logs/ to features/notifications/
- Fix all import paths across codebase and in providers/
- Remove unused useRouter import, replace @ts-ignore with @ts-expect-error
- Delete src/components/wrappers/dashboard/admin/notifications/

* refactor: rename storages dispatch/helpers/types to kebab convention

- Rename dispatch.ts/helpers.ts/types.ts to storages.*.ts
- Fix all import paths across codebase and in providers/
- Fix relative ../types and ../../types imports in all storage providers to absolute @/ paths
- Replace @ts-ignore with @ts-expect-error, remove console.debug/console.error

* refactor: create features/channel with generic UI and notifications/storages provider sub-folders

Consolidates 4 source locations into src/features/channel/:
- components/wrappers/dashboard/admin/channels/ → features/channel/ (11 generic UI files)
- features/notifications/providers/ → features/channel/notifications/ (9 backend + 18 form files)
- features/storages/providers/ → features/channel/storages/ (backend + forms)

The two conflicting google-drive/helpers.ts files (client helpers vs OAuth action)
are kept separate: helpers.ts and google-drive-refresh.action.ts.

Rewrites all imports codebase-wide to point to new locations.
Removes console.log/console.error calls, upgrades @ts-ignore to @ts-expect-error.

* fix: remove console.error from local storage provider

* fix: multi-select.tsx scroll issue

* fix: multi-select.tsx scroll issue
2026-05-24 17:09:12 +02:00
Charles GTE 41d8b2c375 Merge branch 'main' into dev 2026-05-23 18:42:07 +02:00
github-actions[bot] 4fadfcfaf2 chore: release 1.15.4 2026-05-21 17:46:00 +00:00
af3df7f72a fix: multiple-webhook-headers and nextcloud talk provider (#296)
* feat(webhook): replace single secret header pair with webhookHeaders array schema

* feat(webhook): iterate webhookHeaders array; keep backward compat for legacy secret fields

* feat(webhook): replace single header pair with dynamic useFieldArray header list

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(webhook): add field labels to header key/value inputs

* fix(webhook): guard reserved headers; align delete button with inputs

* feat(nextcloud): add nextcloud to provider_kind enum

* feat(nextcloud): add ProviderKind entry and HMAC-signed provider

* feat(nextcloud): add Zod schema and form component

* feat(nextcloud): wire nextcloud into form registry and UI provider list

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* feat(nextcloud): register sendNextcloud in provider dispatch map

* fix: multiple webhook headers and nextcloud talk notification provider

* test(webhook): update e2e to use Add Header button and Header Value label

* fix: webhook.ts

---------

Co-authored-by: charles-gauthereau <charles.gauthereau@soluce-technologies.com>
Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-21 19:45:08 +02:00
Charles GTE 3b8fb93fb2 Merge branch 'main' into dev 2026-05-21 19:28:57 +02:00
github-actions[bot] 2cd5bda416 chore: release 1.15.3 2026-05-21 05:51:50 +00:00
965b215224 fix: replace integer column with bigint for size (#295)
Co-authored-by: charles-gauthereau <charles.gauthereau@soluce-technologies.com>
2026-05-21 07:51:08 +02:00
github-actions[bot] 27f6b746e0 chore: release 1.15.2 2026-05-21 05:28:56 +00:00
7d463d7da8 fix: default user from init .env variables (#293)
* fix: default user from init en .env variables

* fix: .env.example

---------

Co-authored-by: charles-gauthereau <charles.gauthereau@soluce-technologies.com>
2026-05-21 07:28:05 +02:00
killian-larcher 55be9374e1 Revert "docs: add OIDC E2E design spec"
This reverts commit b28bb44f97.
2026-05-20 21:45:46 +02:00
killian-larcher ce1bade773 Merge remote-tracking branch 'origin/dev' into dev 2026-05-17 10:46:53 +02:00
github-actions[bot] 332540488d chore: release 1.15.1 2026-05-17 08:42:07 +00:00
ab52ef6c27 fix: logger.ts (#288)
* 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.

* fix: logger.ts

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
Co-authored-by: killianlarcher <killian.larcher@soluce-technologies.com>
2026-05-17 10:41:36 +02:00
Charles GTE 9fe046617c fix: logger.ts 2026-05-17 10:34:54 +02:00
killian-larcher 01763a6b36 chore: ignore local worktrees 2026-05-17 10:31:15 +02:00
killian-larcher b28bb44f97 docs: add OIDC E2E design spec 2026-05-17 10:23:41 +02:00
Charles GTE 411d0b4565 Merge branch 'main' into dev 2026-05-17 10:04:10 +02:00
github-actions[bot] 2d55618898 chore: release 1.15.0 2026-05-17 08:00:16 +00:00
Charles GTEandGitHub 6b3df405eb fix: add-healthcheck (#287)
fix: add-healthcheck
2026-05-17 09:59:38 +02:00
Charles GTE e91ef4c142 fix: docker-compose.prod.yml 2026-05-17 09:52:58 +02:00
Charles GTE 9b504ccd68 fix: proxy.ts 2026-05-17 09:46:03 +02:00
Charles GTE dd3f4411cf chore: add startupProbe and fix probe timing for cold-boot safety 2026-05-17 09:39:11 +02:00
Charles GTE 0ab94b6221 chore: add liveness and readiness probes to Helm deployment 2026-05-17 09:37:28 +02:00
Charles GTE 8477889cea chore: fix healthcheck start_period and clean up curl command 2026-05-17 09:36:49 +02:00
Charles GTE 2eb4a7a0fe chore: add healthcheck probe to prod docker-compose app service 2026-05-17 09:35:01 +02:00
Charles GTE 1f68adda90 feat: add GET /api/health liveness endpoint 2026-05-17 09:33:53 +02:00
Charles GTE 16697c442c Merge branch 'main' into dev 2026-05-15 23:03:57 +02:00
Charles GTEandGitHub ffba2c56a3 fix: env
fix: env.mjs [skip-release]
2026-05-15 23:03:19 +02:00
github-actions[bot] 02be78a26e chore: release 1.14.1 2026-05-15 20:42:10 +00:00
Charles GTEandGitHub 711d4613ed fix: log noise #284
fix: logs noise
2026-05-15 22:41:21 +02:00
Charles GTE a5fe5225e1 fix: pin @better-auth/sso and @better-auth/passkey to 1.6.2 to fix build 2026-05-15 22:28:38 +02:00
Charles GTE b752d9437c chore: pnpm 2026-05-15 22:22:21 +02:00
Charles GTE 18df92e048 chore: update packages 2026-05-15 22:18:55 +02:00
Charles GTE 4e66b8ed04 fix: logs 2026-05-15 22:08:14 +02:00
Charles GTEandClaude Sonnet 4.6 4f14d16452 fix: replace console.log with Pino logger in server-side files
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-15 22:05:13 +02:00
Charles GTE 592588ef76 docs: set LOG_LEVEL=info as dev default in .env.example 2026-05-15 22:01:19 +02:00
Charles GTE 3fec7c3392 docs: document LOG_LEVEL env var in .env.example 2026-05-15 21:59:00 +02:00
Charles GTE 2335caefd7 fix: downgrade cleaningJob logs to debug; fix catch log level 2026-05-15 21:57:56 +02:00
Charles GTE 194eedeaa8 fix: downgrade agent status ping log to debug 2026-05-15 21:57:21 +02:00
Charles GTE ecd09aedb9 fix: cache log level index at module init in loggingMiddleware 2026-05-15 21:56:37 +02:00
Charles GTE b5350b5fe3 fix: .gitignore 2026-05-15 21:55:19 +02:00
Charles GTE ceaafd5531 fix: suppress agent status ping logs below debug level 2026-05-15 21:55:01 +02:00
Charles GTE 654accad4a fix: use nullish coalescing for LOG_LEVEL fallback 2026-05-15 21:53:54 +02:00
Charles GTE 173c7894b4 fix: respect LOG_LEVEL env var in Pino logger 2026-05-15 21:52:51 +02:00
Charles GTE 1ab51d15f9 fix: added mailpit 2026-05-15 21:24:11 +02:00
Charles GTEandGitHub ddce51d2d3 fix: README.md [skip-release] (#280) 2026-05-12 20:03:49 +02:00
Charles GTEandGitHub fc9fbf4357 chore: update the README.md with mssql (#278) [skip-release] 2026-05-11 22:27:19 +02:00
github-actions[bot] 5e8cd7e0e6 chore: release 1.14.0 2026-05-11 20:09:21 +00:00
Charles GTEandGitHub 1eb5fbb983 feat: add support for mssql (#277)
* feat: add support for mssql

* fix: common.ts
2026-05-11 22:08:36 +02:00
github-actions[bot] d9ccb389f0 chore: release 1.13.1 2026-05-11 17:34:22 +00:00
Charles GTE a3b7af3ba5 Merge branch 'main' into dev 2026-04-29 22:41:02 +02:00
github-actions[bot] dc80d1d752 chore: release 1.13.0 2026-04-29 20:40:27 +00:00
ff28a80d7c feat: migration-tool (#269)
* feat: working on migration backup tool

* feat: add migration sequences

* fix: responsive for migration tool

* fix: truncate database name in migration

* fix: filter on databases success for migration tool

---------

Co-authored-by: charles-gauthereau <charles.gauthereau@soluce-technologies.com>
2026-04-29 22:39:37 +02:00
github-actions[bot] ab55ab349a chore: release 1.12.1 2026-04-24 19:43:11 +00:00
Charles GTEandGitHub a1ec7dbec2 fix: migration error on 2fa (#268) 2026-04-24 21:42:20 +02:00
github-actions[bot] e29846ae25 chore: release 1.12.0 2026-04-10 16:38:28 +00:00
e38519aec2 feat: org agent management (#259)
* feat: org agent management

* feat: org agent management

* fix: adding migrations for legacy and refactoring.

* fix: refactoring

* fix: delete agent

* fix: refactoring

---------

Co-authored-by: charles-gauthereau <charles.gauthereau@soluce-technologies.com>
2026-04-10 18:37:35 +02:00
Charles GTE 33492b8f3a Merge branch 'main' into dev 2026-04-07 18:40:51 +02:00
github-actions[bot] a620d7a9f7 chore: release 1.11.5 2026-04-06 19:24:12 +00:00
Charles GTEandGitHub 88ebd79681 fix: username capitalization in profile modal (#257) 2026-04-06 21:23:36 +02:00
github-actions[bot] e08efa12fa chore: release 1.11.4 2026-04-06 19:09:07 +00:00
Charles GTEandGitHub 7a685c0518 fix: home project counter (#256)
* fix: home project counter

* fix: home project counter
2026-04-06 21:08:29 +02:00
github-actions[bot] 4deaaa2c8e chore: release 1.11.3 2026-04-06 18:34:59 +00:00
Charles GTEandGitHub adaf0f89fb fix: pipeline ci (#255) 2026-04-06 20:34:24 +02:00
47de5373b1 fix: add page size selector to CardsWithPagination (#253)
* fix: add page size selector to CardsWithPagination

Add an optional pageSizeOptions prop to CardsWithPagination that renders
a "Cards per page" dropdown selector, consistent with the existing
TablePaginationSize pattern used in DataTable.

- New PaginationSize component (standalone, no TanStack Table dependency)
- CardsWithPagination: manage pageSize as state, reset to page 1 on change
- Project databases page: increase default from 6 to 20, add selector [10, 20, 50]
- Projects list page: increase default from 9 to 12, add selector [12, 24, 48]
- Fully backward compatible: without pageSizeOptions, behavior is unchanged

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix: address CodeRabbit review feedback

- Validate pageSize in handlePageSizeChange (reject 0/negative/NaN)
- Guard empty pageSizeOptions array from rendering empty selector
- Restore className on PaginationNavigation for backward compatibility
- Add aria-label to SelectTrigger for accessibility on small screens

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

* fix(pagination-size): fallback to pageSizeOptions[0] when pageSize not in options

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>

* fix(pagination-size): fix pageSize is not defined (placeholder ref)

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-06 20:16:15 +02:00
github-actions[bot] db9d23b5ec chore: release 1.11.2 2026-04-02 17:47:39 +00:00
47c74931d4 fix: upload file database extension (#250)
* fix: upload file database

* fix: getFileHeadersBasedOnDbms for default if no dbType compatible

---------

Co-authored-by: charles-gauthereau <charles.gauthereau@soluce-technologies.com>
2026-04-02 19:47:05 +02:00
github-actions[bot] be01031501 chore: release 1.11.1 2026-04-02 12:43:10 +00:00
2e656d9c41 fix: storage broken url (#248)
Co-authored-by: charles-gauthereau <charles.gauthereau@soluce-technologies.com>
2026-04-02 14:42:30 +02:00
github-actions[bot] 9071193c4d chore: release 1.11.0 2026-04-02 09:08:40 +00:00
27e1da6363 fix: smtp crash due to SMTP_SECURE (#246)
Co-authored-by: charles-gauthereau <charles.gauthereau@soluce-technologies.com>
2026-04-02 11:07:48 +02:00
e02c5496ae chore: readme [skip-release] (#244)
* fix: set default notification channel

* fix: readme

* fix: README.md

---------

Co-authored-by: charles-gauthereau <charles.gauthereau@soluce-technologies.com>
2026-04-01 17:22:21 +02:00
3fee236fd0 feat: adding end-to-end testing for notification [skip-release] (#228)
* fix: release.yml

* fix: S3ChannelConfigSchema type port mismatch

* fix(csp): Conditionally sets UPGRADE_INSECURE_REQUESTS based on the PROJECT_URL scheme.

* chore: fix bug in github action.

* chore: fix bug in github action.

* chore: fix bug in github action.

* chore: fix bug in github action.

* chore: fix bug in github action.

* chore: fix bug in github action.

* chore: fix bug in github action.

* chore: fix bug in github action.

* chore: fix bug in github action.

* chore: fix bug in github action.

* chore: fix bug in github action.

* chore: fix bug in github action.

* chore: fix bug in github action.

* chore: fix bug in github action.

* chore: fix bug in github action.

* chore: fix bug in github action.

* chore: adding e2e tesing for notification.

* fix: updating trivy-action to the latest version, using new tagging pattern (vX.XX.XX instead of (X.XX.XX).

* fix: removing browser targeting in e2e workflow as it is already specify in playwright.config.ts

* fix: mapping secrets to environment variables in the end-to-end workflow.

* fix: add a condition to allow end-to-end workflow execution only on main repo (fork are excluded).

* fix: adding some data-testid to rely on more stable locator.

* fix: adding some data-testid to rely on more stable locator.

* fix: removing unusued browsers in end-to-end workflow.

* fix: removing unused browsers in end-to-end workflow.

* fix: removing unused browsers in end-to-end workflow.

---------

Co-authored-by: charlesgauthereau <charles.gauthereau@soluce-technologies.com>
Co-authored-by: killianlarcher <killian.larcher@soluce-technologies.com>
2026-03-31 20:25:15 +02:00
github-actions[bot] 78ba42239d chore: release 1.10.0 2026-03-31 13:09:08 +00:00
Charles GTEandGitHub 8ffb672e26 feat: add firebird (#242) 2026-03-31 15:08:31 +02:00
Charles GTE 150ab83f73 Merge branch 'main' into dev 2026-03-30 17:51:28 +02:00
Théo LAGACHEandGitHub 83ddc934ba fix: readme [skip-release] (#241)
* Update translations

* Add: firebird
2026-03-30 15:52:21 +02:00
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
Charles GTE 35db7a4416 fix: trusted url error due to better auth update. 2026-03-29 14:08:20 +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
Charles GTE 1ce93af144 fix: refactoring logs and adding pino logger for production and dev. 2026-03-29 11:41:39 +02:00
Charles GTE f410a8fdd5 Merge branch 'main' into dev 2026-03-29 11:23:38 +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 GTE 88914169bc Merge branch 'main' into dev 2026-03-29 09:25:00 +02:00
Charles GTEandGitHub aff782aa28 fix: mariadb and mysql separation (#234) 2026-03-29 09:24:31 +02:00
Charles GTE 8d51bcb74d Merge branch 'main' into dev 2026-03-28 19:01:31 +01: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
Charles GTE 5a87529934 fix: refactoring 2026-03-28 16:55:31 +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
Charles GTE 0d7a41996e chore: package.json 2026-03-21 08:39:29 +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
killianlarcher dc97de03d0 Merge branch 'main' into dev 2026-03-18 19:34:44 +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
charlesgauthereau 5d4a1248e4 fix: S3ChannelConfigSchema type port mismatch 2026-03-16 08:54:13 +01:00
charlesgauthereau f876d8eb09 Merge branch 'main' into dev 2026-03-16 07:59:39 +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
charlesgauthereau 1d1d92a5cf fix: release.yml 2026-03-08 21:57:13 +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
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
544 changed files with 88621 additions and 6499 deletions
+24 -2
View File
@@ -1,15 +1,29 @@
# Environment # Environment
NODE_ENV=development NODE_ENV=development
# Logging
# Controls minimum log level. Options: debug | info | warn | error
# Set to "warn" or "error" in production to suppress health-check ping noise.
# Use "debug" to see all request logs including agent status pings.
LOG_LEVEL=info
# Database # Database
DATABASE_URL=postgresql://devuser:changeme@localhost:5433/devdb?schema=public DATABASE_URL=postgresql://devuser:changeme@localhost:5433/devdb?schema=public
# Project # Project
PROJECT_NAME="Portabase" PROJECT_NAME="Portabase"
PROJECT_DESCRIPTION="Portabase is a powerful database manager"
PROJECT_URL=http://localhost:8887 PROJECT_URL=http://localhost:8887
PROJECT_SECRET= PROJECT_SECRET=
AUTH_DEFAULT_USER_NAME="Portabase Admin"
AUTH_DEFAULT_USER=user@example.com
# Password must contain at least 8 characters
# Password must contain at least 1 number
# Password must contain at least 1 lowercase letter
# Password must contain at least 1 uppercase letter
# Password must contain at least 1 special character
AUTH_DEFAULT_PASSWORD=testPASS123456!
# SMTP (email) # SMTP (email)
SMTP_HOST= SMTP_HOST=
SMTP_PORT= SMTP_PORT=
@@ -40,7 +54,6 @@ AUTH_OIDC_ROLE_MAP=""
AUTH_ALLOW_LINKING=true AUTH_ALLOW_LINKING=true
AUTH_ALLOW_UNLINKING=true AUTH_ALLOW_UNLINKING=true
# Social OAuth2 Authentification # Social OAuth2 Authentification
AUTH_SOCIAL_ID="" AUTH_SOCIAL_ID=""
AUTH_SOCIAL_TITLE="" AUTH_SOCIAL_TITLE=""
@@ -59,3 +72,12 @@ AUTH_PASSKEY_ENABLED=true
# Retention # Retention
RETENTION_CRON="* * * * *" RETENTION_CRON="* * * * *"
TRUSTED_DOMAINS="http://localhost:8887, http://localhost:3055, http://localhost:3056"
#OPENAPI_ENABLED=true
#API_ENABLED=true
#MCP_ENABLED=true
# Default to false
#TUSD_BEHIND_PROXY=true
+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 Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at 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 complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the All community leaders are obligated to respect the privacy and security of the
+11 -2
View File
@@ -14,8 +14,9 @@ Please take a moment to review this guide. It will help you understand how to co
2. [Reporting Issues](#reporting-issues) 2. [Reporting Issues](#reporting-issues)
3. [Submitting Changes](#submitting-changes) 3. [Submitting Changes](#submitting-changes)
4. [Code Style Guidelines](#code-style-guidelines) 4. [Code Style Guidelines](#code-style-guidelines)
5. [Pull Request Process](#pull-request-process) 5. [E2E Tests](#e2e-tests)
6. [Community Guidelines](#community-guidelines) 6. [Pull Request Process](#pull-request-process)
7. [Community Guidelines](#community-guidelines)
--- ---
@@ -88,6 +89,14 @@ If you encounter a bug or have a suggestion for improvement, follow these steps:
--- ---
## E2E Tests
E2E tests now live in a dedicated repository: [Portabase/e2e-tests](https://github.com/Portabase/e2e-tests).
If your contribution needs corresponding end-to-end test coverage, add it there instead of this repo.
---
## Pull Request Process ## Pull Request Process
1. Ensure your code passes all tests and linters. 1. Ensure your code passes all tests and linters.
+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. If you discover a security vulnerability in this project, we appreciate your help in disclosing it responsibly.
1. **Contact Us** 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. - A detailed description of the issue.
- Steps to reproduce the vulnerability (if applicable). - Steps to reproduce the vulnerability (if applicable).
- Any potential impacts or risks. - Any potential impacts or risks.
-105
View File
@@ -1,105 +0,0 @@
#name: Auto Release bot
#
#on:
# pull_request:
# types: [ closed ]
# branches:
# - main
#
#jobs:
# release:
# if: github.event.pull_request.merged == true
# runs-on: ubuntu-latest
# permissions:
# contents: write
#
# steps:
# - uses: actions/create-github-app-token@v1
# id: app-token
# with:
# app-id: ${{ vars.APP_ID }}
# private-key: ${{ secrets.APP_PRIVATE_KEY }}
#
# - uses: actions/checkout@v4
# with:
# fetch-depth: 0
# token: ${{ steps.app-token.outputs.token }}
# ref: ${{ github.base_ref }}
#
# - uses: pnpm/action-setup@v4
#
# - uses: actions/setup-node@v4
# with:
# node-version: "lts/*"
# cache: "pnpm"
#
# - run: pnpm install --frozen-lockfile
#
# - run: git config --global user.name 'github-actions[bot]'
# - run: git config --global user.email 'github-actions[bot]@users.noreply.github.com'
#
# - name: Push tags
# run: git push origin --tags
#
# - name: Run release-it
# run: |
# git pull origin main
# pnpm exec release-it --ci
# env:
# GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
name: Auto Release bot
on:
pull_request:
types: [closed]
branches:
- main
jobs:
release:
if: github.event.pull_request.merged == true
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- uses: actions/create-github-app-token@v1
id: app-token
with:
app-id: ${{ vars.APP_ID }}
private-key: ${{ secrets.APP_PRIVATE_KEY }}
- uses: actions/checkout@v4
with:
fetch-depth: 0
token: ${{ steps.app-token.outputs.token }}
ref: ${{ github.base_ref }}
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: "lts/*"
cache: "pnpm"
- run: pnpm install --frozen-lockfile
- run: git config --global user.name 'github-actions[bot]'
- run: git config --global user.email 'github-actions[bot]@users.noreply.github.com'
- name: Bump version and tag
run: pnpm exec release-it --ci --no-github
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
- name: Push commits and tags
run: |
git push origin main
git push origin --tags
- name: Create GitHub release
run: pnpm exec release-it --ci --only-github
env:
GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
+58 -19
View File
@@ -3,6 +3,9 @@ name: Discord Notification
on: on:
workflow_call: workflow_call:
inputs: inputs:
release_tag:
required: true
type: string
discord_title: discord_title:
required: true required: true
type: string type: string
@@ -21,50 +24,86 @@ on:
jobs: jobs:
notify-discord: notify-discord:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Send Discord Notification - name: Send Discord Notification
env: env:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
GH_TOKEN: ${{ secrets.GH_TOKEN }} GH_TOKEN: ${{ secrets.GH_TOKEN }}
RELEASE_TAG: ${{ inputs.release_tag }}
DISCORD_TITLE: ${{ inputs.discord_title }}
DISCORD_COLOR: ${{ inputs.discord_color }}
DISCORD_FOOTER: ${{ inputs.discord_footer }}
run: | run: |
RELEASE_INFO=$(gh release view "${{ github.ref_name }}" -R ${{ github.repository }} --json name,url,body,author) set -euo pipefail
RELEASE_TITLE=$(echo "$RELEASE_INFO" | jq -r .name) RELEASE_INFO=$(gh release view "$RELEASE_TAG" -R "${{ github.repository }}" --json name,url,body,author)
if [ -z "$RELEASE_TITLE" ] || [ "$RELEASE_TITLE" = "null" ]; then RELEASE_TITLE="${{ github.ref_name }}"; fi
RELEASE_URL=$(echo "$RELEASE_INFO" | jq -r .url) RELEASE_TITLE=$(echo "$RELEASE_INFO" | jq -r '.name // empty')
RELEASE_BODY=$(echo "$RELEASE_INFO" | jq -r .body) if [ -z "$RELEASE_TITLE" ]; then
RELEASE_TITLE="$RELEASE_TAG"
fi
RELEASE_URL=$(echo "$RELEASE_INFO" | jq -r '.url // empty')
RELEASE_BODY=$(echo "$RELEASE_INFO" | jq -r '.body // ""')
AUTHOR_NAME="Portabase" AUTHOR_NAME="Portabase"
AUTHOR_ICON="https://github.com/Portabase.png" AUTHOR_ICON="https://github.com/Portabase.png"
PAYLOAD=$(jq -n \ jq -n \
--arg title "$RELEASE_TITLE" \ --arg title "$RELEASE_TITLE" \
--arg description "$RELEASE_BODY" \ --arg description "$RELEASE_BODY" \
--arg url "$RELEASE_URL" \ --arg url "$RELEASE_URL" \
--arg author "$AUTHOR_NAME" \ --arg author "$AUTHOR_NAME" \
--arg icon "$AUTHOR_ICON" \ --arg icon "$AUTHOR_ICON" \
--arg discord_title "${{ inputs.discord_title }}" \ --arg discord_title "$DISCORD_TITLE" \
--arg discord_footer "${{ inputs.discord_footer }}" \ --arg discord_footer "$DISCORD_FOOTER" \
--argjson discord_color ${{ inputs.discord_color }} \ --argjson discord_color "$DISCORD_COLOR" \
'{ '{
content: $discord_title, content: (
if ($discord_title | length) > 1900
then ($discord_title[0:1900] + "...")
else $discord_title
end
),
embeds: [{ embeds: [{
title: $title, title: (
if ($title | length) > 256
then ($title[0:253] + "...")
else $title
end
),
url: $url, url: $url,
description: $description, description: (
if ($description | length) > 3800
then ($description[0:3800] + "\n\n...")
else $description
end
),
color: $discord_color, color: $discord_color,
author: { author: {
name: $author, name: (
if ($author | length) > 256
then ($author[0:253] + "...")
else $author
end
),
icon_url: $icon icon_url: $icon
}, },
footer: { footer: {
text: $discord_footer text: (
if ($discord_footer | length) > 512
then ($discord_footer[0:509] + "...")
else $discord_footer
end
)
} }
}] }]
}' }' > payload.json
)
curl -H "Content-Type: application/json" \ cat payload.json | jq .
-d "$PAYLOAD" \
"$DISCORD_WEBHOOK" curl --fail-with-body -sS \
-H "Content-Type: application/json" \
-d @payload.json \
"$DISCORD_WEBHOOK?wait=true"
+35 -7
View File
@@ -3,6 +3,12 @@ name: Docker Publish
on: on:
workflow_call: workflow_call:
inputs: inputs:
version:
required: true
type: string
ref:
required: true
type: string
image_name: image_name:
required: false required: false
type: string type: string
@@ -32,10 +38,13 @@ jobs:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
platform: [linux/amd64, linux/arm64] platform: [ linux/amd64, linux/arm64 ]
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with:
ref: ${{ inputs.ref }}
fetch-depth: 0
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v3
@@ -50,9 +59,10 @@ jobs:
id: prep id: prep
run: | run: |
ARCH=${{ matrix.platform == 'linux/amd64' && 'amd64' || 'arm64' }} ARCH=${{ matrix.platform == 'linux/amd64' && 'amd64' || 'arm64' }}
echo "image=${{ inputs.image_name }}:${GITHUB_REF#refs/tags/}-$ARCH" >> $GITHUB_OUTPUT echo "image=${{ inputs.image_name }}:${{inputs.version}}-$ARCH" >> $GITHUB_OUTPUT
echo "safe_platform=${{ matrix.platform == 'linux/amd64' && 'linux-amd64' || 'linux-arm64' }}" >> $GITHUB_OUTPUT echo "safe_platform=${{ matrix.platform == 'linux/amd64' && 'linux-amd64' || 'linux-arm64' }}" >> $GITHUB_OUTPUT
- name: Build and push image - name: Build and push image
uses: docker/build-push-action@v6 uses: docker/build-push-action@v6
with: with:
@@ -95,20 +105,38 @@ jobs:
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} 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 - name: Extract Docker metadata
id: meta id: meta
uses: docker/metadata-action@v5 uses: docker/metadata-action@v5
with: with:
images: ${{ inputs.image_name }} images: ${{ inputs.image_name }}
tags: | tags: |
type=semver,pattern={{version}} type=raw,value=${{ steps.tags.outputs.VERSION_TAG }}
type=semver,pattern={{major}}.{{minor}} type=raw,value=${{ steps.tags.outputs.MINOR_TAG }}
type=semver,pattern={{major}} type=raw,value=${{ steps.tags.outputs.MAJOR_TAG }}
type=raw,value=latest,enable=${{ inputs.add_latest }} type=raw,value=${{ steps.tags.outputs.LATEST_TAG }}
- name: Create and push manifest list - name: Create and push manifest list
working-directory: /tmp/digests working-directory: /tmp/digests
run: | run: |
DOCKER_IMAGES="$(cat amd64/image.txt) $(cat arm64/image.txt)" DOCKER_IMAGES="$(cat amd64/image.txt) $(cat arm64/image.txt)"
TAG_ARGS=$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") TAG_ARGS=$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON")
docker buildx imagetools create $DOCKER_IMAGES $TAG_ARGS echo $TAG_ARGS
docker buildx imagetools create $TAG_ARGS $DOCKER_IMAGES
+86
View File
@@ -0,0 +1,86 @@
name: End-to-end testing
on:
pull_request:
branches: [main]
permissions:
contents: read
packages: write
jobs:
build-image:
runs-on: ubuntu-latest
if: github.event.pull_request.head.repo.full_name == github.repository
outputs:
image: ${{ steps.image.outputs.ref }}
steps:
- uses: actions/checkout@v6
- name: Compute image reference
id: image
run: echo "ref=ghcr.io/${GITHUB_REPOSITORY_OWNER,,}/portabase:${GITHUB_SHA}" >> "$GITHUB_OUTPUT"
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push Docker image
uses: docker/build-push-action@v6
with:
context: .
file: ./docker/dockerfile/Dockerfile
push: true
tags: ${{ steps.image.outputs.ref }}
target: prod
run-e2e:
if: github.event.pull_request.head.repo.full_name == github.repository
needs: build-image
uses: portabase/e2e-tests/.github/workflows/run-e2e.yml@main
with:
server_image: ${{ needs.build-image.outputs.image }}
secrets:
E2E_NOTIFICATION_SMTP_HOST: ${{ secrets.E2E_NOTIFICATION_SMTP_HOST }}
E2E_NOTIFICATION_SMTP_PORT: ${{ secrets.E2E_NOTIFICATION_SMTP_PORT }}
E2E_NOTIFICATION_SMTP_USER: ${{ secrets.E2E_NOTIFICATION_SMTP_USER }}
E2E_NOTIFICATION_SMTP_PASSWORD: ${{ secrets.E2E_NOTIFICATION_SMTP_PASSWORD }}
E2E_NOTIFICATION_SMTP_FROM: ${{ secrets.E2E_NOTIFICATION_SMTP_FROM }}
E2E_NOTIFICATION_SMTP_TO: ${{ secrets.E2E_NOTIFICATION_SMTP_TO }}
E2E_NOTIFICATION_SLACK_WEBHOOK: ${{ secrets.E2E_NOTIFICATION_SLACK_WEBHOOK }}
E2E_NOTIFICATION_DISCORD_WEBHOOK: ${{ secrets.E2E_NOTIFICATION_DISCORD_WEBHOOK }}
E2E_NOTIFICATION_TELEGRAM_BOT_TOKEN: ${{ secrets.E2E_NOTIFICATION_TELEGRAM_BOT_TOKEN }}
E2E_NOTIFICATION_TELEGRAM_CHAT_ID: ${{ secrets.E2E_NOTIFICATION_TELEGRAM_CHAT_ID }}
E2E_NOTIFICATION_TELEGRAM_TOPIC_ID: ${{ secrets.E2E_NOTIFICATION_TELEGRAM_TOPIC_ID }}
E2E_NOTIFICATION_GOTIFY_SERVER_URL: ${{ secrets.E2E_NOTIFICATION_GOTIFY_SERVER_URL }}
E2E_NOTIFICATION_GOTIFY_APP_TOKEN: ${{ secrets.E2E_NOTIFICATION_GOTIFY_APP_TOKEN }}
E2E_NOTIFICATION_NTFY_TOPIC: ${{ secrets.E2E_NOTIFICATION_NTFY_TOPIC }}
E2E_NOTIFICATION_NTFY_SERVER_URL: ${{ secrets.E2E_NOTIFICATION_NTFY_SERVER_URL }}
E2E_NOTIFICATION_NTFY_TOKEN: ${{ secrets.E2E_NOTIFICATION_NTFY_TOKEN }}
E2E_NOTIFICATION_NTFY_USERNAME: ${{ secrets.E2E_NOTIFICATION_NTFY_USERNAME }}
E2E_NOTIFICATION_NTFY_PASSWORD: ${{ secrets.E2E_NOTIFICATION_NTFY_PASSWORD }}
E2E_NOTIFICATION_WEBHOOK_URL: ${{ secrets.E2E_NOTIFICATION_WEBHOOK_URL }}
E2E_NOTIFICATION_WEBHOOK_SECRET_HEADER: ${{ secrets.E2E_NOTIFICATION_WEBHOOK_SECRET_HEADER }}
E2E_NOTIFICATION_WEBHOOK_SECRET: ${{ secrets.E2E_NOTIFICATION_WEBHOOK_SECRET }}
E2E_STORAGE_AWS_S3_ENDPOINT_URL: ${{ secrets.E2E_STORAGE_AWS_S3_ENDPOINT_URL }}
E2E_STORAGE_AWS_S3_REGION: ${{ secrets.E2E_STORAGE_AWS_S3_REGION }}
E2E_STORAGE_AWS_S3_ACCESS_KEY: ${{ secrets.E2E_STORAGE_AWS_S3_ACCESS_KEY }}
E2E_STORAGE_AWS_S3_SECRET_KEY: ${{ secrets.E2E_STORAGE_AWS_S3_SECRET_KEY }}
E2E_STORAGE_AWS_S3_BUCKET_NAME: ${{ secrets.E2E_STORAGE_AWS_S3_BUCKET_NAME }}
E2E_STORAGE_AWS_S3_PORT: ${{ secrets.E2E_STORAGE_AWS_S3_PORT }}
E2E_STORAGE_R2_ENDPOINT_URL: ${{ secrets.E2E_STORAGE_R2_ENDPOINT_URL }}
E2E_STORAGE_R2_REGION: ${{ secrets.E2E_STORAGE_R2_REGION }}
E2E_STORAGE_R2_ACCESS_KEY: ${{ secrets.E2E_STORAGE_R2_ACCESS_KEY }}
E2E_STORAGE_R2_SECRET_KEY: ${{ secrets.E2E_STORAGE_R2_SECRET_KEY }}
E2E_STORAGE_R2_BUCKET_NAME: ${{ secrets.E2E_STORAGE_R2_BUCKET_NAME }}
E2E_STORAGE_R2_PORT: ${{ secrets.E2E_STORAGE_R2_PORT }}
E2E_STORAGE_GOOGLE_DRIVE_CLIENT_ID: ${{ secrets.E2E_STORAGE_GOOGLE_DRIVE_CLIENT_ID }}
E2E_STORAGE_GOOGLE_DRIVE_CLIENT_SECRET: ${{ secrets.E2E_STORAGE_GOOGLE_DRIVE_CLIENT_SECRET }}
E2E_STORAGE_GOOGLE_DRIVE_FOLDER_ID: ${{ secrets.E2E_STORAGE_GOOGLE_DRIVE_FOLDER_ID }}
+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
-47
View File
@@ -1,47 +0,0 @@
name: PR Checker
on:
pull_request:
branches:
- main
- dev
jobs:
validate-code:
name: Code Integrity Check
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Setup pnpm
uses: pnpm/action-setup@v4
- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: "lts/*"
cache: "pnpm"
- name: Install dependencies
run: pnpm install --frozen-lockfile
- name: Check code formatting
run: pnpm run format:check
continue-on-error: true
- name: Run Linter
run: pnpm run lint
continue-on-error: true
- name: Type Check
run: pnpm run typecheck
continue-on-error: true
- name: Run Tests
run: pnpm run test
continue-on-error: true
- name: Test Application Build
run: pnpm run build
+120 -22
View File
@@ -1,45 +1,143 @@
name: Publish Docker image for release name: Auto Release & Publish
on: on:
push: pull_request_target:
tags: types: [ closed ]
- "*.*.*" branches:
- "!*-*" - main
permissions: permissions:
contents: read contents: write
packages: write packages: write
jobs: 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' && github.event.pull_request.merged == true }}
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 uses: ./.github/workflows/docker.yml
with: with:
version: ${{ needs.create-release.outputs.version }}
ref: ${{ needs.create-release.outputs.version }}
add_latest: true add_latest: true
secrets: secrets:
DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME_2 }} DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME_2 }}
DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD_2 }} DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD_2 }}
publish_release:
needs: docker_publish publish-helm:
needs: create-release
if: ${{ needs.create-release.result == 'success' }}
uses: ./.github/workflows/helm.yml
with:
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 runs-on: ubuntu-latest
outputs:
release_tag: ${{ steps.publish_release_step.outputs.release_tag }}
steps: steps:
- name: Checkout code - uses: actions/checkout@v4
uses: actions/checkout@v4 with:
with: fetch-depth: 0
fetch-depth: 0
- name: Publish GitHub Release - name: Publish GitHub Release
env: id: publish_release_step
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} env:
run: | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
gh release edit v${{ github.ref_name }} --draft=false 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
discord_notification: notify-discord:
needs: docker_publish needs:
- publish-docker
- publish-helm
- create-release
- finalize-release
uses: ./.github/workflows/discord.yml uses: ./.github/workflows/discord.yml
with: with:
discord_title: "New Release: v${{ github.ref_name }}" release_tag: ${{ needs.create-release.outputs.version }}
discord_title: "New Release"
discord_color: 3066993 discord_color: 3066993
discord_footer: "Portabase" discord_footer: "Portabase"
secrets: secrets:
DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }} DISCORD_WEBHOOK: ${{ secrets.DISCORD_WEBHOOK }}
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+1 -2
View File
@@ -9,14 +9,13 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- uses: aquasecurity/trivy-action@0.20.0 - uses: aquasecurity/trivy-action@v0.35.0
with: with:
scan-type: 'fs' scan-type: 'fs'
format: 'table' format: 'table'
severity: 'CRITICAL,HIGH' severity: 'CRITICAL,HIGH'
ignore-unfixed: true ignore-unfixed: true
secrets-gitleaks: secrets-gitleaks:
if: github.event.pull_request.head.repo.fork == false if: github.event.pull_request.head.repo.fork == false
runs-on: ubuntu-latest runs-on: ubuntu-latest
+13
View File
@@ -47,3 +47,16 @@ private/keys/*
seeds/pocket-id/*.db seeds/pocket-id/*.db
certificates certificates
# Playwright
/test-results/
/playwright-report/
/blob-report/
/playwright/.cache/
/playwright/.auth/
/e2e/local-storage.json
.claude
.codex
/docs
.worktrees
+2 -2
View File
@@ -6,10 +6,10 @@
}, },
"git": { "git": {
"commit": true, "commit": true,
"commitMessage": "chore: release v${version}", "commitMessage": "chore: release ${version}",
"requireCleanWorkingDir": true, "requireCleanWorkingDir": true,
"tag": true, "tag": true,
"tagName": "v${version}", "tagName": "${version}",
"push": true "push": true
}, },
"plugins": { "plugins": {
+3 -1
View File
@@ -21,6 +21,8 @@ keywords:
- backups - backups
- postgresql - postgresql
- mysql - mysql
- firebird
- mssql
- mariadb - mariadb
- devops - devops
- database - database
@@ -31,5 +33,5 @@ keywords:
- web-ui - web-ui
- agent - agent
license: Apache-2.0 license: Apache-2.0
version: 1.4.26 version: 1.20.2
date-released: '2026-03-02' date-released: '2026-03-02'
+9 -1
View File
@@ -16,7 +16,7 @@ seed-pocket:
@docker compose -f docker-compose.func.yml stop pocket-id >/dev/null 2>&1 || true @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 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 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 run --rm -v $$(pwd)/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 @docker compose -f docker-compose.func.yml up -d pocket-id
@sleep 2 @sleep 2
@docker compose -f docker-compose.func.yml exec pocket-id ./pocket-id one-time-access-token admin @docker compose -f docker-compose.func.yml exec pocket-id ./pocket-id one-time-access-token admin
@@ -46,3 +46,11 @@ export-keycloak:
@docker rm -f kc-exporter >/dev/null 2>&1 @docker rm -f kc-exporter >/dev/null 2>&1
@docker compose -f docker-compose.func.yml start keycloak >/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" @echo "Keycloak configuration and users exported to seeds/keycloak/*.json"
seed-blob:
@chmod +x seeds/azurite/azurite-seed.sh
@bash seeds/azurite/azurite-seed.sh
list-blob:
@chmod +x seeds/azurite/azurite-list.sh
@bash seeds/azurite/azurite-list.sh
+26 -3
View File
@@ -12,7 +12,8 @@
[![License: Apache](https://img.shields.io/badge/License-apache-yellow.svg)](LICENSE) [![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)
[![Helm Chart](https://img.shields.io/badge/Helm-Kubernetes-326ce5?logo=helm&logoColor=white)](https://github.com/Portabase/portabase/pkgs/container/charts%2Fportabase)
[![Platform](https://img.shields.io/badge/platform-linux%20%7C%20macos%20%7C%20windows-lightgrey)](https://github.com/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) [![Support Portabase](https://img.shields.io/badge/Support-Portabase-orange)](https://www.buymeacoffee.com/portabase)
@@ -20,7 +21,12 @@
[![MySQL](https://img.shields.io/badge/MySQL-4479A1?logo=mysql&logoColor=white)](https://www.mysql.com/) [![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/) [![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/) [![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/)
[![MongoDB](https://img.shields.io/badge/-MongoDB-13aa52?logo=mongodb&logoColor=white)](https://www.mongodb.com/) [![MongoDB](https://img.shields.io/badge/-MongoDB-13aa52?logo=mongodb&logoColor=white)](https://www.mongodb.com/)
[![Valkey](https://img.shields.io/badge/Valkey-6284fc?style=flat&logo=Valkey&logoColor=white)](https://valkey.io/)
[![Firebird](https://img.shields.io/badge/Firebird-f55b14?style=flat&logo=Firebird&logoColor=white)](https://firebirdsql.org/)
[![Microsoft SQL Server](https://img.shields.io/badge/Microsoft%20SQL%20Server-CC2927?style=flat&logo=microsoftsqlserver&logoColor=white)](https://www.microsoft.com/en-us/sql-server)
[![Self Hosted](https://img.shields.io/badge/self--hosted-yes-brightgreen)](https://github.com/Portabase/portabase) [![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) [![Open Source](https://img.shields.io/badge/open%20source-❤️-red)](https://github.com/Portabase/portabase)
@@ -48,16 +54,33 @@
## Installation ## Installation
You have 4 ways to install Portabase: You have 5 ways to install Portabase:
- Automated CLI (recommended) - [details](https://portabase.io/docs/dashboard/setup#cli) - Automated CLI (recommended) - [details](https://portabase.io/docs/dashboard/setup#cli)
- Docker Run - [details](https://portabase.io/docs/dashboard/setup#docker) - Docker Run - [details](https://portabase.io/docs/dashboard/setup#docker)
- Docker Compose setup - [details](https://portabase.io/docs/dashboard/setup#docker-compose) - Docker Compose setup - [details](https://portabase.io/docs/dashboard/setup#docker-compose)
- Kubernetes with Helm (soon) - Unraid Community Apps - [details](https://ca.unraid.net/apps/portabase-dashboard-1sdc97m05ufd7q) - [unraid support thread](https://forums.unraid.net/topic/199072-support-portabase-dashboard-agent-dockers/)
- Kubernetes with Helm - [details](https://portabase.io/docs/dashboard/setup#helm)
- Development setup - [details](https://portabase.io/docs/dashboard/setup#development) - Development setup - [details](https://portabase.io/docs/dashboard/setup#development)
**Ensure Docker is installed on your machine before getting started.** **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 and 18 | Yes |
| **MySQL** | ✅ Stable | 5.7, 8 and 9 | Yes |
| **MariaDB** | ✅ Stable | 10 and 11 | Yes |
| **MongoDB** | ✅ Stable | 4, 5, 6, 7 and 8 | Yes |
| **SQLite** | ✅ Stable | 3.x | Yes |
| **Redis** | ✅ Stable | 2.8+ | No |
| **Valkey** | ✅ Stable | 7.2+ | No |
| **Firebird** | ✅ Stable | 3.0, 4.0, 5.0 | Yes |
| **MSSQL Server** | ✅ Stable | 2017, 2019, 2022, Azure SQL | Yes |
See the [Database Servers documentation](https://portabase.io/docs/agent/db) for version-specific backup and restore details.
## Contributors ## Contributors
[![Contributors](https://contrib.rocks/image?repo=Portabase/portabase)](https://github.com/Portabase/portabase/graphs/contributors) [![Contributors](https://contrib.rocks/image?repo=Portabase/portabase)](https://github.com/Portabase/portabase/graphs/contributors)
+1 -1
View File
@@ -1,7 +1,7 @@
import { CardContent, CardHeader } from "@/components/ui/card"; import { CardContent, CardHeader } from "@/components/ui/card";
import { TooltipProvider } from "@/components/ui/tooltip"; import { TooltipProvider } from "@/components/ui/tooltip";
import { ForgotPasswordForm } from "@/components/wrappers/auth/login/forgot-password-form/forgot-password-form"; import { ForgotPasswordForm } from "@/features/auth/forgot-password-form";
import { env } from "@/env.mjs"; import { env } from "@/env.mjs";
import { CardAuth } from "@/features/layout/card-auth"; import { CardAuth } from "@/features/layout/card-auth";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
+1 -1
View File
@@ -1,6 +1,6 @@
import {CardContent, CardHeader} from "@/components/ui/card"; import {CardContent, CardHeader} from "@/components/ui/card";
import {TooltipProvider} from "@/components/ui/tooltip"; import {TooltipProvider} from "@/components/ui/tooltip";
import {GuardForm} from "@/components/wrappers/auth/guard/guard-form"; import {GuardForm} from "@/features/auth/guard-form";
import {cookies} from "next/headers"; import {cookies} from "next/headers";
import {redirect} from "next/navigation"; import {redirect} from "next/navigation";
import {CardAuth} from "@/features/layout/card-auth"; import {CardAuth} from "@/features/layout/card-auth";
+2 -5
View File
@@ -1,10 +1,8 @@
import React, { Suspense } from "react"; import React from "react";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { currentUser } from "@/lib/auth/current-user"; import { currentUser } from "@/lib/auth/current-user";
import { AuthLogoSection } from "@/components/wrappers/auth/auth-logo-section"; import { AuthLogoSection } from "@/features/auth/auth-logo-section";
import { env } from "@/env.mjs";
import { Heart } from "lucide-react"; import { Heart } from "lucide-react";
import { ErrorLayout } from "@/components/wrappers/common/error-layout";
export default async function Layout({ export default async function Layout({
children, children,
@@ -31,7 +29,6 @@ export default async function Layout({
<Heart className="size-3 fill-red-500 text-red-500 animate-pulse" />{" "} <Heart className="size-3 fill-red-500 text-red-500 animate-pulse" />{" "}
by <span className="font-medium text-foreground">Portabase</span> by <span className="font-medium text-foreground">Portabase</span>
</p> </p>
<p>v{env.NEXT_PUBLIC_PROJECT_VERSION}</p>
</footer> </footer>
</div> </div>
); );
+2 -3
View File
@@ -1,14 +1,13 @@
import { LoginForm } from "@/components/wrappers/auth/login/login-form/login-form"; import { LoginForm } from "@/features/auth/login-form";
import { Metadata } from "next"; import { Metadata } from "next";
import { SUPPORTED_PROVIDERS } from "@/lib/auth/config"; import { SUPPORTED_PROVIDERS } from "@/lib/auth/config";
import { SocialAuthButtons } from "@/components/wrappers/auth/social-buttons"; import { SocialAuthButtons } from "@/features/auth/social-buttons";
import { TooltipProvider } from "@/components/ui/tooltip"; import { TooltipProvider } from "@/components/ui/tooltip";
import { CardContent, CardHeader } from "@/components/ui/card"; import { CardContent, CardHeader } from "@/components/ui/card";
import Link from "next/link"; import Link from "next/link";
import { Separator } from "@/components/ui/separator"; import { Separator } from "@/components/ui/separator";
import { CardAuth } from "@/features/layout/card-auth"; import { CardAuth } from "@/features/layout/card-auth";
import { env } from "@/env.mjs"; import { env } from "@/env.mjs";
import { en } from "zod/v4/locales";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Login", title: "Login",
+1 -1
View File
@@ -1,5 +1,5 @@
import { PageParams } from "@/types/next"; import { PageParams } from "@/types/next";
import { RegisterForm } from "@/components/wrappers/auth/register/register-form/register-form"; import { RegisterForm } from "@/features/auth/register-form";
import { Metadata } from "next"; import { Metadata } from "next";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { env } from "@/env.mjs"; import { env } from "@/env.mjs";
+1 -1
View File
@@ -1,6 +1,6 @@
import { CardContent, CardHeader } from "@/components/ui/card"; import { CardContent, CardHeader } from "@/components/ui/card";
import { TooltipProvider } from "@/components/ui/tooltip"; import { TooltipProvider } from "@/components/ui/tooltip";
import { ResetPasswordForm } from "@/components/wrappers/auth/login/reset-password-form/reset-password-form"; import { ResetPasswordForm } from "@/features/auth/reset-password-form";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { auth } from "@/lib/auth/auth"; import { auth } from "@/lib/auth/auth";
import { Avatar, AvatarImage, AvatarFallback } from "@radix-ui/react-avatar"; import { Avatar, AvatarImage, AvatarFallback } from "@radix-ui/react-avatar";
@@ -4,7 +4,7 @@ import {db} from "@/db";
import * as drizzleDb from "@/db"; import * as drizzleDb from "@/db";
import {PageParams} from "@/types/next"; import {PageParams} from "@/types/next";
import {Page} from "@/features/layout/page"; import {Page} from "@/features/layout/page";
import {OrganizationManagement} from "@/components/wrappers/dashboard/admin/organizations/organization/organization-management"; import {OrganizationManagement} from "@/features/organizations/admin-organization-management";
import {buildOrganizationWithMembers} from "@/utils/common"; import {buildOrganizationWithMembers} from "@/utils/common";
import {isUUID} from "@/utils/text"; import {isUUID} from "@/utils/text";
import {user} from "@/db/schema/02_user"; import {user} from "@/db/schema/02_user";
@@ -3,8 +3,8 @@ import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/
import {db} from "@/db"; import {db} from "@/db";
import { import {
AdminOrganizationAddModal AdminOrganizationAddModal
} from "@/components/wrappers/dashboard/admin/organizations/organization/admin-organization-add-modal"; } from "@/features/organizations/admin-organization-add-modal";
import {AdminOrganizationList} from "@/components/wrappers/dashboard/admin/organizations/organization/admin-orgnization-list"; import {AdminOrganizationList} from "@/features/organizations/admin-organization-list";
import {isNull} from "drizzle-orm"; import {isNull} from "drizzle-orm";
export default async function RoutePage(props: PageParams<{}>) { export default async function RoutePage(props: PageParams<{}>) {
@@ -1,11 +1,15 @@
import {PageParams} from "@/types/next"; import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page"; import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {db} from "@/db"; import {db} from "@/db";
import {logger} from "@/lib/logger";
import {notFound} from "next/navigation"; import {notFound} from "next/navigation";
import {SettingsTabs} from "@/components/wrappers/dashboard/admin/settings/settings-tabs"; import {SettingsTabs} from "@/features/settings/settings-tabs";
import {desc, isNull} from "drizzle-orm"; import {desc, isNull} from "drizzle-orm";
import * as drizzleDb from "@/db"; import * as drizzleDb from "@/db";
import {StorageChannelWith} from "@/db/schema/12_storage-channel"; import {StorageChannelWith} from "@/db/schema/12_storage-channel";
import {NotificationChannelWith} from "@/db/schema/09_notification-channel";
const log = logger.child({module: "dashboard/admin/settings"});
export default async function RoutePage(props: PageParams<{}>) { export default async function RoutePage(props: PageParams<{}>) {
@@ -13,6 +17,8 @@ export default async function RoutePage(props: PageParams<{}>) {
where: (fields, {eq}) => eq(fields.name, "system"), where: (fields, {eq}) => eq(fields.name, "system"),
}); });
log.debug({settings}, "Settings loaded");
const storageChannels = await db.query.storageChannel.findMany({ const storageChannels = await db.query.storageChannel.findMany({
with: { with: {
organizations: true organizations: true
@@ -21,7 +27,16 @@ export default async function RoutePage(props: PageParams<{}>) {
orderBy: desc(drizzleDb.schemas.storageChannel.createdAt) orderBy: desc(drizzleDb.schemas.storageChannel.createdAt)
}) as StorageChannelWith[] }) 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() notFound()
} }
@@ -33,7 +48,8 @@ export default async function RoutePage(props: PageParams<{}>) {
</div> </div>
</PageHeader> </PageHeader>
<PageContent className="flex flex-col gap-5"> <PageContent className="flex flex-col gap-5">
<SettingsTabs storageChannels={storageChannels} settings={settings} /> <SettingsTabs storageChannels={storageChannels} notificationChannels={notificationChannels}
settings={settings}/>
</PageContent> </PageContent>
</Page> </Page>
); );
@@ -2,8 +2,8 @@ import {PageParams} from "@/types/next";
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page"; import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {db} from "@/db"; import {db} from "@/db";
import {desc, isNull} from "drizzle-orm"; import {desc, isNull} from "drizzle-orm";
import {AdminUserList} from "@/components/wrappers/dashboard/admin/users/admin-user-list"; import {AdminUserList} from "@/features/users/admin-user-list";
import {AdminUserAddModal} from "@/components/wrappers/dashboard/admin/users/admin-user-add-modal"; import {AdminUserAddModal} from "@/features/users/admin-user-add-modal";
import {SUPPORTED_PROVIDERS} from "@/lib/auth/config"; import {SUPPORTED_PROVIDERS} from "@/lib/auth/config";
export default async function RoutePage(props: PageParams<{}>) { export default async function RoutePage(props: PageParams<{}>) {
@@ -30,9 +30,11 @@ export default async function RoutePage(props: PageParams<{}>) {
<PageHeader className="flex flex-col"> <PageHeader className="flex flex-col">
<div className="flex justify-between"> <div className="flex justify-between">
<PageTitle className="mb-3">Active users</PageTitle> <PageTitle className="mb-3">Active users</PageTitle>
<PageActions> {isPasswordAuthEnabled && (
<AdminUserAddModal organizations={organizations}/> <PageActions>
</PageActions> <AdminUserAddModal organizations={organizations}/>
</PageActions>
)}
</div> </div>
</PageHeader> </PageHeader>
<PageContent className="flex flex-col gap-5"> <PageContent className="flex flex-col gap-5">
@@ -7,15 +7,15 @@ import {
} from "@/features/layout/page"; } from "@/features/layout/page";
import { db } from "@/db"; import { db } from "@/db";
import * as drizzleDb from "@/db"; import * as drizzleDb from "@/db";
import { eq } from "drizzle-orm"; import {eq, isNull} from "drizzle-orm";
import { notFound } from "next/navigation"; import { notFound } from "next/navigation";
import { ButtonDeleteAgent } from "@/components/wrappers/dashboard/agent/button-delete-agent/button-delete-agent"; import { ButtonDeleteAgent } from "@/features/agents/agent-delete-button";
import { capitalizeFirstLetter } from "@/utils/text"; import { capitalizeFirstLetter } from "@/utils/text";
import { generateEdgeKey } from "@/utils/edge_key"; import { generateEdgeKey } from "@/utils/edge_key";
import { getServerUrl } from "@/utils/get-server-url"; import { getServerUrl } from "@/utils/get-server-url";
import { AgentContentPage } from "@/components/wrappers/dashboard/agent/agent-content"; import { AgentContentPage } from "@/features/agents/agent-content";
import { AgentDialog } from "@/features/agents/components/agent.dialog"; import { AgentDialog } from "@/features/agents/agent-dialog";
import { AgentType } from "@/features/agents/agents.schema";
export default async function RoutePage( export default async function RoutePage(
props: PageParams<{ agentId: string }>, props: PageParams<{ agentId: string }>,
@@ -26,13 +26,30 @@ export default async function RoutePage(
where: eq(drizzleDb.schemas.agent.id, agentId), where: eq(drizzleDb.schemas.agent.id, agentId),
with: { with: {
databases: true, databases: true,
organizations: true,
}, },
}); });
const organizations = await db.query.organization.findMany({
where: (fields) => isNull(fields.deletedAt),
with: {
members: true,
},
});
if (!agent) { if (!agent) {
notFound(); notFound();
} }
const isOwnerByAnOrganization = agent.organizationId
if (isOwnerByAnOrganization){
notFound();
}
const organizationIds = agent.organizations.map(org => org.organizationId)
const edgeKey = await generateEdgeKey(getServerUrl(), agent.id); const edgeKey = await generateEdgeKey(getServerUrl(), agent.id);
return ( return (
@@ -45,12 +62,14 @@ export default async function RoutePage(
<div className="flex items-center gap-2 md:justify-between w-full "> <div className="flex items-center gap-2 md:justify-between w-full ">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<AgentDialog <AgentDialog
agent={agent as AgentType & { id: string }} agent={agent}
typeTrigger={"edit"} typeTrigger={"edit"}
adminView={true}
organizations={organizations}
/> />
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<ButtonDeleteAgent agentId={agentId} text={"Delete Agent"} /> <ButtonDeleteAgent organizationIds={organizationIds} agentId={agentId} text={"Delete Agent"} />
</div> </div>
</div> </div>
</PageTitle> </PageTitle>
@@ -1,13 +1,13 @@
import {PageParams} from "@/types/next"; import {PageParams} from "@/types/next";
import {AgentCard} from "@/components/wrappers/dashboard/agent/agent-card/agent-card"; import {AgentCard} from "@/features/agents/agent-card";
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination"; import {CardsWithPagination} from "@/components/common/cards-with-pagination";
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page"; import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {notFound} from "next/navigation"; import {notFound} from "next/navigation";
import {db} from "@/db"; import {db} from "@/db";
import * as drizzleDb from "@/db"; import * as drizzleDb from "@/db";
import {desc, eq, not} from "drizzle-orm"; import {and, desc, eq, isNull, not} from "drizzle-orm";
import {Metadata} from "next"; import {Metadata} from "next";
import {AgentDialog} from "@/features/agents/components/agent.dialog"; import {AgentDialog} from "@/features/agents/agent-dialog";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Agents", title: "Agents",
@@ -16,13 +16,13 @@ export const metadata: Metadata = {
export default async function RoutePage(props: PageParams<{}>) { export default async function RoutePage(props: PageParams<{}>) {
const agents = await db.query.agent.findMany({ const agents = await db.query.agent.findMany({
where: not(eq(drizzleDb.schemas.agent.isArchived, true)), where: and(not(eq(drizzleDb.schemas.agent.isArchived, true)),isNull(drizzleDb.schemas.agent.organizationId)),
with: { with: {
databases: true databases: true
}, },
orderBy: (fields) => desc(fields.lastContact), orderBy: (fields) => desc(fields.lastContact),
}); });
if (!agents) { if (!agents) {
notFound(); notFound();
} }
@@ -4,8 +4,8 @@ import {Metadata} from "next";
import {db} from "@/db"; import {db} from "@/db";
import {notificationChannel, NotificationChannelWith} from "@/db/schema/09_notification-channel"; import {notificationChannel, NotificationChannelWith} from "@/db/schema/09_notification-channel";
import {desc, isNull} from "drizzle-orm"; import {desc, isNull} from "drizzle-orm";
import {ChannelsSection} from "@/components/wrappers/dashboard/admin/channels/channels-section"; import {ChannelsSection} from "@/features/channel/channels-section";
import {ChannelAddEditModal} from "@/components/wrappers/dashboard/admin/channels/channel/channel-add-edit-modal"; import {ChannelAddEditModal} from "@/features/channel/channel-add-edit-modal";
import * as drizzleDb from "@/db"; import * as drizzleDb from "@/db";
export const metadata: Metadata = { export const metadata: Metadata = {
@@ -1,7 +1,7 @@
import {PageParams} from "@/types/next"; import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page"; import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {Metadata} from "next"; import {Metadata} from "next";
import {NotificationLogsList} from "@/components/wrappers/dashboard/admin/notifications/logs/notification-logs-list"; import {NotificationLogsList} from "@/features/notifications/notification-logs-list";
import {notFound} from "next/navigation"; import {notFound} from "next/navigation";
import {getNotificationHistory} from "@/db/services/notification-log"; import {getNotificationHistory} from "@/db/services/notification-log";
@@ -1,12 +1,12 @@
import {PageParams} from "@/types/next"; import {PageParams} from "@/types/next";
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page"; import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {Metadata} from "next"; import {Metadata} from "next";
import {ChannelsSection} from "@/components/wrappers/dashboard/admin/channels/channels-section"; import {ChannelsSection} from "@/features/channel/channels-section";
import {db} from "@/db"; import {db} from "@/db";
import {desc, eq, isNull} from "drizzle-orm"; import {desc, eq, isNull} from "drizzle-orm";
import * as drizzleDb from "@/db"; import * as drizzleDb from "@/db";
import {StorageChannelWith} from "@/db/schema/12_storage-channel"; import {StorageChannelWith} from "@/db/schema/12_storage-channel";
import {ChannelAddEditModal} from "@/components/wrappers/dashboard/admin/channels/channel/channel-add-edit-modal"; import {ChannelAddEditModal} from "@/features/channel/channel-add-edit-modal";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Storage Channels", title: "Storage Channels",
@@ -0,0 +1,65 @@
import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {notFound} from "next/navigation";
import {getOrganization} from "@/lib/auth/auth";
import {Metadata} from "next";
import {db} from "@/db";
import {MigrationTool} from "@/features/migration/migration-tool";
export const metadata: Metadata = {
title: "Projects",
};
export default async function RoutePage(props: PageParams<{}>) {
const organization = await getOrganization({});
if (!organization) {
notFound();
}
const projects = await db.query.project.findMany({
where: (project, {eq, and, not}) =>
and(
eq(project.organizationId, organization.id),
not(eq(project.isArchived, true))
),
with: {
organization: true,
databases: {
where: (database, { isNull, not, inArray, and }) =>
and(
isNull(database.deletedAt),
not(inArray(database.dbms, ["valkey", "redis"]))
),
with: {
backups: {
where: (backup, { isNull, eq, and }) =>
and(
isNull(backup.deletedAt),
eq(backup.status, "success")
),
orderBy: (backup, {desc}) => [desc(backup.createdAt)],
limit: 15,
}
}
},
},
});
return (
<Page>
<PageHeader className="flex flex-col items-start justify-between mb-6">
<PageTitle className="mb-2">
Database Migration
</PageTitle>
<p className="text-sm text-muted-foreground">
Import backups from a source project into your target database
</p>
</PageHeader>
<PageContent>
<MigrationTool projects={projects}/>
</PageContent>
</Page>
);
}
@@ -4,10 +4,12 @@ import {Page} from "@/features/layout/page";
import {db} from "@/db"; import {db} from "@/db";
import {eq, and, inArray} from "drizzle-orm"; import {eq, and, inArray} from "drizzle-orm";
import * as drizzleDb from "@/db"; import * as drizzleDb from "@/db";
import {getOrganizationProjectDatabases} from "@/lib/services"; import {getOrganizationProjectDatabases} from "@/db/services/project";
import {getActiveMember, getOrganization} from "@/lib/auth/auth"; import {getActiveMember, getOrganization} from "@/lib/auth/auth";
import {BackupModalProvider} from "@/components/wrappers/dashboard/database/backup/backup-modal-context"; import {BackupModalProvider} from "@/features/database/backup-modal-context";
import {DatabaseContent} from "@/components/wrappers/dashboard/projects/database/database-content"; import {DatabaseContent} from "@/features/database/database-content";
import {getHealthLast12hLogs} from "@/db/services/healthcheck";
import {LogsModalProvider} from "@/features/logs/logs-modal-context";
export default async function RoutePage(props: PageParams<{ export default async function RoutePage(props: PageParams<{
projectId: string; projectId: string;
@@ -33,7 +35,7 @@ export default async function RoutePage(props: PageParams<{
project: true, project: true,
retentionPolicy: true, retentionPolicy: true,
alertPolicies: true, alertPolicies: true,
storagePolicies: true storagePolicies: true,
} }
}); });
@@ -49,13 +51,17 @@ export default async function RoutePage(props: PageParams<{
with: { with: {
storageChannel: true storageChannel: true
} }
} },
logs: true
}, },
orderBy: (b, {desc}) => [desc(b.createdAt)], orderBy: (b, {desc}) => [desc(b.createdAt)],
}); });
const restorations = await db.query.restoration.findMany({ const restorations = await db.query.restoration.findMany({
where: eq(drizzleDb.schemas.restoration.databaseId, dbItem.id), where: eq(drizzleDb.schemas.restoration.databaseId, dbItem.id),
with: {
logs: true
},
orderBy: (r, {desc}) => [desc(r.createdAt)], orderBy: (r, {desc}) => [desc(r.createdAt)],
}); });
@@ -83,6 +89,8 @@ export default async function RoutePage(props: PageParams<{
notFound(); notFound();
} }
const databaseHealthLogs = dbItem ? await getHealthLast12hLogs({id: dbItem.id}) : []
const successRate = totalBackups > 0 ? (successfulBackups / totalBackups) * 100 : null; const successRate = totalBackups > 0 ? (successfulBackups / totalBackups) * 100 : null;
@@ -90,22 +98,25 @@ export default async function RoutePage(props: PageParams<{
return ( return (
<Page> <Page>
<BackupModalProvider> <LogsModalProvider>
<DatabaseContent <BackupModalProvider>
activeMember={activeMember} <DatabaseContent
settings={settings} activeMember={activeMember}
database={dbItem} settings={settings}
isAlreadyRestore={isAlreadyRestore} database={dbItem}
restorations={restorations} databaseHealthLogs={databaseHealthLogs}
backups={backups} isAlreadyRestore={isAlreadyRestore}
totalBackups={totalBackups} restorations={restorations}
availableBackups={availableBackups} backups={backups}
successRate={successRate} totalBackups={totalBackups}
organizationId={organization.id} availableBackups={availableBackups}
activeOrganizationChannels={[]} successRate={successRate}
activeOrganizationStorageChannels={[]} organizationId={organization.id}
/> activeOrganizationChannels={[]}
</BackupModalProvider> activeOrganizationStorageChannels={[]}
/>
</BackupModalProvider>
</LogsModalProvider>
</Page> </Page>
); );
} }
@@ -2,20 +2,19 @@ import {PageParams} from "@/types/next";
import {Page, PageContent, PageTitle} from "@/features/layout/page"; import {Page, PageContent, PageTitle} from "@/features/layout/page";
import { import {
ButtonDeleteProject ButtonDeleteProject
} from "@/components/wrappers/dashboard/projects/button-delete-project/button-delete-project"; } from "@/features/projects/project-delete-button";
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination"; import {CardsWithPagination} from "@/components/common/cards-with-pagination";
import {ProjectDatabaseCard} from "@/components/wrappers/dashboard/projects/project-card/project-database-card"; import {ProjectDatabaseCard} from "@/features/projects/project-database-card";
import {notFound, redirect} from "next/navigation"; import {notFound, redirect} from "next/navigation";
import {db} from "@/db"; import {db} from "@/db";
import {eq} from "drizzle-orm"; import {eq} from "drizzle-orm";
import {getActiveMember, getOrganization} from "@/lib/auth/auth"; import {getActiveMember, getOrganization} from "@/lib/auth/auth";
import * as drizzleDb from "@/db"; import * as drizzleDb from "@/db";
import {capitalizeFirstLetter} from "@/utils/text"; import {capitalizeFirstLetter} from "@/utils/text";
import {ProjectDialog} from "@/features/projects/components/project.dialog"; import {ProjectDialog} from "@/features/projects/project-dialog";
import {DatabaseWith} from "@/db/schema/07_database";
import {ProjectWith} from "@/db/schema/06_project"; import {ProjectWith} from "@/db/schema/06_project";
import {isUuidv4} from "@/utils/verify-uuid"; import {isUuidv4} from "@/utils/verify-uuid";
import {getOrganizationAvailableDatabases} from "@/db/services/database";
export default async function RoutePage(props: PageParams<{ export default async function RoutePage(props: PageParams<{
@@ -56,19 +55,7 @@ export default async function RoutePage(props: PageParams<{
redirect("/dashboard/projects"); redirect("/dashboard/projects");
} }
const availableDatabases = ( const availableDatabases = await getOrganizationAvailableDatabases(organization.id, proj.id)
await db.query.database.findMany({
where: (db, {or, eq, isNull}) => or(isNull(db.projectId), eq(db.projectId, proj.id)),
with: {
agent: true,
project: true,
backups: true,
restorations: true,
},
orderBy: (db, {desc}) => [desc(db.createdAt)],
})
) as DatabaseWith[];
const isMember = activeMember?.role === "member"; const isMember = activeMember?.role === "member";
return ( return (
@@ -98,12 +85,15 @@ export default async function RoutePage(props: PageParams<{
<PageContent className="flex flex-col w-full h-full"> <PageContent className="flex flex-col w-full h-full">
{proj.databases.length > 0 ? ( {proj.databases.length > 0 ? (
<CardsWithPagination <CardsWithPagination
data={proj.databases} data={[...proj.databases].sort((a, b) =>
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime()
)}
organizationSlug={organization.slug} organizationSlug={organization.slug}
// @ts-ignore // @ts-ignore
cardItem={ProjectDatabaseCard} cardItem={ProjectDatabaseCard}
cardsPerPage={6} cardsPerPage={20}
numberOfColumns={3} numberOfColumns={3}
pageSizeOptions={[10, 20, 50]}
extendedProps={proj} extendedProps={proj}
/> />
) : ( ) : (
@@ -1,14 +1,15 @@
import {PageParams} from "@/types/next"; import {PageParams} from "@/types/next";
import {CardsWithPagination} from "@/components/wrappers/common/cards-with-pagination"; import {CardsWithPagination} from "@/components/common/cards-with-pagination";
import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page"; import {Page, PageActions, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {ProjectCard} from "@/components/wrappers/dashboard/projects/project-card/project-card"; import {ProjectCard} from "@/features/projects/project-card";
import {db} from "@/db"; import {db} from "@/db";
import {notFound} from "next/navigation"; import {notFound} from "next/navigation";
import {getActiveMember, getOrganization} from "@/lib/auth/auth"; import {getActiveMember, getOrganization} from "@/lib/auth/auth";
import {EmptyStatePlaceholder} from "@/components/wrappers/common/empty-state-placeholder"; import {EmptyStatePlaceholder} from "@/components/common/empty-state-placeholder";
import {Metadata} from "next"; import {Metadata} from "next";
import {ProjectDialog} from "@/features/projects/components/project.dialog"; import {ProjectDialog} from "@/features/projects/project-dialog";
import {DatabaseWith} from "@/db/schema/07_database"; import {DatabaseWith} from "@/db/schema/07_database";
import {getOrganizationAvailableDatabases} from "@/db/services/database";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Projects", title: "Projects",
@@ -35,18 +36,7 @@ export default async function RoutePage(props: PageParams<{}>) {
}); });
const isMember = activeMember?.role === "member"; const isMember = activeMember?.role === "member";
const availableDatabases = ( const availableDatabases = await getOrganizationAvailableDatabases(organization.id)
await db.query.database.findMany({
where: (db, {isNull}) => isNull(db.projectId),
with: {
agent: true,
project: true,
backups: true,
restorations: true,
},
orderBy: (db, {desc}) => [desc(db.createdAt)],
})
).filter((db) => db.project == null) as DatabaseWith[];
return ( return (
@@ -66,11 +56,12 @@ export default async function RoutePage(props: PageParams<{}>) {
organizationSlug={organization.slug} organizationSlug={organization.slug}
data={projects} data={projects}
cardItem={ProjectCard} cardItem={ProjectCard}
cardsPerPage={9} cardsPerPage={12}
numberOfColumns={3} numberOfColumns={3}
pageSizeOptions={[12, 24, 48]}
/> />
) : isMember ? ( ) : isMember ? (
<EmptyStatePlaceholder text="No project available"/> <EmptyStatePlaceholder state={"empty"} text="No project available"/>
) : ( ) : (
<ProjectDialog databases={availableDatabases} organization={organization} isEmpty={true}/> <ProjectDialog databases={availableDatabases} organization={organization} isEmpty={true}/>
)} )}
@@ -0,0 +1,101 @@
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 "@/features/agents/agent-delete-button";
import {capitalizeFirstLetter} from "@/utils/text";
import {generateEdgeKey} from "@/utils/edge_key";
import {getServerUrl} from "@/utils/get-server-url";
import {AgentContentPage} from "@/features/agents/agent-content";
import {AgentDialog} from "@/features/agents/agent-dialog";
import {getActiveMember, getOrganization} from "@/lib/auth/auth";
import {currentUser} from "@/lib/auth/current-user";
import {computeOrganizationPermissions} from "@/lib/acl/organization-acl";
export default async function RoutePage(
props: PageParams<{ agentId: string }>,
) {
const {agentId} = await props.params;
const organization = await getOrganization({});
const user = await currentUser();
const activeMember = await getActiveMember();
if (!organization || !activeMember || !user) {
notFound();
}
const {canManageAgents} = computeOrganizationPermissions(activeMember);
if (!canManageAgents){
notFound();
}
const agent = await db.query.agent.findFirst({
where: eq(drizzleDb.schemas.agent.id, agentId),
with: {
databases: true,
organizations: true,
},
});
if (!agent) {
notFound();
}
const hasAccess =
agent.organizationId === organization.id ||
agent.organizations.some(org => org.organizationId === organization.id);
if (!hasAccess) {
notFound();
}
const isOwned = agent.organizationId
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>
{isOwned && (
<div className="flex items-center gap-2 md:justify-between w-full ">
<div className="flex items-center gap-2">
<AgentDialog
agent={agent}
typeTrigger={"edit"}
/>
</div>
<div className="flex items-center gap-2">
<ButtonDeleteAgent organizationId={organization.id ?? null} 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>
);
}
@@ -0,0 +1,5 @@
import { redirect } from "next/navigation";
export default async function RoutePage() {
redirect("/dashboard/settings?tab=agents");
}
@@ -1,95 +1,114 @@
import { PageParams } from "@/types/next"; import {PageParams} from "@/types/next";
import { import {
Page, Page,
PageActions, PageContent,
PageContent, PageHeader,
PageHeader, PageTitle,
PageTitle,
} from "@/features/layout/page"; } from "@/features/layout/page";
import { currentUser } from "@/lib/auth/current-user"; import {currentUser} from "@/lib/auth/current-user";
import { getActiveMember, getOrganization } from "@/lib/auth/auth"; import {getActiveMember, getOrganization} from "@/lib/auth/auth";
import { notFound } from "next/navigation"; import {notFound} from "next/navigation";
import { Metadata } from "next"; import {Metadata} from "next";
import { OrganizationTabs } from "@/components/wrappers/dashboard/organization/tabs/organization-tabs"; import {OrganizationTabs} from "@/features/organizations/organization-tabs";
import { getOrganizationChannels } from "@/db/services/notification-channel"; import {getOrganizationChannels} from "@/db/services/notification-channel";
import { computeOrganizationPermissions } from "@/lib/acl/organization-acl"; import {computeOrganizationPermissions} from "@/lib/acl/organization-acl";
import { getOrganizationStorageChannels } from "@/db/services/storage-channel"; import {getOrganizationStorageChannels} from "@/db/services/storage-channel";
import { DeleteOrganizationButton } from "@/components/wrappers/dashboard/organization/delete-organization-button"; import {DeleteOrganizationButton} from "@/features/organizations/organization-delete-button";
import { EditOrganizationDialog } from "@/features/organization/components/edit-organization.dialog"; import {EditOrganizationDialog} from "@/features/organizations/organization-edit-dialog";
import { db } from "@/db"; import {db} from "@/db";
import { isNull } from "drizzle-orm"; import {isNull} from "drizzle-orm";
import * as drizzleDb from "@/db"; import * as drizzleDb from "@/db";
import { eq } from "drizzle-orm"; import {eq} from "drizzle-orm";
import {getOrganizationAgents} from "@/db/services/agent";
import {Tooltip, TooltipContent, TooltipTrigger} from "@/components/ui/tooltip";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Settings", title: "Settings",
}; };
export default async function RoutePage(props: PageParams<{ slug: string }>) { export default async function RoutePage(props: PageParams<{ slug: string }>) {
const organization = await getOrganization({}); const organization = await getOrganization({});
const user = await currentUser(); const user = await currentUser();
const activeMember = await getActiveMember(); const activeMember = await getActiveMember();
if (!organization || !activeMember || !user) { if (!organization || !activeMember || !user) {
notFound(); notFound();
} }
const notificationChannels = await getOrganizationChannels(organization.id); const notificationChannels = await getOrganizationChannels(organization.id);
const storageChannels = await getOrganizationStorageChannels(organization.id); const storageChannels = await getOrganizationStorageChannels(organization.id);
const permissions = computeOrganizationPermissions(activeMember); const agents = await getOrganizationAgents(organization.id);
const permissions = computeOrganizationPermissions(activeMember);
const users = await db.query.user.findMany({ const users = await db.query.user.findMany({
where: (fields) => isNull(fields.deletedAt), where: (fields) => isNull(fields.deletedAt),
}); });
const organizationWithMembers = await db.query.organization.findFirst({ const organizationWithMembers = await db.query.organization.findFirst({
where: eq(drizzleDb.schemas.organization.id, organization.id), where: eq(drizzleDb.schemas.organization.id, organization.id),
with: {
members: {
with: { with: {
user: true, projects: true,
members: {
with: {
user: true,
},
},
}, },
}, });
},
});
if (!organizationWithMembers) notFound(); if (!organizationWithMembers) notFound();
return ( return (
<Page> <Page>
<PageHeader> <PageHeader>
<PageTitle className="flex flex-col md:flex-row items-center justify-between w-full "> <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="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 md:justify-between w-full ">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{permissions.canManageSettings && {permissions.canManageSettings &&
organization.slug !== "default" && ( organization.slug !== "default" && (
<EditOrganizationDialog <EditOrganizationDialog
organization={organizationWithMembers} organization={organizationWithMembers}
users={users} users={users}
currentUser={user} currentUser={user}
/> />
)} )}
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{permissions.canManageDangerZone && {permissions.canManageDangerZone &&
organization.slug !== "default" && ( organization.slug !== "default" && (
<DeleteOrganizationButton
organizationSlug={organization.slug} <Tooltip>
/> <TooltipTrigger asChild>
)} <div>
</div> <DeleteOrganizationButton
</div> disabled={organizationWithMembers.projects.length > 0}
</PageTitle> organizationSlug={organization.slug}
</PageHeader> />
<PageContent> </div>
<OrganizationTabs
activeMember={activeMember} </TooltipTrigger>
organization={organization} {organizationWithMembers.projects.length > 0 && (
notificationChannels={notificationChannels} <TooltipContent>
storageChannels={storageChannels} <p>Your organization has some projects associated with it. Please delete them before deleting the organization.</p>
/> </TooltipContent>
</PageContent> )}
</Page> </Tooltip>
);
)}
</div>
</div>
</PageTitle>
</PageHeader>
<PageContent>
<OrganizationTabs
activeMember={activeMember}
organization={organization}
notificationChannels={notificationChannels}
storageChannels={storageChannels}
agents={agents}
/>
</PageContent>
</Page>
);
} }
@@ -1,8 +1,8 @@
import {PageParams} from "@/types/next"; import {PageParams} from "@/types/next";
import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page"; import {Page, PageContent, PageHeader, PageTitle} from "@/features/layout/page";
import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card"; import {Card, CardContent, CardHeader, CardTitle} from "@/components/ui/card";
import {EvolutionLineChart} from "@/components/wrappers/dashboard/statistics/charts/evolution-line-chart"; import {EvolutionLineChart} from "@/features/statistics/evolution-line-chart";
import {PercentageLineChart} from "@/components/wrappers/dashboard/statistics/charts/percentage-line-chart"; import {PercentageLineChart} from "@/features/statistics/percentage-line-chart";
import {notFound} from "next/navigation"; import {notFound} from "next/navigation";
import {db} from "@/db"; import {db} from "@/db";
import {and, asc, count, eq, inArray} from "drizzle-orm"; import {and, asc, count, eq, inArray} from "drizzle-orm";
+2 -2
View File
@@ -5,7 +5,7 @@ import {Building2, Database, DatabaseBackup, Folder, RefreshCcw, Server, Workflo
import {currentUser} from "@/lib/auth/current-user"; import {currentUser} from "@/lib/auth/current-user";
import {notFound} from "next/navigation"; import {notFound} from "next/navigation";
import {db} from "@/db"; import {db} from "@/db";
import {asc, inArray} from "drizzle-orm"; import {and, asc, eq, inArray} from "drizzle-orm";
import * as drizzleDb from "@/db"; import * as drizzleDb from "@/db";
import {listOrganizations} from "@/lib/auth/auth"; import {listOrganizations} from "@/lib/auth/auth";
import {Metadata} from "next"; import {Metadata} from "next";
@@ -26,7 +26,7 @@ export default async function RoutePage(props: PageParams<{}>) {
const agents = await db.query.agent.findMany({}); const agents = await db.query.agent.findMany({});
const projects = await db.query.project.findMany({ const projects = await db.query.project.findMany({
where: inArray(drizzleDb.schemas.project.organizationId, organizationIds), where: and(inArray(drizzleDb.schemas.project.organizationId, organizationIds), eq(drizzleDb.schemas.project.isArchived, false)),
}); });
const projectIds = projects.map(project => project.id); const projectIds = projects.map(project => project.id);
+7 -5
View File
@@ -2,10 +2,12 @@ import { ReactNode } from "react";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar"; import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
import { AppSidebar } from "@/components/wrappers/dashboard/common/sidebar/app-sidebar"; import { AppSidebar } from "@/features/layout/app-sidebar";
import { Header } from "@/features/layout/Header"; import { Header } from "@/features/layout/header";
import { currentUser } from "@/lib/auth/current-user"; import { currentUser } from "@/lib/auth/current-user";
import { ThemeMetaUpdater } from "@/features/browser/theme-meta-updater"; import { ThemeMetaUpdater } from "@/features/theme/theme-meta-updater";
import { ModeToggle } from "@/features/theme/mode-toggle";
import { UpdateNotification } from "@/features/updates/update-notification";
export default async function Layout({ children }: { children: ReactNode }) { export default async function Layout({ children }: { children: ReactNode }) {
const user = await currentUser(); const user = await currentUser();
@@ -15,9 +17,9 @@ export default async function Layout({ children }: { children: ReactNode }) {
<SidebarProvider> <SidebarProvider>
<div className="flex flex-col lg:flex-row w-full"> <div className="flex flex-col lg:flex-row w-full">
<ThemeMetaUpdater /> <ThemeMetaUpdater />
<AppSidebar /> <AppSidebar updateNotification={<UpdateNotification />} />
<SidebarInset> <SidebarInset>
<Header /> <Header actions={<ModeToggle />} />
<main className="h-full">{children}</main> <main className="h-full">{children}</main>
</SidebarInset> </SidebarInset>
</div> </div>
+1 -1
View File
@@ -1,4 +1,4 @@
import { LoadingSpinner } from "@/components/wrappers/common/loading/loading-spinner"; import { LoadingSpinner } from "@/components/common/loading-spinner";
export default function Loading() { export default function Loading() {
return ( return (
+1 -5
View File
@@ -1,5 +1,5 @@
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { getCurrentOrganizationSlug } from "@/features/dashboard/organization-cookie"; import { getCurrentOrganizationSlug } from "@/features/organizations/organization-cookie";
import { currentUser } from "@/lib/auth/current-user"; import { currentUser } from "@/lib/auth/current-user";
export default async function Index() { export default async function Index() {
@@ -9,8 +9,4 @@ export default async function Index() {
redirect(`/dashboard/home`); redirect(`/dashboard/home`);
} }
redirect("/login"); redirect("/login");
//Do not delete
// return (
// <Home/>
// );
} }
+15 -13
View File
@@ -1,7 +1,10 @@
import { NextResponse } from "next/server"; import {NextResponse} from "next/server";
import { eq } from "drizzle-orm"; import {and, eq} from "drizzle-orm";
import { db } from "@/db"; import {db} from "@/db";
import * as drizzleDb 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) { export function withAgentCheck(handler: Function) {
return async (request: Request, context: { params: Promise<{ agentId: string }> }) => { return async (request: Request, context: { params: Promise<{ agentId: string }> }) => {
@@ -9,28 +12,27 @@ export function withAgentCheck(handler: Function) {
const agentId = (await context.params).agentId; const agentId = (await context.params).agentId;
const agent = await db.query.agent.findFirst({ const agent = await db.query.agent.findFirst({
where: eq(drizzleDb.schemas.agent.id, agentId), where: and(eq(drizzleDb.schemas.agent.id, agentId), eq(drizzleDb.schemas.agent.isArchived, false)),
}); });
if (!agent) { if (!agent) {
return NextResponse.json( return NextResponse.json(
{ error: "Agent not found" }, {error: "Agent not found"},
{ status: 404 } {status: 404}
); );
} }
return handler(request, { ...context, agent }); return handler(request, {...context, agent});
} catch (err) { } catch (err) {
console.error("Error in agent middleware:", err); log.error({error: err, name: "withAgentCheck"}, "Error in agent middleware");
return NextResponse.json( return NextResponse.json(
{ error: "Internal server error" }, {error: "Internal server error"},
{ status: 500 } {status: 500}
); );
} }
}; };
} }
export async function getDatabaseOrThrow(generatedId: string) { export async function getDatabaseOrThrow(generatedId: string) {
const database = await db.query.database.findFirst({ const database = await db.query.database.findFirst({
where: eq(drizzleDb.schemas.database.agentDatabaseId, generatedId), where: eq(drizzleDb.schemas.database.agentDatabaseId, generatedId),
@@ -43,8 +45,8 @@ export async function getDatabaseOrThrow(generatedId: string) {
if (!database) { if (!database) {
throw NextResponse.json( throw NextResponse.json(
{ error: "Database associated with generatedId not found" }, {error: "Database associated with generatedId not found"},
{ status: 404 } {status: 404}
); );
} }
+38 -10
View File
@@ -5,9 +5,13 @@ import {db as dbClient, db} from "@/db";
import {getDatabaseOrThrow, withAgentCheck} from "./helpers"; import {getDatabaseOrThrow, withAgentCheck} from "./helpers";
import {Backup} from "@/db/schema/07_database"; import {Backup} from "@/db/schema/07_database";
import {withUpdatedAt} from "@/db/utils"; import {withUpdatedAt} from "@/db/utils";
import {eventEmitter} from "@/features/shared/event"; import {eventEmitter} from "@/lib/event";
import {sendNotificationsBackupRestore} from "@/features/notifications/helpers"; import {sendNotificationsBackupRestore} from "@/features/notifications/notifications.helpers";
import {EventKind} from "@/features/notifications/types"; import {EventKind} from "@/features/notifications/notifications.types";
import {logger} from "@/lib/logger";
import {JobLogEntry} from "@/features/logs/types";
const log = logger.child({module: "api/agent/backup/route"});
export type BodyPost = { export type BodyPost = {
method: "manual" | "automatic" method: "manual" | "automatic"
@@ -19,6 +23,8 @@ export type BodyPatch = {
status: "success" | "failed" status: "success" | "failed"
size: number size: number
generatedId: string generatedId: string
logs: JobLogEntry[]
durationMs: number
} }
export const POST = withAgentCheck(async (request: Request, {params, agent}: { export const POST = withAgentCheck(async (request: Request, {params, agent}: {
@@ -27,6 +33,7 @@ export const POST = withAgentCheck(async (request: Request, {params, agent}: {
}) => { }) => {
try { try {
const body: BodyPost = await request.json(); const body: BodyPost = await request.json();
const method = body.method const method = body.method
const database = await getDatabaseOrThrow(body.generatedId); const database = await getDatabaseOrThrow(body.generatedId);
@@ -77,7 +84,6 @@ export const POST = withAgentCheck(async (request: Request, {params, agent}: {
} }
} }
eventEmitter.emit('modification', {update: true}); eventEmitter.emit('modification', {update: true});
return NextResponse.json( return NextResponse.json(
@@ -88,7 +94,7 @@ export const POST = withAgentCheck(async (request: Request, {params, agent}: {
{status: 200} {status: 200}
); );
} catch (error) { } catch (error) {
console.error("Error in POST for INIT backup:", error); log.error({error: error}, "Error in POST for INIT backup");
return NextResponse.json( return NextResponse.json(
{error: "Internal server error"}, {error: "Internal server error"},
{status: 500} {status: 500}
@@ -102,7 +108,6 @@ export const PATCH = withAgentCheck(async (request: Request, {params, agent}: {
}) => { }) => {
try { try {
const body: BodyPatch = await request.json(); const body: BodyPatch = await request.json();
console.log("body", body);
const status = body.status const status = body.status
const backupId = body.backupId const backupId = body.backupId
@@ -125,12 +130,37 @@ export const PATCH = withAgentCheck(async (request: Request, {params, agent}: {
.update(drizzleDb.schemas.backup) .update(drizzleDb.schemas.backup)
.set(withUpdatedAt({ .set(withUpdatedAt({
status: status, status: status,
fileSize: backupSize fileSize: backupSize,
durationMs: body.durationMs
})) }))
.where(eq(drizzleDb.schemas.backup.id, backup.id)) .where(eq(drizzleDb.schemas.backup.id, backup.id))
.returning(); .returning();
const logsToInsert = body.logs.map((entry) => ({
backupId: backup.id,
restorationId: null,
loggedAt: new Date(entry.timestamp),
entryType: entry.type,
level: entry.level,
message: entry.message,
command: entry.command ?? null,
output: entry.output ?? null,
exitCode: entry.exit_code ?? null,
durationMs: entry.duration_ms ?? null,
}));
if (logsToInsert.length > 0) {
await dbClient
.insert(drizzleDb.schemas.jobLog)
.values(logsToInsert);
}
eventEmitter.emit('modification', {update: true}); eventEmitter.emit('modification', {update: true});
await sendNotificationsBackupRestore(database, status == "failed" ? "error_backup" : "success_backup" as EventKind); await sendNotificationsBackupRestore(database, status == "failed" ? "error_backup" : "success_backup" as EventKind);
@@ -142,9 +172,7 @@ export const PATCH = withAgentCheck(async (request: Request, {params, agent}: {
{status: 200} {status: 200}
); );
} catch (error) { } catch (error) {
log.error({error: error}, "Error in PATCH backup")
console.error("Error in PATCH backup:", error);
return NextResponse.json( return NextResponse.json(
{error: "Internal server error"}, {error: "Internal server error"},
{status: 500} {status: 500}
@@ -4,7 +4,10 @@ import * as drizzleDb from "@/db";
import {db} from "@/db"; import {db} from "@/db";
import {getDatabaseOrThrow, withAgentCheck} from "../../helpers"; import {getDatabaseOrThrow, withAgentCheck} from "../../helpers";
import {isUuidv4} from "@/utils/verify-uuid"; import {isUuidv4} from "@/utils/verify-uuid";
import {eventEmitter} from "@/features/shared/event"; import {eventEmitter} from "@/lib/event";
import {logger} from "@/lib/logger";
const log = logger.child({module: "api/agent/backup/upload/init"});
export type Body = { export type Body = {
generatedId: string generatedId: string
@@ -18,7 +21,7 @@ export const POST = withAgentCheck(async (request: Request, {params, agent}: {
try { try {
const body: Body = await request.json(); const body: Body = await request.json();
console.log("body", body); log.info({data: body}, "Body for backup upload init");
const generatedId = body.generatedId; const generatedId = body.generatedId;
const storageChannelId = body.storageChannelId; const storageChannelId = body.storageChannelId;
@@ -66,7 +69,7 @@ export const POST = withAgentCheck(async (request: Request, {params, agent}: {
{status: 200} {status: 200}
); );
} catch (error) { } catch (error) {
console.error("Error in POST for INIT backup:", error); log.error({error: error}, "Error in POST for INIT backup");
return NextResponse.json( return NextResponse.json(
{error: "Internal server error"}, {error: "Internal server error"},
{status: 500} {status: 500}
@@ -4,7 +4,10 @@ import * as drizzleDb from "@/db";
import {db as dbClient, db} from "@/db"; import {db as dbClient, db} from "@/db";
import {withUpdatedAt} from "@/db/utils"; import {withUpdatedAt} from "@/db/utils";
import {getDatabaseOrThrow, withAgentCheck} from "../../helpers"; import {getDatabaseOrThrow, withAgentCheck} from "../../helpers";
import {eventEmitter} from "@/features/shared/event"; import {eventEmitter} from "@/lib/event";
import {logger} from "@/lib/logger";
const log = logger.child({module: "api/agent/backup/upload/status"});
export type Body = { export type Body = {
generatedId: string generatedId: string
@@ -27,8 +30,7 @@ export const PATCH = withAgentCheck(async (request: Request, {params, agent}: {
const backupStorageId = body.backupStorageId; const backupStorageId = body.backupStorageId;
const backupId = body.backupId; const backupId = body.backupId;
log.info({data: body}, "Body for backup upload status");
console.log("body", body);
const database = await getDatabaseOrThrow(generatedId); const database = await getDatabaseOrThrow(generatedId);
@@ -68,10 +70,10 @@ export const PATCH = withAgentCheck(async (request: Request, {params, agent}: {
if (hasSuccessfulStorage && backup.status !== "success") { if (hasSuccessfulStorage && backup.status !== "success") {
await db await db
.update(drizzleDb.schemas.backup) .update(drizzleDb.schemas.backup)
.set({ .set(withUpdatedAt({
status: "success", status: "success",
fileSize: fileSize, fileSize: fileSize,
}) }))
.where(eq(drizzleDb.schemas.backup.id, backup.id)); .where(eq(drizzleDb.schemas.backup.id, backup.id));
} }
} }
@@ -85,7 +87,7 @@ export const PATCH = withAgentCheck(async (request: Request, {params, agent}: {
{status: 200} {status: 200}
); );
} catch (error) { } catch (error) {
console.error("Error in POST for INIT backup:", error); log.error({error: error},"Error in POST for INIT backup");
return NextResponse.json( return NextResponse.json(
{error: "Internal server error"}, {error: "Internal server error"},
{status: 500} {status: 500}
+40 -12
View File
@@ -1,14 +1,20 @@
import {NextResponse} from "next/server"; import {NextResponse} from "next/server";
import {isUuidv4} from "@/utils/verify-uuid"; import {isUuidv4} from "@/utils/verify-uuid";
import * as drizzleDb from "@/db"; import * as drizzleDb from "@/db";
import {db} from "@/db"; import {db as dbClient, db} from "@/db";
import {and, eq} from "drizzle-orm"; import {and, eq} from "drizzle-orm";
import {sendNotificationsBackupRestore} from "@/features/notifications/helpers"; import {sendNotificationsBackupRestore} from "@/features/notifications/notifications.helpers";
import {eventEmitter} from "@/features/shared/event"; import {logger} from "@/lib/logger";
import {withUpdatedAt} from "@/db/utils";
import {JobLogEntry} from "@/features/logs/types";
const log = logger.child({module: "api/agent/restore"});
export type BodyResultRestore = { export type BodyResultRestore = {
generatedId: string generatedId: string
status: string status: string
logs: JobLogEntry[]
durationMs: number
} }
type RestorationStatus = 'waiting' | 'ongoing' | 'failed' | 'success'; type RestorationStatus = 'waiting' | 'ongoing' | 'failed' | 'success';
@@ -32,7 +38,7 @@ export async function POST(
} }
const agent = await db.query.agent.findFirst({ const agent = await db.query.agent.findFirst({
where: eq(drizzleDb.schemas.agent.id, agentId) where: and(eq(drizzleDb.schemas.agent.id, agentId), eq(drizzleDb.schemas.agent.isArchived, false)),
}) })
if (!agent) { if (!agent) {
return NextResponse.json({error: "Agent not found"}, {status: 404}) return NextResponse.json({error: "Agent not found"}, {status: 404})
@@ -57,23 +63,45 @@ export async function POST(
return NextResponse.json({error: "Unable to fin the corresponding restoration"}, {status: 404}) return NextResponse.json({error: "Unable to fin the corresponding restoration"}, {status: 404})
} }
const [restorationUpdated] = await db
await db
.update(drizzleDb.schemas.restoration) .update(drizzleDb.schemas.restoration)
.set({status: body.status as RestorationStatus}) .set(withUpdatedAt({status: body.status as RestorationStatus, durationMs: body.durationMs}))
.where(eq(drizzleDb.schemas.restoration.id, restoration.id)); .where(eq(drizzleDb.schemas.restoration.id, restoration.id)).returning();
const logsToInsert = body.logs.map((entry) => ({
backupId: null,
restorationId: restorationUpdated.id,
loggedAt: new Date(entry.timestamp),
entryType: entry.type,
level: entry.level,
message: entry.message,
command: entry.command ?? null,
output: entry.output ?? null,
exitCode: entry.exit_code ?? null,
durationMs: entry.duration_ms ?? null,
}));
if (logsToInsert.length > 0) {
await dbClient
.insert(drizzleDb.schemas.jobLog)
.values(logsToInsert);
}
await sendNotificationsBackupRestore(database, body.status == "failed" ? "error_restore" : "success_restore"); await sendNotificationsBackupRestore(database, body.status == "failed" ? "error_restore" : "success_restore");
const response = { const response = {
message: true, status: true,
details: "Restoration successfully updated" message: "Restoration successfully updated"
} }
return Response.json(response, {status: 200}) return Response.json(response, {status: 200})
} catch (error) { } catch (error) {
console.error('Error in POST handler:', error); log.error({error: error}, "Error in POST handler")
return NextResponse.json( return NextResponse.json(
{error: 'Internal server error'}, {error: 'Internal server error'},
{status: 500} {status: 500}
+46 -16
View File
@@ -2,20 +2,23 @@ import {NextResponse} from "next/server";
import {Body} from "./route"; import {Body} from "./route";
import {isUuidv4} from "@/utils/verify-uuid"; import {isUuidv4} from "@/utils/verify-uuid";
import {Agent} from "@/db/schema/08_agent"; 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 * as drizzleDb from "@/db";
import {db, db as dbClient} from "@/db"; import {db, db as dbClient} from "@/db";
import {and, eq, inArray} from "drizzle-orm"; import {and, eq, inArray} from "drizzle-orm";
import {dbmsEnumSchema, EDbmsSchema} from "@/db/schema/types"; import {dbmsEnumSchema, EDbmsSchema} from "@/db/schema/types";
import {withUpdatedAt} from "@/db/utils"; import {withUpdatedAt} from "@/db/utils";
import type {StorageInput} from "@/features/storages/types"; import type {StorageInput} from "@/features/storages/storages.types";
import {dispatchStorage} from "@/features/storages/dispatch"; import {dispatchStorage} from "@/features/storages/storages.dispatch";
import {Setting} from "@/db/schema/01_setting"; 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) { export async function handleDatabases(body: Body, agent: Agent, lastContact: Date, settings: Setting) {
const databasesResponse = []; const databasesResponse = [];
const formatDatabase = (database: DatabaseWith, backupAction: boolean, restoreAction: boolean, UrlBackup: string | null, storages: PingDatabaseStorageChannels[], urlMeta: string | null) => ({ const formatDatabase = (database: DatabaseWith, backupAction: boolean, restoreAction: boolean, UrlBackup: string | null, storages: PingDatabaseStorageChannels[], urlMeta: string | null, backupSize: number | null) => ({
generatedId: database.agentDatabaseId, generatedId: database.agentDatabaseId,
dbms: database.dbms, dbms: database.dbms,
storages: storages, storages: storages,
@@ -28,7 +31,8 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
restore: { restore: {
action: restoreAction, action: restoreAction,
file: UrlBackup, file: UrlBackup,
metaFile: urlMeta metaFile: urlMeta,
size: backupSize
}, },
}, },
}); });
@@ -46,6 +50,7 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
let restoreAction: boolean = false let restoreAction: boolean = false
let urlBackup: string | null = null; let urlBackup: string | null = null;
let urlMeta: string | null = null let urlMeta: string | null = null
let backupSize: number | null = null
if (!existingDatabase) { if (!existingDatabase) {
if (!isUuidv4(db.generatedId)) { if (!isUuidv4(db.generatedId)) {
@@ -56,7 +61,7 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
} }
if (!dbmsEnumSchema.safeParse(db.dbms).success) { 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; continue;
} }
@@ -67,15 +72,27 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
name: db.name, name: db.name,
dbms: db.dbms as EDbmsSchema, dbms: db.dbms as EDbmsSchema,
agentDatabaseId: db.generatedId, agentDatabaseId: db.generatedId,
lastContact: lastContact, lastContact: db.pingStatus ? lastContact : null,
healthErrorCount: null
}) })
.returning(); .returning();
if (databaseCreated) { 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) const storages = await getDatabaseStorageChannels(databaseCreated.id)
databasesResponse.push(formatDatabase(databaseCreated, backupAction, restoreAction, urlBackup, storages, null)); databasesResponse.push(formatDatabase(databaseCreated, backupAction, restoreAction, urlBackup, storages, null, null));
} }
} else { } else {
@@ -84,11 +101,24 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
.set(withUpdatedAt({ .set(withUpdatedAt({
name: db.name, name: db.name,
agentId: agent.id, 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)) .where(eq(drizzleDb.schemas.database.id, existingDatabase.id))
.returning(); .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({ const activeBackup = await dbClient.query.backup.findFirst({
where: and( where: and(
eq(drizzleDb.schemas.backup.databaseId, databaseUpdated.id), eq(drizzleDb.schemas.backup.databaseId, databaseUpdated.id),
@@ -152,35 +182,35 @@ export async function handleDatabases(body: Body, agent: Agent, lastContact: Dat
if (result.success) { if (result.success) {
urlBackup = result.url ?? null; urlBackup = result.url ?? null;
urlMeta = resultMeta.url ?? null urlMeta = resultMeta.url ?? null
backupSize = restoration.backupStorage.size
} else { } else {
await dbClient await dbClient
.update(drizzleDb.schemas.restoration) .update(drizzleDb.schemas.restoration)
.set({status: "failed"}) .set(withUpdatedAt({status: "failed"}))
.where(eq(drizzleDb.schemas.restoration.id, restoration.id)); .where(eq(drizzleDb.schemas.restoration.id, restoration.id));
const errorMessage = "Failed to get backup URL"; const errorMessage = "Failed to get backup URL";
console.error("Restoration failed: ", errorMessage); log.error({error: errorMessage, name: "handleDatabases"}, "Restoration failed");
continue; continue;
} }
} catch (err) { } catch (err) {
console.error("Restoration crashed unexpectedly:", err); log.error({error: err, name: "handleDatabases"}, "Restoration crashed unexpectedly");
await dbClient await dbClient
.update(drizzleDb.schemas.restoration) .update(drizzleDb.schemas.restoration)
.set({status: "failed"}) .set(withUpdatedAt({status: "failed"}))
.where(eq(drizzleDb.schemas.restoration.id, restoration.id)); .where(eq(drizzleDb.schemas.restoration.id, restoration.id));
continue; continue;
} }
await dbClient await dbClient
.update(drizzleDb.schemas.restoration) .update(drizzleDb.schemas.restoration)
.set({status: "ongoing"}) .set(withUpdatedAt({status: "ongoing"}))
.where(eq(drizzleDb.schemas.restoration.id, restoration.id)); .where(eq(drizzleDb.schemas.restoration.id, restoration.id));
} }
const storages = await getDatabaseStorageChannels(databaseUpdated.id) const storages = await getDatabaseStorageChannels(databaseUpdated.id)
databasesResponse.push(formatDatabase(databaseUpdated, backupAction, restoreAction, urlBackup, storages, urlMeta)); databasesResponse.push(formatDatabase(databaseUpdated, backupAction, restoreAction, urlBackup, storages, urlMeta, backupSize));
} }
} }
return databasesResponse; return databasesResponse;
} }
+21 -10
View File
@@ -3,16 +3,20 @@ import {handleDatabases} from "./helpers";
import * as drizzleDb from "@/db"; import * as drizzleDb from "@/db";
import {db} from "@/db"; import {db} from "@/db";
import {EDbmsSchema} from "@/db/schema/types"; import {EDbmsSchema} from "@/db/schema/types";
import {eq} from "drizzle-orm"; import {and, eq} from "drizzle-orm";
import {isUuidv4} from "@/utils/verify-uuid"; import {isUuidv4} from "@/utils/verify-uuid";
import {withUpdatedAt} from "@/db/utils"; import {withUpdatedAt} from "@/db/utils";
import {eventEmitter} from "@/features/shared/event"; import {logger} from "@/lib/logger";
import {notFound} from "next/navigation";
const log = logger.child({module: "api/agent/status/route"});
export type databaseAgent = { export type databaseAgent = {
name: string, name: string,
dbms: EDbmsSchema, dbms: EDbmsSchema,
generatedId: string generatedId: string
pingStatus: boolean
} }
export type Body = { export type Body = {
@@ -27,14 +31,14 @@ export async function POST(
) { ) {
try { try {
const agentId = (await params).agentId const agentId = (await params).agentId
console.log(agentId) log.debug(`Agent ID: ${agentId}`)
const body: Body = await request.json(); const body: Body = await request.json();
const lastContact = new Date(); const lastContact = new Date();
let message: string let message: string
if (!isUuidv4(agentId)) { if (!isUuidv4(agentId)) {
message = "agentId is not a valid uuid" message = "agentId is not a valid uuid"
console.error(message) log.error({error: message}, "An error occurred")
return NextResponse.json( return NextResponse.json(
{error: "agentId is not a valid uuid"}, {error: "agentId is not a valid uuid"},
{status: 500} {status: 500}
@@ -42,7 +46,7 @@ export async function POST(
} }
const agent = await db.query.agent.findFirst({ const agent = await db.query.agent.findFirst({
where: eq(drizzleDb.schemas.agent.id, agentId), where: and(eq(drizzleDb.schemas.agent.id, agentId), eq(drizzleDb.schemas.agent.isArchived, false)),
}) })
if (!agent) { if (!agent) {
@@ -55,17 +59,25 @@ export async function POST(
return NextResponse.json({error: "An error occured"}, {status: 404}) return NextResponse.json({error: "An error occured"}, {status: 404})
} }
const databasesResponse = await handleDatabases(body, agent, lastContact, settings) const databasesResponse = await handleDatabases(body, agent, lastContact, settings)
await db await db
.update(drizzleDb.schemas.agent) .update(drizzleDb.schemas.agent)
.set(withUpdatedAt({ .set(withUpdatedAt({
version: body.version, version: body.version,
lastContact: lastContact lastContact: lastContact,
healthErrorCount: null
})) }))
.where(eq(drizzleDb.schemas.agent.id, agentId)); .where(eq(drizzleDb.schemas.agent.id, agentId));
await db
.insert(drizzleDb.schemas.healthcheckLog)
.values({
kind: "agent",
status: "success",
objectId: agentId,
date: lastContact
})
const response = { const response = {
agent: { agent: {
@@ -75,10 +87,9 @@ export async function POST(
databases: databasesResponse databases: databasesResponse
} }
return Response.json(response) return Response.json(response)
} catch (error) { } catch (error) {
console.error('Error in POST handler:', error); log.error({error: error}, "Error in POST handler")
return NextResponse.json( return NextResponse.json(
{error: 'Internal server error'}, {error: 'Internal server error'},
{status: 500} {status: 500}
+32 -1
View File
@@ -1,4 +1,35 @@
import { auth } from "@/lib/auth/auth"; import { auth } from "@/lib/auth/auth";
import { toNextJsHandler } from "better-auth/next-js"; import { toNextJsHandler } from "better-auth/next-js";
import { NextRequest, NextResponse } from "next/server";
import { headers } from "next/headers";
export const { GET, POST } = toNextJsHandler(auth.handler); const authHandler = toNextJsHandler(auth.handler);
async function blockApiKeyCreateForRestrictedUsers(req: NextRequest): Promise<NextResponse | null> {
const url = req.nextUrl;
if (req.method !== "POST" || !url.pathname.endsWith("/api-key/create")) {
return null;
}
const session = await auth.api.getSession({ headers: await headers() });
if (!session?.user) {
return null;
}
// @ts-ignore
if (session.user.banned || (session.user.role as string) === "pending") {
return NextResponse.json(
{ error: "Account not eligible to create API keys" },
{ status: 403 }
);
}
return null;
}
export async function GET(req: NextRequest) {
return authHandler.GET(req);
}
export async function POST(req: NextRequest) {
const guard = await blockApiKeyCreateForRestrictedUsers(req);
if (guard) return guard;
return authHandler.POST(req);
}
+1 -1
View File
@@ -4,6 +4,6 @@ export async function GET() {
return NextResponse.json({ return NextResponse.json({
PROJECT_URL: process.env.PROJECT_URL, PROJECT_URL: process.env.PROJECT_URL,
PROJECT_NAME: process.env.PROJECT_NAME, PROJECT_NAME: process.env.PROJECT_NAME,
PROJECT_DESCRIPTION: process.env.PROJECT_DESCRIPTION, PROJECT_DESCRIPTION: process.env.PROJECT_DESCRIPTION
}); });
} }
+7 -4
View File
@@ -1,7 +1,10 @@
import {auth} from "@/lib/auth/auth"; import {auth} from "@/lib/auth/auth";
import {headers} from "next/headers"; import {headers} from "next/headers";
import {NextResponse} from "next/server"; import {NextResponse} from "next/server";
import {eventEmitter} from "@/features/shared/event"; import {eventEmitter} from "@/lib/event";
import {logger} from "@/lib/logger";
const log = logger.child({module: "api/events"});
export async function GET(request: Request) { export async function GET(request: Request) {
@@ -16,9 +19,9 @@ export async function GET(request: Request) {
return new Response( return new Response(
new ReadableStream({ new ReadableStream({
start(controller) { start(controller) {
console.log('Stream started'); log.info("Stream started");
const handleModification = (data: any) => { const handleModification = (data: any) => {
console.log('Modification event triggered:', data); log.info({data: data},"Modification event triggered");
controller.enqueue(`event: modification\n`); controller.enqueue(`event: modification\n`);
controller.enqueue(`data: ${JSON.stringify(data)}\n\n`); controller.enqueue(`data: ${JSON.stringify(data)}\n\n`);
}; };
@@ -26,7 +29,7 @@ export async function GET(request: Request) {
eventEmitter.on('modification', handleModification); eventEmitter.on('modification', handleModification);
request.signal.addEventListener('abort', () => { request.signal.addEventListener('abort', () => {
console.log('Client disconnected'); log.info("Client disconnected");
controller.close(); controller.close();
eventEmitter.off('modification', handleModification); eventEmitter.off('modification', handleModification);
}); });
+6 -3
View File
@@ -1,8 +1,11 @@
import {NextResponse} from "next/server"; import {NextResponse} from "next/server";
import path from "path"; import path from "path";
import type {StorageInput} from "@/features/storages/types"; import type {StorageInput} from "@/features/storages/storages.types";
import {dispatchStorage} from "@/features/storages/dispatch"; import {dispatchStorage} from "@/features/storages/storages.dispatch";
import {Readable} from "node:stream"; import {Readable} from "node:stream";
import {logger} from "@/lib/logger";
const log = logger.child({module: "api/files/backups"});
export async function GET( export async function GET(
request: Request, 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); const result = await dispatchStorage(input, undefined, storageId);
+7 -7
View File
@@ -1,12 +1,12 @@
import {NextResponse} from "next/server"; import {NextResponse} from "next/server";
import {auth} from "@/lib/auth/auth"; import {auth} from "@/lib/auth/auth";
import {headers} from "next/headers"; import {headers} from "next/headers";
import {db} from "@/db"; import {StorageInput} from "@/features/storages/storages.types";
import * as drizzleDb from "@/db"; import {dispatchStorage} from "@/features/storages/storages.dispatch";
import {eq} from "drizzle-orm";
import {StorageInput} from "@/features/storages/types";
import {dispatchStorage} from "@/features/storages/dispatch";
import {Readable} from "node:stream"; import {Readable} from "node:stream";
import {logger} from "@/lib/logger";
const log = logger.child({module: "api/files/images"});
export async function GET( export async function GET(
req: Request, req: Request,
@@ -56,7 +56,7 @@ export async function GET(
const result = await dispatchStorage(input, undefined, storageId); const result = await dispatchStorage(input, undefined, storageId);
if (!result.file || !(result.file instanceof Readable)) { 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( return NextResponse.json(
{error: "Invalid file payload"}, {error: "Invalid file payload"},
{status: 500} {status: 500}
@@ -82,7 +82,7 @@ export async function GET(
}); });
} catch (err) { } catch (err) {
console.error("Error streaming image:", err); log.error({error: err}, `Error streaming image`);
return NextResponse.json({error: "Error fetching file"}, {status: 500}); return NextResponse.json({error: "Error fetching file"}, {status: 500});
} }
} }
+5
View File
@@ -0,0 +1,5 @@
import { NextResponse } from "next/server";
export async function GET() {
return NextResponse.json({ success: true });
}
+5 -5
View File
@@ -2,7 +2,9 @@ import {NextResponse} from "next/server";
import fs from "fs"; import fs from "fs";
import path from "path"; import path from "path";
import {env} from "@/env.mjs"; import {env} from "@/env.mjs";
import {logger} from "@/lib/logger";
const log = logger.child({module: "api/tus/hooks"});
export async function POST(request: Request) { export async function POST(request: Request) {
try { try {
@@ -13,7 +15,7 @@ export async function POST(request: Request) {
const uploadOffset = headers["Upload-Offset"]?.[0]; const uploadOffset = headers["Upload-Offset"]?.[0];
const status = headers["X-Status"]?.[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 (status === "success") {
if ( if (
@@ -29,9 +31,7 @@ export async function POST(request: Request) {
return NextResponse.json({error: "Missing X-File-Path"}, {status: 500}); return NextResponse.json({error: "Missing X-File-Path"}, {status: 500});
} }
const uploadDir = path.join(env.PRIVATE_PATH!, "/uploads/");
// const uploadDir = path.join(process.cwd(), "/private/uploads/");
const uploadDir = path.join(env.PRIVATE_PATH, "/uploads/");
const oldFilePath = path.join(uploadDir, "tmp", id); const oldFilePath = path.join(uploadDir, "tmp", id);
const newFilePath = path.join(uploadDir, filePath); const newFilePath = path.join(uploadDir, filePath);
@@ -74,7 +74,7 @@ export async function POST(request: Request) {
} }
return NextResponse.json({}); return NextResponse.json({});
} catch (error) { } catch (error) {
console.error("Hook error:", error); log.error({error: error},"TUS Hook error");
return NextResponse.json({error: "Internal server error"}, {status: 500}); return NextResponse.json({error: "Internal server error"}, {status: 500});
} }
} }
+34
View File
@@ -0,0 +1,34 @@
import {withApiKey} from "@/lib/api-v1/middleware";
import {ApiKeyContext} from "@/lib/api-v1/types";
import {NextResponse} from "next/server";
import {getAgent, resolveAgentAccess} from "@/lib/api-v1/services/agents";
import {logger} from "@/lib/logger";
import {generateEdgeKey} from "@/utils/edge_key";
import {getServerUrl} from "@/utils/get-server-url";
const log = logger.child({ module: "api/v1/agents/[id]/key" });
export const GET = withApiKey(
async (_req: Request, ctx: ApiKeyContext, params?: Record<string, string>) => {
try {
const id = params?.id;
if (!id) return NextResponse.json({ error: "Not found" }, { status: 404 });
const access = await resolveAgentAccess(id, ctx.user);
if (access === "forbidden") return NextResponse.json({ error: "Forbidden" }, { status: 403 });
if (access === "not_found") return NextResponse.json({ error: "Not found" }, { status: 404 });
const agent = await getAgent(id);
if (!agent) return NextResponse.json({ error: "Agent not found" }, { status: 404 });
const edgeKey = await generateEdgeKey(getServerUrl(), agent.id);
return NextResponse.json({ data: edgeKey });
} catch (error) {
log.error({ error }, "Error in GET /api/v1/agents/[id]");
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
}
}
);
+77
View File
@@ -0,0 +1,77 @@
import {NextResponse} from "next/server";
import {withApiKey} from "@/lib/api-v1/middleware";
import {logger} from "@/lib/logger";
import {ApiKeyContext} from "@/lib/api-v1/types";
import {getAgent, resolveAgentAccess} from "@/lib/api-v1/services/agents";
import {deleteAgentService} from "@/features/agents/agent-delete.action";
const log = logger.child({module: "api/v1/agents/[id]"});
export const GET = withApiKey(
async (_req: Request, ctx: ApiKeyContext, params?: Record<string, string>) => {
try {
const id = params?.id;
if (!id) return NextResponse.json({error: "Not found"}, {status: 404});
const access = await resolveAgentAccess(id, ctx.user);
if (access === "forbidden") return NextResponse.json({error: "Forbidden"}, {status: 403});
if (access === "not_found") return NextResponse.json({error: "Not found"}, {status: 404});
const agent = await getAgent(id, {
includeDatabases: true,
includeOrganizations: false,
});
if (!agent) return NextResponse.json({error: "Not found"}, {status: 404});
return NextResponse.json({data: agent});
} catch (error) {
log.error({error}, "Error in GET /api/v1/agents/[id]");
return NextResponse.json({error: "Internal server error"}, {status: 500});
}
}
);
export const DELETE = withApiKey(
async (_req: Request, ctx: ApiKeyContext, params?: Record<string, string>) => {
try {
const id = params?.id;
if (!id) return NextResponse.json({error: "Not found"}, {status: 404});
const access = await resolveAgentAccess(id, ctx.user);
if (access === "forbidden") return NextResponse.json({error: "Forbidden"}, {status: 403});
if (access === "not_found") return NextResponse.json({error: "Not found"}, {status: 404});
const agent = await getAgent(id, {
includeDatabases: false,
includeOrganizations: true,
});
if (!agent) return NextResponse.json({error: "Agent no found"}, {status: 404});
const organizationIds = agent.organizations.map(org => org.organizationId)
const canDeleteGlobalAgent =
ctx.user.permissions.isAdmin || ctx.user.permissions.isSuperAdmin;
if (!agent.organizationId && !canDeleteGlobalAgent) {
return NextResponse.json(
{ error: "Forbidden" },
{ status: 403 }
);
}
await deleteAgentService({
agentId: agent.id,
organizationId: agent.organizationId ?? undefined,
organizationIds: organizationIds
});
return new Response(null, {status: 204});
} catch (error) {
log.error({error}, "Error in DELETE /api/v1/agents/[id]");
return NextResponse.json({error: "Internal server error"}, {status: 500});
}
}
);
+112
View File
@@ -0,0 +1,112 @@
import { NextResponse } from "next/server";
import { withApiKey } from "@/lib/api-v1/middleware";
import { db } from "@/db";
import * as drizzleDb from "@/db";
import { inArray, eq, and, or, isNull } from "drizzle-orm";
import { z } from "zod";
import { logger } from "@/lib/logger";
import {createAgentService} from "@/features/agents/agents.action";
import { ActionError } from "@/lib/safe-actions/actions";
import {getAccessibleAgentIds} from "@/lib/api-v1/services/agents";
import {ApiKeyContext} from "@/lib/api-v1/types";
import {parseJsonBody} from "@/lib/api-v1/validation/json-body";
const log = logger.child({ module: "api/v1/agents" });
export const GET = withApiKey(async (_req: Request, ctx: ApiKeyContext) => {
try {
const agentIds = await getAccessibleAgentIds(ctx.user);
if (agentIds.length === 0) {
return NextResponse.json({ data: [] });
}
const agents = await db.query.agent.findMany({
where: and(
inArray(drizzleDb.schemas.agent.id, agentIds),
or(
eq(drizzleDb.schemas.agent.isArchived, false),
isNull(drizzleDb.schemas.agent.isArchived)
)
),
});
return NextResponse.json({ data: agents });
} catch (error) {
log.error({ error }, "Error in GET /api/v1/agents");
return NextResponse.json({ error: "Internal server error" }, { status: 500 });
}
});
const CreateAgentSchema = z.object({
name: z.string().min(1, "name is required"),
organizationId: z.string().uuid("organizationId must be a valid UUID").optional(),
});
export const POST = withApiKey(
async (req: Request, ctx: ApiKeyContext) => {
try {
const body = await parseJsonBody(req, CreateAgentSchema);
if (!body.ok) {
return body.response;
}
const { name, organizationId } = body.data;
const org = ctx.organizations.find(
(org) => org.id === organizationId
);
if (
organizationId &&
(!org || !org.permissions.canManageAgents)
) {
return NextResponse.json(
{ error: "Forbidden" },
{ status: 403 }
);
}
const canCreateGlobalAgent =
ctx.user.permissions.isAdmin || ctx.user.permissions.isSuperAdmin;
if (!organizationId && !canCreateGlobalAgent) {
return NextResponse.json(
{ error: "Forbidden" },
{ status: 403 }
);
}
const createdAgent = await createAgentService({
organizationId,
data: {
name,
description: "",
},
});
return NextResponse.json(
{ data: createdAgent },
{ status: 201 }
);
} catch (error: unknown) {
if (error instanceof ActionError) {
return NextResponse.json(
{ error: error.message },
{ status: 400 }
);
}
log.error({error},
"Error in POST /api/v1/agents"
);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
);
@@ -0,0 +1,61 @@
import { NextResponse } from "next/server";
import { withApiKey } from "@/lib/api-v1/middleware";
import { db } from "@/db";
import * as drizzleDb from "@/db";
import { and, eq, isNull } from "drizzle-orm";
import { logger } from "@/lib/logger";
import { ApiKeyContext } from "@/lib/api-v1/types";
import {requireDatabaseAccess} from "@/lib/api-v1/services/databases";
const log = logger.child({
module: "api/v1/databases/[id]/backup/[backupId]",
});
export const GET = withApiKey(
async (_req: Request, ctx: ApiKeyContext, params?: Record<string, string>) => {
try {
const guard = await requireDatabaseAccess(params, ctx.user);
if (!guard.ok) {
return guard.response;
}
const { id } = guard.data;
const backupId = params?.backupId;
if (!backupId) {
return NextResponse.json({ error: "Not found" }, { status: 404 });
}
const backup = await db.query.backup.findFirst({
where: and(
eq(drizzleDb.schemas.backup.id, backupId),
eq(drizzleDb.schemas.backup.databaseId, id),
isNull(drizzleDb.schemas.backup.deletedAt)
),
with: {
storages: {
where: (backupStorage, { isNull }) =>
isNull(backupStorage.deletedAt),
},
},
});
if (!backup) {
return NextResponse.json({ error: "Not found" }, { status: 404 });
}
return NextResponse.json({ data: backup });
} catch (error) {
log.error(
{ error },
"Error in GET /api/v1/databases/[id]/backup/[backupId]"
);
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
);
+96
View File
@@ -0,0 +1,96 @@
import { NextResponse } from "next/server";
import { withApiKey } from "@/lib/api-v1/middleware";
import { db } from "@/db";
import * as drizzleDb from "@/db";
import {and, desc, eq, inArray, isNull} from "drizzle-orm";
import { logger } from "@/lib/logger";
import { ApiKeyContext } from "@/lib/api-v1/types";
import {requireDatabaseAccess} from "@/lib/api-v1/services/databases";
const log = logger.child({ module: "api/v1/databases/[id]/backup" });
export const GET = withApiKey(
async (_req: Request, ctx: ApiKeyContext, params?: Record<string, string>) => {
try {
const guard = await requireDatabaseAccess(params, ctx.user);
if (!guard.ok) {
return guard.response;
}
const { id } = guard.data;
const backups = await db.query.backup.findMany({
where: and(
eq(drizzleDb.schemas.backup.databaseId, id),
isNull(drizzleDb.schemas.backup.deletedAt)
),
orderBy: [desc(drizzleDb.schemas.backup.createdAt)],
});
return NextResponse.json({ data: backups });
} catch (error) {
log.error({ error }, "Error in GET /api/v1/databases/[id]/backup");
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
);
export const POST = withApiKey(
async (_req: Request, ctx: ApiKeyContext, params?: Record<string, string>) => {
try {
const guard = await requireDatabaseAccess(params, ctx.user);
if (!guard.ok) {
return guard.response;
}
const { id } = guard.data;
const existingBackup = await db.query.backup.findFirst({
where: and(
eq(drizzleDb.schemas.backup.databaseId, id),
inArray(drizzleDb.schemas.backup.status, ["waiting", "ongoing"])
),
});
if (existingBackup) {
return NextResponse.json(
{
error:
"A backup is already waiting or ongoing for this database",
},
{ status: 409 }
);
}
const [createdBackup] = await db
.insert(drizzleDb.schemas.backup)
.values({
databaseId: id,
status: "waiting",
})
.returning();
if (!createdBackup) {
return NextResponse.json(
{ error: "Failed to create backup" },
{ status: 500 }
);
}
return NextResponse.json({ data: createdBackup }, { status: 201 });
} catch (error) {
log.error({ error }, "Error in POST /api/v1/databases/[id]/backup");
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
);
+122
View File
@@ -0,0 +1,122 @@
import { NextResponse } from "next/server";
import { withApiKey } from "@/lib/api-v1/middleware";
import { db } from "@/db";
import * as drizzleDb from "@/db";
import {and, eq, inArray, isNull} from "drizzle-orm";
import { z } from "zod";
import { logger } from "@/lib/logger";
import { ApiKeyContext } from "@/lib/api-v1/types";
import { parseJsonBody } from "@/lib/api-v1/validation/json-body";
import {requireDatabaseAccess} from "@/lib/api-v1/services/databases";
const log = logger.child({ module: "api/v1/databases/[id]/restore" });
const RestoreSchema = z.object({
backupId: z.string().uuid("backupId must be a valid UUID"),
backupStorageId: z.string().uuid("backupStorageId must be a valid UUID"),
});
export const POST = withApiKey(
async (req: Request, ctx: ApiKeyContext, params?: Record<string, string>) => {
try {
const guard = await requireDatabaseAccess(params, ctx.user);
if (!guard.ok) {
return guard.response;
}
const { id } = guard.data;
const body = await parseJsonBody(req, RestoreSchema);
if (!body.ok) {
return body.response;
}
const { backupId, backupStorageId } = body.data;
const backupRecord = await db.query.backup.findFirst({
where: and(
eq(drizzleDb.schemas.backup.id, backupId),
eq(drizzleDb.schemas.backup.databaseId, id),
isNull(drizzleDb.schemas.backup.deletedAt)
),
columns: {
id: true,
},
});
if (!backupRecord) {
return NextResponse.json(
{ error: "Backup not found for this database" },
{ status: 404 }
);
}
const backupStorage = await db.query.backupStorage.findFirst({
where: and(
eq(drizzleDb.schemas.backupStorage.id, backupStorageId),
eq(drizzleDb.schemas.backupStorage.backupId, backupId),
isNull(drizzleDb.schemas.backupStorage.deletedAt)
),
});
if (!backupStorage) {
return NextResponse.json(
{ error: "Backup storage not found" },
{ status: 404 }
);
}
if (backupStorage.status !== "success") {
return NextResponse.json(
{ error: "Backup storage is not in a successful state" },
{ status: 422 }
);
}
const existingRestorationBackup = await db.query.restoration.findFirst({
where: and(
eq(drizzleDb.schemas.restoration.databaseId, id),
inArray(drizzleDb.schemas.restoration.status, ["waiting", "ongoing"])
),
});
if (existingRestorationBackup) {
return NextResponse.json(
{
error:
"A restoration is already waiting or ongoing for this database",
},
{ status: 409 }
);
}
const [restoration] = await db
.insert(drizzleDb.schemas.restoration)
.values({
databaseId: id,
backupId,
backupStorageId,
status: "waiting",
})
.returning();
if (!restoration) {
return NextResponse.json(
{ error: "Failed to create restoration" },
{ status: 500 }
);
}
return NextResponse.json({ data: restoration }, { status: 201 });
} catch (error) {
log.error({ error }, "Error in POST /api/v1/databases/[id]/restore");
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
);
+44
View File
@@ -0,0 +1,44 @@
import { NextResponse } from "next/server";
import { withApiKey } from "@/lib/api-v1/middleware";
import { db } from "@/db";
import * as drizzleDb from "@/db";
import { and, eq, isNull } from "drizzle-orm";
import { logger } from "@/lib/logger";
import { ApiKeyContext } from "@/lib/api-v1/types";
import {requireDatabaseAccess} from "@/lib/api-v1/services/databases";
const log = logger.child({ module: "api/v1/databases/[id]" });
export const GET = withApiKey(
async (_req: Request, ctx: ApiKeyContext, params?: Record<string, string>) => {
try {
const guard = await requireDatabaseAccess(params, ctx.user);
if (!guard.ok) {
return guard.response;
}
const { id } = guard.data;
const database = await db.query.database.findFirst({
where: and(
eq(drizzleDb.schemas.database.id, id),
isNull(drizzleDb.schemas.database.deletedAt)
),
});
if (!database) {
return NextResponse.json({ error: "Not found" }, { status: 404 });
}
return NextResponse.json({ data: database });
} catch (error) {
log.error({ error }, "Error in GET /api/v1/databases/[id]");
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
);
+72
View File
@@ -0,0 +1,72 @@
import { NextResponse } from "next/server";
import { withApiKey } from "@/lib/api-v1/middleware";
import { db } from "@/db";
import * as drizzleDb from "@/db";
import { and, desc, eq, isNull } from "drizzle-orm";
import { logger } from "@/lib/logger";
import { ApiKeyContext } from "@/lib/api-v1/types";
import {requireDatabaseAccess} from "@/lib/api-v1/services/databases";
const log = logger.child({ module: "api/v1/databases/[id]/status" });
export const GET = withApiKey(
async (_req: Request, ctx: ApiKeyContext, params?: Record<string, string>) => {
try {
const guard = await requireDatabaseAccess(params, ctx.user);
if (!guard.ok) {
return guard.response;
}
const { id } = guard.data;
const [database, latestBackup, latestRestoration] = await Promise.all([
db.query.database.findFirst({
where: and(
eq(drizzleDb.schemas.database.id, id),
isNull(drizzleDb.schemas.database.deletedAt)
),
}),
db.query.backup.findFirst({
where: and(
eq(drizzleDb.schemas.backup.databaseId, id),
isNull(drizzleDb.schemas.backup.deletedAt)
),
orderBy: [desc(drizzleDb.schemas.backup.createdAt)],
}),
db.query.restoration.findFirst({
where: and(
eq(drizzleDb.schemas.restoration.databaseId, id),
isNull(drizzleDb.schemas.restoration.deletedAt)
),
orderBy: [desc(drizzleDb.schemas.restoration.createdAt)],
}),
]);
if (!database) {
return NextResponse.json(
{ error: "Database not found" },
{ status: 404 }
);
}
return NextResponse.json({
data: {
isWaitingForBackup: database.isWaitingForBackup,
lastContact: database.lastContact,
latestBackup: latestBackup ?? null,
latestRestoration: latestRestoration ?? null,
},
});
} catch (error) {
log.error({ error }, "Error in GET /api/v1/databases/[id]/status");
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
}
);
+24
View File
@@ -0,0 +1,24 @@
import { NextResponse } from "next/server";
import { withApiKey } from "@/lib/api-v1/middleware";
import { logger } from "@/lib/logger";
import { ApiKeyContext } from "@/lib/api-v1/types";
import { getAccessibleDatabases } from "@/lib/api-v1/services/databases";
const log = logger.child({ module: "api/v1/databases" });
export const GET = withApiKey(async (_req: Request, ctx: ApiKeyContext) => {
try {
const databases = await getAccessibleDatabases(ctx.user);
return NextResponse.json({
data: databases,
});
} catch (error) {
log.error({ error }, "Error in GET /api/v1/databases");
return NextResponse.json(
{ error: "Internal server error" },
{ status: 500 }
);
}
});
+45
View File
@@ -0,0 +1,45 @@
export const dynamic = "force-dynamic";
const html = `<!DOCTYPE html>
<html>
<head>
<title>Portabase API Docs</title>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link
rel="stylesheet"
href="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.32.6/swagger-ui.css"
integrity="sha384-9Q2fpS+xeS4ffJy6CagnwoUl+4ldAYhOs9pgZuEKxypVModhmZFzeMlvVsAjf7uT"
crossorigin="anonymous"
/>
<style>
body { margin: 0; }
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.32.6/swagger-ui-bundle.js" integrity="sha384-EYdOaiRwn44zNjrw+Tfs06qYz9BGQVo2f4/pLY5i7VorbjnZNhdplAbTBk8FXHUJ" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/swagger-ui-dist@5.32.6/swagger-ui-standalone-preset.js" integrity="sha384-49fpFaVrAWI/qdgl9Vv5E/4NXxRUiJX5vGuLws1NUpTWGtEqzWEx8gHTw2UTehFK" crossorigin="anonymous"></script>
<script>
window.onload = function () {
SwaggerUIBundle({
url: "/api/v1/openapi",
dom_id: "#swagger-ui",
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset,
],
layout: "StandaloneLayout",
persistAuthorization: true,
filter: true,
});
};
</script>
</body>
</html>`;
export function GET() {
return new Response(html, {
headers: { "Content-Type": "text/html; charset=utf-8" },
});
}
+31
View File
@@ -0,0 +1,31 @@
import { WebStandardStreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/webStandardStreamableHttp.js";
import { NextResponse } from "next/server";
import { withApiKey } from "@/lib/api-v1/middleware";
import { createPortabaseMcpServer } from "@/lib/mcp/server";
import type { ApiKeyContext } from "@/lib/api-v1/types";
import { env } from "@/env.mjs";
/**
* MCP endpoint Streamable HTTP transport, stateless mode.
*
* Auth is handled by withApiKey before MCP is ever touched.
* The validated key is forwarded by MCP tools to downstream /api/v1 REST calls.
*/
const apiEnabled = env.API_ENABLED;
const mcpEnabled = env.MCP_ENABLED;
export const POST = apiEnabled && mcpEnabled
? withApiKey(async (req: Request, ctx: ApiKeyContext) => {
const apiKey = req.headers.get("x-api-key")!;
const server = createPortabaseMcpServer(ctx, apiKey);
const transport = new WebStandardStreamableHTTPServerTransport({
sessionIdGenerator: undefined,
});
await server.connect(transport);
return transport.handleRequest(req);
})
: () => NextResponse.json({ error: "Not found" }, { status: 404 });
+8
View File
@@ -0,0 +1,8 @@
import { NextResponse } from "next/server";
import { buildSpec } from "@/lib/api-v1/openapi/spec";
export const dynamic = "force-dynamic";
export function GET() {
return NextResponse.json(buildSpec());
}
+2 -2
View File
@@ -1,12 +1,12 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import type React from "react"; import type React from "react";
import "./globals.css"; import "./globals.css";
import { ConsoleSilencer } from "@/components/wrappers/common/console-silencer"; import { ConsoleSilencer } from "@/components/common/console-silencer";
import { author, geistMono, poppins } from "@/fonts/fonts"; import { author, geistMono, poppins } from "@/fonts/fonts";
import { cn } from "@/lib/utils"; import { cn } from "@/lib/utils";
import { Providers } from "./providers"; import { Providers } from "./providers";
const title = process.env.PROJECT_NAME ?? "App Title"; const title = process.env.PROJECT_NAME ?? "Portabase";
export const metadata: Metadata = { export const metadata: Metadata = {
title: { title: {
+1 -1
View File
@@ -1,4 +1,4 @@
import BackButton from "@/components/wrappers/common/button/back-button"; import BackButton from "@/components/common/back-button";
export default async function NotFound() { export default async function NotFound() {
return ( return (
+2 -2
View File
@@ -4,8 +4,8 @@ import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { type PropsWithChildren, Suspense } from "react"; import { type PropsWithChildren, Suspense } from "react";
import { Toaster } from "@/components/ui/sonner"; import { Toaster } from "@/components/ui/sonner";
import { ErrorLayout } from "@/components/wrappers/common/error-layout"; import { ErrorLayout } from "@/components/common/error-layout";
import { ThemeMetaUpdaterRoot } from "@/features/browser/theme-meta-updater-root"; import { ThemeMetaUpdaterRoot } from "@/features/theme/theme-meta-updater-root";
import { ThemeProvider } from "@/features/theme/theme-provider"; import { ThemeProvider } from "@/features/theme/theme-provider";
export type ProviderProps = PropsWithChildren<{}>; export type ProviderProps = PropsWithChildren<{}>;
+11 -5
View File
@@ -1,10 +1,10 @@
services: services:
app: app:
# build: # build:
# context: . # context: .
# dockerfile: docker/dockerfile/Dockerfile # dockerfile: docker/dockerfile/Dockerfile
# target: prod # target: prod
image: portabase/portabase:1.4.0 image: portabase/portabase:1
ports: ports:
- '8887:80' - '8887:80'
environment: environment:
@@ -17,6 +17,12 @@ services:
db: db:
condition: service_healthy condition: service_healthy
container_name: portabase-app-prod container_name: portabase-app-prod
healthcheck:
test: ["CMD-SHELL", "curl -f http://localhost/api/health"]
interval: 30s
timeout: 5s
retries: 3
start_period: 60s
db: db:
image: postgres:16-alpine image: postgres:16-alpine
+30 -1
View File
@@ -12,7 +12,7 @@ services:
- POSTGRES_USER=devuser - POSTGRES_USER=devuser
- POSTGRES_PASSWORD=changeme - POSTGRES_PASSWORD=changeme
healthcheck: healthcheck:
test: ["CMD-SHELL", "pg_isready -U devuser -d devdb"] test: [ "CMD-SHELL", "pg_isready -U devuser -d devdb" ]
interval: 10s interval: 10s
timeout: 5s timeout: 5s
retries: 5 retries: 5
@@ -32,5 +32,34 @@ services:
- ./private/uploads/tmp:/data/uploads/tmp - ./private/uploads/tmp:/data/uploads/tmp
user: "1000:1000" user: "1000:1000"
mailpit:
image: axllent/mailpit:latest
container_name: portabase-mailpit-dev
hostname: mailpit
restart: unless-stopped
ports:
- "8025:8025"
- "1025:1025"
azurite:
image: mcr.microsoft.com/azure-storage/azurite
command: azurite --blobHost 0.0.0.0 --skipApiVersionCheck
ports:
- "10000:10000"
volumes:
- azurite-data:/data
extra_hosts:
- "localhost:host-gateway"
networks:
- portabase
# DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;QueueEndpoint=http://127.0.0.1:10001/devstoreaccount1;TableEndpoint=http://127.0.0.1:10002/devstoreaccount1;
volumes: volumes:
postgres-data: postgres-data:
azurite-data:
networks:
portabase:
name: portabase_network
external: true
+1
View File
@@ -92,6 +92,7 @@ ENV PORT=80
ENV HOSTNAME="0.0.0.0" ENV HOSTNAME="0.0.0.0"
ENV PGDATA=/data/postgres ENV PGDATA=/data/postgres
ENV PRIVATE_PATH=/data/private ENV PRIVATE_PATH=/data/private
ENV TUSD_BEHIND_PROXY=false
RUN addgroup --system --gid 1001 nodejs RUN addgroup --system --gid 1001 nodejs
+12 -1
View File
@@ -72,7 +72,18 @@ fi
mkdir -p /data/private/uploads/tmp mkdir -p /data/private/uploads/tmp
echo "▶ Starting tusd server..." echo "▶ Starting tusd server..."
tusd --base-path /tus/files/ --upload-dir /data/private/uploads/tmp --hooks-http http://127.0.0.1:3000/api/tus/hooks --port 1080 --max-size 21474836480 & TUSD_BEHIND_PROXY_FLAG=""
if [ "${TUSD_BEHIND_PROXY:-false}" = "true" ]; then
TUSD_BEHIND_PROXY_FLAG="--behind-proxy"
echo "[INFO] TUSD_BEHIND_PROXY=true, enabling tusd proxy mode"
fi
tusd \
--base-path /tus/files/ \
--upload-dir /data/private/uploads/tmp \
--hooks-http http://127.0.0.1:3000/api/tus/hooks \
--port 1080 \
--max-size 21474836480 \
$TUSD_BEHIND_PROXY_FLAG &
echo "▶ Starting Next.js server..." echo "▶ Starting Next.js server..."
PORT=3000 node server.js & PORT=3000 node server.js &
+7 -2
View File
@@ -1,6 +1,11 @@
events {} events {}
http { http {
map $http_x_forwarded_proto $forwarded_proto {
default $scheme;
"~*^\s*(https?)\s*(?:,|$)" $1;
}
client_max_body_size 20G; client_max_body_size 20G;
ignore_invalid_headers off; ignore_invalid_headers off;
@@ -18,7 +23,7 @@ http {
proxy_set_header Host $http_host; proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Host $http_host; proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $forwarded_proto;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Upgrade $http_upgrade; proxy_set_header Upgrade $http_upgrade;
@@ -34,7 +39,7 @@ http {
proxy_set_header Host $http_host; proxy_set_header Host $http_host;
proxy_set_header X-Forwarded-Host $http_host; proxy_set_header X-Forwarded-Host $http_host;
proxy_set_header X-Forwarded-Proto $scheme; proxy_set_header X-Forwarded-Proto $forwarded_proto;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
} }
} }
+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
```
+59
View File
@@ -0,0 +1,59 @@
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 }}
- name: TUSD_BEHIND_PROXY
value: {{ .Values.project.behindProxy | quote }}
volumeMounts:
- name: data
mountPath: /data
startupProbe:
httpGet:
path: /api/health
port: 80
initialDelaySeconds: 10
periodSeconds: 10
failureThreshold: 12
timeoutSeconds: 5
livenessProbe:
httpGet:
path: /api/health
port: 80
initialDelaySeconds: 0
periodSeconds: 30
timeoutSeconds: 5
failureThreshold: 3
readinessProbe:
httpGet:
path: /api/health
port: 80
initialDelaySeconds: 0
periodSeconds: 10
timeoutSeconds: 5
failureThreshold: 3
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 }}
+48
View File
@@ -0,0 +1,48 @@
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"
behindProxy: false
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: { typescript: {
ignoreBuildErrors: true, ignoreBuildErrors: true,
}, },
logging: {
browserToTerminal: false,
},
experimental: { experimental: {
serverActions: { serverActions: {
bodySizeLimit: "10gb", bodySizeLimit: "10gb",
+52 -44
View File
@@ -1,6 +1,6 @@
{ {
"name": "portabase", "name": "portabase",
"version": "1.4.26", "version": "1.20.2",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "next dev --turbopack -p 8887", "dev": "next dev --turbopack -p 8887",
@@ -15,9 +15,14 @@
"release": "release-it" "release": "release-it"
}, },
"dependencies": { "dependencies": {
"@better-auth/passkey": "^1.4.19", "@asteasolutions/zod-to-openapi": "^8.5.0",
"@better-auth/sso": "^1.4.19", "@azure/storage-blob": "^12.32.0",
"@hookform/resolvers": "^5.2.2", "@better-auth/api-key": "1.6.11",
"@better-auth/core": "1.6.11",
"@better-auth/passkey": "1.6.11",
"@better-auth/sso": "1.6.11",
"@hookform/resolvers": "^5.4.0",
"@modelcontextprotocol/sdk": "^1.29.0",
"@radix-ui/react-accordion": "^1.2.12", "@radix-ui/react-accordion": "^1.2.12",
"@radix-ui/react-alert-dialog": "^1.1.15", "@radix-ui/react-alert-dialog": "^1.1.15",
"@radix-ui/react-aspect-ratio": "^1.1.8", "@radix-ui/react-aspect-ratio": "^1.1.8",
@@ -47,45 +52,47 @@
"@radix-ui/react-toggle-group": "^1.1.11", "@radix-ui/react-toggle-group": "^1.1.11",
"@radix-ui/react-tooltip": "^1.2.8", "@radix-ui/react-tooltip": "^1.2.8",
"@react-email/components": "^0.0.41", "@react-email/components": "^0.0.41",
"@t3-oss/env-nextjs": "^0.13.10", "@t3-oss/env-nextjs": "^0.13.11",
"@tanstack/react-query": "^5.90.21", "@tanstack/react-query": "^5.100.14",
"@tanstack/react-table": "^8.21.3", "@tanstack/react-table": "^8.21.3",
"@types/nodemailer": "^6.4.23", "@types/nodemailer": "^6.4.23",
"@types/ws": "^8.18.1", "@types/ws": "^8.18.1",
"@zenstackhq/runtime": "2.14.2", "@zenstackhq/runtime": "2.14.2",
"argon2": "^0.43.1", "argon2": "^0.43.1",
"bcrypt": "^6.0.0", "bcrypt": "^6.0.0",
"better-auth": "1.4.18", "better-auth": "1.6.11",
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "^1.1.1", "cmdk": "^1.1.1",
"date-fns": "^4.1.0", "date-fns": "^4.3.0",
"dockerode": "^4.0.9", "dockerode": "^4.0.12",
"dotenv": "^16.6.1", "dotenv": "^16.6.1",
"drizzle-orm": "^0.43.1", "drizzle-orm": "0.45.2",
"drizzle-zod": "^0.7.1", "drizzle-zod": "0.8.3",
"embla-carousel-react": "^8.6.0", "embla-carousel-react": "^8.6.0",
"googleapis": "^170.1.0", "googleapis": "^170.1.0",
"input-otp": "^1.4.2", "input-otp": "^1.4.2",
"lucide-react": "^0.553.0", "lucide-react": "^0.553.0",
"minio": "^8.0.6", "minio": "^8.0.7",
"motion": "^12.34.3", "motion": "^12.40.0",
"next": "16.1.5", "next": "16.2.3",
"next-safe-action": "^7.10.8", "next-safe-action": "^7.10.8",
"next-themes": "^0.4.6", "next-themes": "^0.4.6",
"node-cron": "^4.2.1", "node-cron": "^4.2.1",
"node-forge": "^1.3.3", "node-forge": "^1.4.0",
"nodemailer": "^7.0.13", "nodemailer": "8.0.5",
"npm-check-updates": "^18.3.1", "npm-check-updates": "^18.3.1",
"pg": "^8.18.0", "pg": "^8.21.0",
"prettier": "^3.8.1", "pino": "^10.3.1",
"react": "^19.2.4", "pino-pretty": "^13.1.3",
"prettier": "^3.8.3",
"react": "^19.2.6",
"react-day-picker": "9.7.0", "react-day-picker": "9.7.0",
"react-dom": "^19.2.4", "react-dom": "^19.2.6",
"react-dropzone": "^14.4.1", "react-dropzone": "^14.4.1",
"react-email": "^4.3.2", "react-email": "^4.3.2",
"react-hook-form": "^7.71.2", "react-hook-form": "^7.76.1",
"react-qr-code": "^2.0.18", "react-qr-code": "^2.0.21",
"react-resizable-panels": "^3.0.6", "react-resizable-panels": "^3.0.6",
"react-twc": "^1.5.1", "react-twc": "^1.5.1",
"react-use-measure": "^2.1.7", "react-use-measure": "^2.1.7",
@@ -94,42 +101,43 @@
"socket.io": "^4.8.3", "socket.io": "^4.8.3",
"socket.io-client": "^4.8.3", "socket.io-client": "^4.8.3",
"sonner": "^2.0.7", "sonner": "^2.0.7",
"swiper": "^12.1.2", "swiper": "^12.1.4",
"tailwind-merge": "^3.5.0", "tailwind-merge": "^3.6.0",
"uuid": "^11.1.0", "uuid": "^11.1.1",
"vaul": "^1.1.2", "vaul": "^1.1.2",
"ws": "^8.19.0", "ws": "^8.21.0",
"zod": "^3.25.76" "zod": "4.3.6"
}, },
"devDependencies": { "devDependencies": {
"@iconify/react": "^6.0.2", "@iconify/react": "^6.0.2",
"@react-email/preview-server": "4.3.2", "@react-email/preview-server": "4.3.2",
"@react-email/render": "^2.0.4", "@react-email/render": "^2.0.8",
"@release-it/bumper": "^7.0.5", "@release-it/bumper": "^7.0.5",
"@release-it/conventional-changelog": "^10.0.5", "@release-it/conventional-changelog": "^10.0.6",
"@tailwindcss/postcss": "^4.2.1", "@tailwindcss/postcss": "^4.3.0",
"@types/eslint-plugin-tailwindcss": "^3.17.0", "@types/eslint-plugin-tailwindcss": "^3.17.0",
"@types/node": "^22.19.11", "@types/node": "^22.19.19",
"@types/node-forge": "^1.3.14", "@types/node-forge": "^1.3.14",
"@types/pg": "^8.16.0", "@types/pg": "^8.20.0",
"@types/react": "^19.2.14", "@types/react": "^19.2.15",
"@types/react-dom": "^19.2.3", "@types/react-dom": "^19.2.3",
"@zenstackhq/openapi": "^2.22.1", "@zenstackhq/openapi": "^2.22.1",
"@zenstackhq/tanstack-query": "^2.22.2", "@zenstackhq/tanstack-query": "^2.22.2",
"baseline-browser-mapping": "^2.10.0", "baseline-browser-mapping": "^2.10.32",
"drizzle-kit": "^0.31.9", "drizzle-kit": "^0.31.10",
"esbuild": "^0.27.3", "esbuild": "^0.27.7",
"eslint": "^9.39.3", "eslint": "^9.39.4",
"eslint-config-next": "^16.1.6", "eslint-config-next": "^16.2.6",
"eslint-plugin-tailwindcss": "^3.18.2", "eslint-plugin-tailwindcss": "^3.18.3",
"framer-motion": "^12.34.3", "framer-motion": "^12.40.0",
"postcss": "^8.5.6", "node-pty": "^1.1.0",
"postcss": "8.5.10",
"release-it": "^19.2.4", "release-it": "^19.2.4",
"tailwindcss": "^4.2.1", "tailwindcss": "^4.3.0",
"tsx": "^4.21.0", "tsx": "^4.22.3",
"tw-animate-css": "^1.4.0", "tw-animate-css": "^1.4.0",
"typescript": "^5.9.3", "typescript": "^5.9.3",
"zenstack": "2.14.2" "zenstack": "2.14.2"
}, },
"packageManager": "pnpm@10.30.1+sha512.3590e550d5384caa39bd5c7c739f72270234b2f6059e13018f975c313b1eb9fefcc09714048765d4d9efe961382c312e624572c0420762bdc5d5940cdf9be73a" "packageManager": "pnpm@11.8.0+sha512.c1f5e7c4cb241c8f174b743851d82f42b802324afc8b0f116b96adb15aa06664948dde36960a3ba1079ba5b4b29dd0140135b94b5b5f5263592249d68e555f26"
} }
+4646 -2835
View File
File diff suppressed because it is too large Load Diff
+16
View File
@@ -1,3 +1,19 @@
allowBuilds:
'@prisma/client': true
'@prisma/engines': true
argon2: true
bcrypt: true
cpu-features: true
esbuild: true
node-pty: true
prisma: true
protobufjs: true
sharp: true
ssh2: true
unrs-resolver: true
zenstack: true
ignoredBuiltDependencies:
- node-pty
onlyBuiltDependencies: onlyBuiltDependencies:
- '@prisma/client' - '@prisma/client'
- '@prisma/engines' - '@prisma/engines'
+1
View File
@@ -23,6 +23,7 @@ export const PORTABASE_DEFAULT_SETTINGS = {
"https://code.iconify.design", "https://code.iconify.design",
"https://cdn.iconify.design", "https://cdn.iconify.design",
"https://code.iconify.com", "https://code.iconify.com",
"https://cdn.jsdelivr.net",
], ],
IMG_SRC: [ IMG_SRC: [
"'self'", "'self'",
+47 -2
View File
@@ -3,6 +3,8 @@ import { loggingMiddleware } from "@/middleware/loggingMiddleware";
import { errorHandler } from "@/middleware/errorHandler"; import { errorHandler } from "@/middleware/errorHandler";
import { auth } from "@/lib/auth/auth"; import { auth } from "@/lib/auth/auth";
import { headers } from "next/headers"; import { headers } from "next/headers";
import { env } from "@/env.mjs";
import {User} from "@/db/schema/02_user";
export async function proxy(request: NextRequest) { export async function proxy(request: NextRequest) {
const url = request.nextUrl.clone(); const url = request.nextUrl.clone();
@@ -17,11 +19,13 @@ export async function proxy(request: NextRequest) {
new URL(`/login?redirect=${redirectUrl}`, request.url), new URL(`/login?redirect=${redirectUrl}`, request.url),
); );
} }
if (session.user.banned) { const user = session.user as User
if (user.banned) {
await auth.api.signOut({ headers: await headers() }); await auth.api.signOut({ headers: await headers() });
return NextResponse.redirect(new URL("/login?error=banned", request.url)); return NextResponse.redirect(new URL("/login?error=banned", request.url));
} }
if (session.user.role === "pending") { if (user.role === "pending") {
await auth.api.signOut({ headers: await headers() }); await auth.api.signOut({ headers: await headers() });
return NextResponse.redirect( return NextResponse.redirect(
new URL(`/login?error=pending?redirect=${redirectUrl}`, request.url), new URL(`/login?error=pending?redirect=${redirectUrl}`, request.url),
@@ -38,6 +42,33 @@ export async function proxy(request: NextRequest) {
} }
if (url.pathname.startsWith("/api")) { if (url.pathname.startsWith("/api")) {
if (url.pathname.startsWith("/api/v1")) {
const apiEnabled = env.API_ENABLED;
if (!apiEnabled) {
return new NextResponse(
JSON.stringify({
message: "This API route does not exist.",
status: 404,
}),
{ status: 404, headers: { "Content-Type": "application/json" } },
);
}
const openapiEnabled = env.OPENAPI_ENABLED;
if (
!openapiEnabled &&
(url.pathname.startsWith("/api/v1/docs") ||
url.pathname.startsWith("/api/v1/openapi"))
) {
return new NextResponse(
JSON.stringify({
message: "This API route does not exist.",
status: 404,
}),
{ status: 404, headers: { "Content-Type": "application/json" } },
);
}
}
const routeExists = checkRouteExists(url.pathname); const routeExists = checkRouteExists(url.pathname);
if (!routeExists) { if (!routeExists) {
return new NextResponse( return new NextResponse(
@@ -71,7 +102,21 @@ function checkRouteExists(pathname: string) {
/^\/api\/tus\/hooks\/?$/, /^\/api\/tus\/hooks\/?$/,
/^\/api\/events\/?$/, /^\/api\/events\/?$/,
/^\/api\/config\/?$/, /^\/api\/config\/?$/,
/^\/api\/health\/?$/,
/^\/api\/google\/drive\/callback\/?$/, /^\/api\/google\/drive\/callback\/?$/,
// v1 external API
/^\/api\/v1\/mcp\/?$/,
/^\/api\/v1\/docs\/?$/,
/^\/api\/v1\/openapi\/?$/,
/^\/api\/v1\/agents\/?$/,
/^\/api\/v1\/agents\/[^/]+\/?$/,
/^\/api\/v1\/agents\/[^/]+\/key\/?$/,
/^\/api\/v1\/databases\/?$/,
/^\/api\/v1\/databases\/[^/]+\/?$/,
/^\/api\/v1\/databases\/[^/]+\/backup\/?$/,
/^\/api\/v1\/databases\/[^/]+\/backup\/[^/]+\/?$/,
/^\/api\/v1\/databases\/[^/]+\/restore\/?$/,
/^\/api\/v1\/databases\/[^/]+\/status\/?$/,
]; ];
return routePatterns.some((pattern) => pattern.test(pathname)); return routePatterns.some((pattern) => pattern.test(pathname));
} }
Binary file not shown.

After

Width:  |  Height:  |  Size: 125 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

+51
View File
@@ -0,0 +1,51 @@
#!/usr/bin/env bash
#
# azurite-list.sh — enumerate every container and blob in a local Azurite emulator.
#
# Usage:
# ./azurite-list.sh # list containers + blobs
# ./azurite-list.sh -v # also print blob size, last-modified, content-type
#
set -euo pipefail
export AZURE_STORAGE_CONNECTION_STRING="DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;QueueEndpoint=http://127.0.0.1:10001/devstoreaccount1;TableEndpoint=http://127.0.0.1:10002/devstoreaccount1;"
VERBOSE=0
[[ "${1:-}" == "-v" || "${1:-}" == "--verbose" ]] && VERBOSE=1
command -v az >/dev/null 2>&1 || { echo "ERROR: 'az' CLI not found in PATH." >&2; exit 1; }
command -v jq >/dev/null 2>&1 || { echo "ERROR: 'jq' not found in PATH." >&2; exit 1; }
if ! az storage container list --num-results 1 >/dev/null 2>&1; then
echo "ERROR: Cannot reach Azurite at 127.0.0.1:10000. Is the container running?" >&2
echo " Start it with: docker start azurite" >&2
exit 1
fi
containers=$(az storage container list --query "[].name" -o tsv)
if [[ -z "$containers" ]]; then
echo "(no containers found in this emulator)"
exit 0
fi
while IFS= read -r container; do
echo "📦 container: $container"
if [[ "$VERBOSE" -eq 1 ]]; then
az storage blob list --container-name "$container" \
--query "[].{name:name, bytes:properties.contentLength, modified:properties.lastModified, type:properties.contentSettings.contentType}" \
-o json \
| jq -r '.[] | " • \(.name) [\(.bytes) bytes] \(.type // "?") \(.modified)"'
else
az storage blob list --container-name "$container" \
--query "[].{name:name, bytes:properties.contentLength}" \
-o json \
| jq -r '.[] | " • \(.name) [\(.bytes) bytes]"'
fi
count=$(az storage blob list --container-name "$container" --query "length(@)" -o tsv)
[[ "$count" -eq 0 ]] && echo " (empty)"
echo
done <<< "$containers"
+46
View File
@@ -0,0 +1,46 @@
#!/usr/bin/env bash
#
# azurite-seed.sh — seed a local Azurite emulator with a container + sample blob.
#
# Usage:
# ./azurite-seed.sh # create container "portabase" + sample blob
# ./azurite-seed.sh my-container # override container name
#
set -euo pipefail
export AZURE_STORAGE_CONNECTION_STRING="DefaultEndpointsProtocol=http;AccountName=devstoreaccount1;AccountKey=Eby8vdM02xNOcqFlqUwJPLlmEtlCDXJ1OUzFT50uSRZ6IFsuFq2UVErCz4I6tq/K1SZFPTOtr/KBHBeksoGMGw==;BlobEndpoint=http://127.0.0.1:10000/devstoreaccount1;QueueEndpoint=http://127.0.0.1:10001/devstoreaccount1;TableEndpoint=http://127.0.0.1:10002/devstoreaccount1;"
CONTAINER="${1:-portabase}"
BLOB_NAME="hello.txt"
command -v az >/dev/null 2>&1 || { echo "ERROR: 'az' CLI not found in PATH." >&2; exit 1; }
# Reachability check — surface the real az error instead of guessing "container down".
if ! err=$(az storage container list --num-results 1 2>&1 >/dev/null); then
echo "ERROR: Azurite query failed:" >&2
printf '%s\n' "$err" | sed 's/^/ /' >&2
echo " Is Azurite up? Start it with: docker compose up -d azurite" >&2
exit 1
fi
# Create container (idempotent — az returns created:false if it already exists).
az storage container create --name "$CONTAINER" -o none
echo "📦 container ready: $CONTAINER"
# Upload a sample blob from a temp file.
tmp=$(mktemp)
trap 'rm -f "$tmp"' EXIT
printf 'hello from azurite seed — %s\n' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > "$tmp"
az storage blob upload \
--container-name "$CONTAINER" \
--name "$BLOB_NAME" \
--file "$tmp" \
--overwrite true \
--no-progress \
-o none
echo " • uploaded blob: $BLOB_NAME"
echo
echo "Seed complete. Verify with: make list-blob"
-1
View File
@@ -1 +0,0 @@
-1
View File
@@ -1 +0,0 @@

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