Compare commits

..

75 Commits

Author SHA1 Message Date
Mauricio Siu
6a0acd9cad fix: update schedule scoping from user to organization
Changed the schedule scoping in the schedule utility to use organizationId instead of userId, ensuring that schedules are shared across all owners and admins within the same organization. This aligns with the recent changes to enhance organizational resource management.
2026-06-02 02:17:51 -06:00
Mauricio Siu
64a606ffa4 refactor: streamline imports and enhance permission test readability
Consolidated import statements across multiple provider components by removing duplicate imports of VALID_BRANCH_REGEX. Improved the readability of the permission test for denied access by simplifying the expect statement. Additionally, added copy-to-clipboard functionality in relevant components to enhance user experience.
2026-06-02 02:17:51 -06:00
Mauricio Siu
29851491f6 chore: update version to v0.29.7 in package.json and enhance permission tests
Bumped the version of dokploy to v0.29.7. Updated test descriptions for clarity, specifically renaming the test suite to reflect the roles of "owner" and "admin." Added new tests to ensure that members are denied access to various org-level enterprise resources, improving coverage and validation of permission checks.
2026-06-02 02:17:50 -06:00
Mauricio Siu
95633b4122 fix: refine permission check for privileged static roles in permission service
Updated the permission check logic to specifically identify "owner" and "admin" roles as privileged static roles, enhancing clarity and accuracy in permission validation. This change ensures that only users with these roles are granted access to enterprise-only resources.
2026-06-02 02:17:50 -06:00
Mauricio Siu
c73632cbe0 fix: scope dokploy-server schedules to organization instead of user (#4526)
* fix: scope dokploy-server schedules to organization instead of user

Replaces userId with organizationId on the schedule table so that
global (dokploy-server) schedules are shared across all owners and
admins of the same organization, while remaining isolated between
different organizations.

Includes a data migration that backfills organizationId from the
owner membership record for any existing dokploy-server schedules.

Closes #4300

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
2026-06-02 02:17:49 -06:00
github-actions[bot]
30b3e1fe48 🚀 Release v0.29.6 (#4514)
* fix(migrate-auth-secret): exit cleanly when there are no 2FA records

The empty-records branch of `main()` returned without calling
`process.exit(0)`, leaving the Drizzle Postgres connection pool
holding the event loop open. The `migrate-auth-secret` process
then hangs indefinitely after printing "No 2FA records found,
nothing to migrate." causing the upstream `0.29.3.sh` security
migration script (which calls this via `docker exec`) to never
reach its final `docker service update` step that mounts the new
Docker Secret. Operators end up with the new secret created but
the dokploy service still configured with the hardcoded
`BETTER_AUTH_SECRET`, while believing the migration completed.

Match the success branch a few lines below which already does
`process.exit(0)`, and the pattern used in sibling scripts
`reset-password.ts` and `reset-2fa.ts`.

Closes #4392

* feat(compose): add import from base64 in create service dropdown

Adds an "Import" option to the Create Service dropdown that lets users
paste a base64-encoded compose export, preview the template (compose YAML,
domains, envs, mounts) before confirming, and create the service only on
confirm. Adds a `previewTemplate` tRPC procedure that processes the base64
without touching the DB, with server access validation via session.

* [autofix.ci] apply automated fixes

* Enhance version synchronization workflow to include SDK repository

- Updated the GitHub Actions workflow to sync versioning across MCP, CLI, and SDK repositories.
- Added steps to bump the version in the SDK repository and regenerate tools from the latest OpenAPI spec.
- Improved commit message formatting to include source and release information for all repositories.
- Ensured successful synchronization messages for each repository after the version update.

* feat(deployment): add readLogs procedure to fetch deployment logs

- Introduced a new `readLogs` procedure that allows users to retrieve logs for a specific deployment by providing the deployment ID and an optional tail parameter.
- Implemented permission checks to ensure users have access to the requested logs.
- Enhanced log retrieval for both cloud and non-cloud environments, utilizing appropriate commands based on the server context.

Resolve https://github.com/Dokploy/mcp/issues/14

* feat(deployment): add server access validation for deployment actions

- Implemented server access validation in deployment procedures to ensure users can only access deployments associated with their active organization.
- Added checks to throw an UNAUTHORIZED error if a user attempts to access a deployment linked to a server outside their organization.

This enhancement improves security and access control within the deployment management system.

* feat(organization): prevent inviting users with owner role

- Added validation to prevent users from being invited with the owner role in the organization and user routers.
- Implemented TRPCError responses to ensure proper error handling when attempting to assign the owner role.
This change enhances role management and security within the organization structure.

https://github.com/Dokploy/dokploy/security/advisories/GHSA-fm9p-wmpw-gxjh

* feat(user): implement session cleanup on user update

- Added functionality to delete old sessions when a user updates their password, ensuring that only the current session remains active.
- This change enhances security by preventing unauthorized access from previous sessions after a password change.

Close here https://github.com/Dokploy/dokploy/security/advisories/GHSA-rr9m-w87g-46f3

* feat(settings): add copy button to server IP in web server settings (#4397)

* fix: copy Dokploy server IP when clicking server badge (#4390)

* fix: copy Dokploy server IP when clicking server badge

When a service runs on the local Dokploy server (no remote server),
clicking the server badge did nothing because `data.server` is null.
Now falls back to the server IP from settings so the badge always
copies an IP address.

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

* feat(copy-ip): implement IP address copying functionality across database service components

- Added the ability to copy the server IP address to the clipboard when clicking the server badge in various database service components (Libsql, MariaDB, MongoDB, MySQL, PostgreSQL, Redis).
- Integrated the `copy-to-clipboard` library and `sonner` for user feedback upon successful copy action.
- Ensured fallback to the server IP from settings when the service data is not available, enhancing user experience and functionality.

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Mauricio Siu <siumauricio@icloud.com>

* fix: responsive layout (#4391)

Signed-off-by: Nahidujjaman Hridoy <hridoyboss12@gmail.com>

* fix: automatically converting username to lowercase both in creation of register, and build for extra. (#4382)

* fix: allow square brackets in zip path validation for Next.js dynamic routes (#4468)

* fix: allow square brackets in zip drop path validation for Next.js dynamic routes

ZIP uploads containing Next.js dynamic route files (e.g. app/api/[id]/route.ts,
pages/[slug].tsx) were rejected by readValidDirectory because the path regex
did not include square bracket characters.

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>

* fix: prevent webhook deploy crash when commit data lacks modified files (#4470)

shouldDeploy passed undefined/null entries from commit.modified straight
into micromatch, which throws "Expected input to be a string" and fails
every webhook deployment when watch paths are configured. Filter out
non-string values before matching.

* fix: add type="button" to TooltipTrigger in form components to prevent accidental submission (#4422)

Co-authored-by: Maks Pikov <mixelburg@users.noreply.github.com>

* fix: enable comment toggle shortcut in env variable editor (#4402) (#4473)

* fix: add tls=true label for domains when certificateType is none (#4018) (#4474)

* fix: add tls=true label for compose domains when certificateType is none (#4018)

* test: cover tls=true label for certificateType none, require https

* fix: scope tls fix to compose labels, leave traefik file config unchanged (#4018)

* chore: update version to v0.29.5 in package.json

* chore(deps): upgrade next to 16.2.6 (#4477)

Upgraded next dependency in apps/dokploy to 16.2.6 exactly. Verified typescript typecheck passes successfully.

* feat: add self-hosted enterprise restrictions (remote-servers-only, enforce-sso) (#4511)

* feat: add self-hosted enterprise restrictions (remote-servers-only, enforce-sso)

- Add `remoteServersOnly` field to webServerSettings: prevents creating services
  on the local Dokploy VM, forcing all deployments to remote servers. Validated
  in all 8 service routers (application, compose, postgres, mysql, mongo, redis,
  mariadb, libsql).
- Add `enforceSSO` field to webServerSettings: hides the email/password login
  form and shows only the SSO button on the login page.
- Both settings are enterprise-only (enterpriseProcedure) and self-hosted-only
  (blocked at the API level when IS_CLOUD=true).
- UI toggles added to the SSO settings page under a new "Self-hosted
  Restrictions" card (hidden in cloud). Login page reads enforceSSO from
  getServerSideProps to avoid client-side flash.
- Migrations: 0167_fresh_goliath.sql, 0168_long_justice.sql

* fix: add missing final newlines to migration files

* refactor: improve code formatting for better readability in multiple components

- Adjusted formatting in `add-application.tsx`, `add-compose.tsx`, and `add-database.tsx` to enhance readability by adding line breaks and consistent indentation.
- Updated `toggle-enforce-sso.tsx` to simplify the Switch component's props.
- Reformatted imports in `index.tsx` and `sso.tsx` for consistency.
- Cleaned up conditional statements in various router files for improved clarity.

* fix: add enforceSSO to test mock

* fix: grant create and delete SSH key permissions when canAccessToSSHKeys is enabled for members (#4512)

* fix: use create permission for basic auth delete instead of delete (#4513)

* fix: wrap long server names and keep actions menu visible (#4434)

On settings/servers, a long server name in the card title (h3) did not
wrap and overflowed its container, overlapping nearby content and
squeezing the three-dots actions menu until it disappeared.

Allow the title block to shrink and wrap (min-w-0 + break-words), keep
the server icon and the actions trigger from being crushed (shrink-0),
and add gap between the title and the actions button.

* chore: update version to v0.29.6 in package.json

* fix: preserve HOME in compose deploy so --with-registry-auth can read docker config (#4485)

The compose/stack deploy command runs under `env -i PATH="$PATH"`, which
clears the environment except for PATH. That strips HOME, so when the
generated command is `docker stack deploy --prune --with-registry-auth`
the docker CLI cannot resolve `~/.docker/config.json` (e.g.
`/root/.docker/config.json`) and ships no registry credentials to the
swarm. Private-registry images then fail to pull on the nodes:

  image registry.example.com/... could not be accessed on a registry to
  record its digest. Each node will access ... independently

while the deploy still logs "Docker Compose Deployed: ".

Keep PATH isolation but preserve HOME so docker can read its config for
both `stack deploy --with-registry-auth` and `compose up -d --build`.

Add a regression test asserting the generated command preserves
`HOME="$HOME"` for both stack and docker-compose deploys.

Fixes #4401

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

---------

Signed-off-by: Nahidujjaman Hridoy <hridoyboss12@gmail.com>
Co-authored-by: ngenohkevin <ngenohkevin19@gmail.com>
Co-authored-by: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com>
Co-authored-by: Mauricio Siu <siumauricio@icloud.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Volodymyr Kravchuk <volodymyr.kravch@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Nahidujjaman Hridoy <75487507+nhridoy@users.noreply.github.com>
Co-authored-by: Francis <9560564+Baker@users.noreply.github.com>
Co-authored-by: mixelburg <52622705+mixelburg@users.noreply.github.com>
Co-authored-by: Maks Pikov <mixelburg@users.noreply.github.com>
Co-authored-by: Jasael <67719321+jasael@users.noreply.github.com>
Co-authored-by: Philippe Parage <69145356+pparage@users.noreply.github.com>
Co-authored-by: youcef zr <93142224+youcefzemmar@users.noreply.github.com>
2026-05-30 16:01:52 -06:00
github-actions[bot]
a07106d649 🚀 Release v0.29.5 (#4475)
* fix(migrate-auth-secret): exit cleanly when there are no 2FA records

The empty-records branch of `main()` returned without calling
`process.exit(0)`, leaving the Drizzle Postgres connection pool
holding the event loop open. The `migrate-auth-secret` process
then hangs indefinitely after printing "No 2FA records found,
nothing to migrate." causing the upstream `0.29.3.sh` security
migration script (which calls this via `docker exec`) to never
reach its final `docker service update` step that mounts the new
Docker Secret. Operators end up with the new secret created but
the dokploy service still configured with the hardcoded
`BETTER_AUTH_SECRET`, while believing the migration completed.

Match the success branch a few lines below which already does
`process.exit(0)`, and the pattern used in sibling scripts
`reset-password.ts` and `reset-2fa.ts`.

Closes #4392

* feat(compose): add import from base64 in create service dropdown

Adds an "Import" option to the Create Service dropdown that lets users
paste a base64-encoded compose export, preview the template (compose YAML,
domains, envs, mounts) before confirming, and create the service only on
confirm. Adds a `previewTemplate` tRPC procedure that processes the base64
without touching the DB, with server access validation via session.

* [autofix.ci] apply automated fixes

* Enhance version synchronization workflow to include SDK repository

- Updated the GitHub Actions workflow to sync versioning across MCP, CLI, and SDK repositories.
- Added steps to bump the version in the SDK repository and regenerate tools from the latest OpenAPI spec.
- Improved commit message formatting to include source and release information for all repositories.
- Ensured successful synchronization messages for each repository after the version update.

* feat(deployment): add readLogs procedure to fetch deployment logs

- Introduced a new `readLogs` procedure that allows users to retrieve logs for a specific deployment by providing the deployment ID and an optional tail parameter.
- Implemented permission checks to ensure users have access to the requested logs.
- Enhanced log retrieval for both cloud and non-cloud environments, utilizing appropriate commands based on the server context.

Resolve https://github.com/Dokploy/mcp/issues/14

* feat(deployment): add server access validation for deployment actions

- Implemented server access validation in deployment procedures to ensure users can only access deployments associated with their active organization.
- Added checks to throw an UNAUTHORIZED error if a user attempts to access a deployment linked to a server outside their organization.

This enhancement improves security and access control within the deployment management system.

* feat(organization): prevent inviting users with owner role

- Added validation to prevent users from being invited with the owner role in the organization and user routers.
- Implemented TRPCError responses to ensure proper error handling when attempting to assign the owner role.
This change enhances role management and security within the organization structure.

https://github.com/Dokploy/dokploy/security/advisories/GHSA-fm9p-wmpw-gxjh

* feat(user): implement session cleanup on user update

- Added functionality to delete old sessions when a user updates their password, ensuring that only the current session remains active.
- This change enhances security by preventing unauthorized access from previous sessions after a password change.

Close here https://github.com/Dokploy/dokploy/security/advisories/GHSA-rr9m-w87g-46f3

* feat(settings): add copy button to server IP in web server settings (#4397)

* fix: copy Dokploy server IP when clicking server badge (#4390)

* fix: copy Dokploy server IP when clicking server badge

When a service runs on the local Dokploy server (no remote server),
clicking the server badge did nothing because `data.server` is null.
Now falls back to the server IP from settings so the badge always
copies an IP address.

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

* feat(copy-ip): implement IP address copying functionality across database service components

- Added the ability to copy the server IP address to the clipboard when clicking the server badge in various database service components (Libsql, MariaDB, MongoDB, MySQL, PostgreSQL, Redis).
- Integrated the `copy-to-clipboard` library and `sonner` for user feedback upon successful copy action.
- Ensured fallback to the server IP from settings when the service data is not available, enhancing user experience and functionality.

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Mauricio Siu <siumauricio@icloud.com>

* fix: responsive layout (#4391)

Signed-off-by: Nahidujjaman Hridoy <hridoyboss12@gmail.com>

* fix: automatically converting username to lowercase both in creation of register, and build for extra. (#4382)

* fix: allow square brackets in zip path validation for Next.js dynamic routes (#4468)

* fix: allow square brackets in zip drop path validation for Next.js dynamic routes

ZIP uploads containing Next.js dynamic route files (e.g. app/api/[id]/route.ts,
pages/[slug].tsx) were rejected by readValidDirectory because the path regex
did not include square bracket characters.

* [autofix.ci] apply automated fixes

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>

* fix: prevent webhook deploy crash when commit data lacks modified files (#4470)

shouldDeploy passed undefined/null entries from commit.modified straight
into micromatch, which throws "Expected input to be a string" and fails
every webhook deployment when watch paths are configured. Filter out
non-string values before matching.

* fix: add type="button" to TooltipTrigger in form components to prevent accidental submission (#4422)

Co-authored-by: Maks Pikov <mixelburg@users.noreply.github.com>

* fix: enable comment toggle shortcut in env variable editor (#4402) (#4473)

* fix: add tls=true label for domains when certificateType is none (#4018) (#4474)

* fix: add tls=true label for compose domains when certificateType is none (#4018)

* test: cover tls=true label for certificateType none, require https

* fix: scope tls fix to compose labels, leave traefik file config unchanged (#4018)

* chore: update version to v0.29.5 in package.json

---------

Signed-off-by: Nahidujjaman Hridoy <hridoyboss12@gmail.com>
Co-authored-by: ngenohkevin <ngenohkevin19@gmail.com>
Co-authored-by: Mauricio Siu <47042324+Siumauricio@users.noreply.github.com>
Co-authored-by: Mauricio Siu <siumauricio@icloud.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Volodymyr Kravchuk <volodymyr.kravch@gmail.com>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Co-authored-by: Nahidujjaman Hridoy <75487507+nhridoy@users.noreply.github.com>
Co-authored-by: Francis <9560564+Baker@users.noreply.github.com>
Co-authored-by: mixelburg <52622705+mixelburg@users.noreply.github.com>
Co-authored-by: Maks Pikov <mixelburg@users.noreply.github.com>
2026-05-22 17:21:12 -06:00
Mauricio Siu
df98cea19f Merge pull request #4381 from Dokploy/canary
🚀 Release v0.29.4
2026-05-11 13:46:38 -06:00
Mauricio Siu
ccc8f6d047 Merge pull request #4372 from Dokploy/canary
🚀 Release v0.29.3
2026-05-11 11:57:12 -06:00
Mauricio Siu
222b167a76 Merge pull request #4299 from Dokploy/canary
🚀 Release v0.29.2
2026-04-24 22:46:42 -06:00
Mauricio Siu
bad9731878 Merge pull request #4261 from Dokploy/canary
🚀 Release v0.29.1
2026-04-20 07:16:11 -06:00
Mauricio Siu
7e13243c1d Merge pull request #4155 from Dokploy/canary
🚀 Release v0.29.0
2026-04-17 14:10:37 -06:00
Mauricio Siu
4d8a2a38e8 Merge pull request #4029 from Dokploy/canary
🚀 Release v0.28.8
2026-03-18 21:43:35 -06:00
Mauricio Siu
de3db08e60 Merge pull request #4020 from Dokploy/canary
🚀 Release v0.28.7
2026-03-17 23:34:20 -06:00
Mauricio Siu
a2d655083a Merge pull request #3965 from Dokploy/canary
🚀 Release v0.28.6
2026-03-10 10:18:15 -06:00
Mauricio Siu
f3356cfe90 Merge pull request #3938 from Dokploy/canary
🚀 Release v0.28.5
2026-03-09 00:13:30 -06:00
Mauricio Siu
2362778fe1 Merge pull request #3907 from Dokploy/canary
🚀 Release v0.28.4
2026-03-06 11:51:08 -06:00
Mauricio Siu
628f16e8cb fix: update import statements to include file extensions for consistency 2026-03-03 15:34:56 -06:00
Mauricio Siu
ea8e99d76d Merge pull request #3875 from Dokploy/canary
🚀 Release v0.28.3
2026-03-03 15:05:02 -06:00
Mauricio Siu
d4719ece58 Merge pull request #3845 from Dokploy/canary
🚀 Release v0.28.2
2026-03-01 00:36:46 -06:00
Mauricio Siu
e679a322b9 Merge pull request #3825 from Dokploy/canary
🚀 Release v0.28.1
2026-02-27 14:02:42 -06:00
Mauricio Siu
f24f1ada5f Merge pull request #3805 from Dokploy/canary
🚀 Release v0.28.0
2026-02-27 02:02:04 -06:00
Mauricio Siu
5b6d80e177 Merge pull request #3682 from Dokploy/canary
🚀 Release v0.27.1
2026-02-18 01:54:44 -06:00
Mauricio Siu
2c9ca651a8 Merge pull request #3668 from Dokploy/canary
refactor(deployments): enhance deployment worker and queue handling f…
2026-02-10 03:16:04 -06:00
Mauricio Siu
413ed9bd80 Merge pull request #3604 from Dokploy/canary
🚀 Release v0.27.0
2026-02-10 02:06:41 -06:00
Mauricio Siu
4f578516d6 Merge pull request #3570 from Dokploy/canary
🚀 Release v0.26.7
2026-01-31 05:06:50 -06:00
Mauricio Siu
1e57d48ab4 Merge pull request #3499 from Dokploy/canary
🚀 Release v0.26.6
2026-01-27 13:42:24 -06:00
Mauricio Siu
a177d34dfd Merge pull request #3456 from Dokploy/canary
🚀 Release v0.26.5
2026-01-15 09:25:26 -06:00
Mauricio Siu
1034c79245 Merge pull request #3442 from Dokploy/canary
🚀 Release v0.26.4
2026-01-15 01:55:28 -06:00
Mauricio Siu
304454b22d Merge pull request #3312 from Dokploy/canary
🚀 Release v0.26.3
2026-01-01 22:37:09 -06:00
Mauricio Siu
42c2076281 Merge pull request #3254 from Dokploy/canary
🚀 Release v0.26.2
2025-12-13 01:41:50 -06:00
Mauricio Siu
5cd7de8188 Merge pull request #3211 from Dokploy/canary
🚀 Release v0.26.1
2025-12-10 00:47:12 -06:00
Mauricio Siu
1352b859e2 Merge pull request #3166 from Dokploy/canary
🚀 Release v0.26.0
2025-12-08 14:37:15 -06:00
Mauricio Siu
1c2307b86f Merge pull request #3114 from Dokploy/canary
🚀 Release v0.25.11
2025-11-26 03:41:51 -05:00
Mauricio Siu
4832fd929c Merge pull request #3072 from Dokploy/canary
🚀 Release v0.25.10
2025-11-20 08:59:37 -06:00
Mauricio Siu
d1b639a55a Merge pull request #3063 from Dokploy/canary
🚀 Release v0.25.9
2025-11-19 23:10:38 -06:00
Mauricio Siu
40de13e4d4 Merge pull request #3055 from Dokploy/canary
🚀 Release v0.25.8
2025-11-19 02:45:33 -06:00
Mauricio Siu
f0ea1c8796 Merge pull request #3043 from Dokploy/canary
🚀 Release v0.25.7
2025-11-18 23:01:38 -06:00
Mauricio Siu
b45e7e415c Merge pull request #2901 from Dokploy/canary
🚀 Release v0.25.6
2025-10-26 02:14:56 -06:00
Mauricio Siu
67d3e92aaf Merge pull request #2765 from Dokploy/canary
🚀 Release v0.25.5
2025-10-05 23:06:46 -06:00
Mauricio Siu
76af74d8aa Merge pull request #2721 from Dokploy/canary
🚀 Release v0.25.4
2025-09-29 23:06:29 -06:00
Mauricio Siu
b15ede8877 Merge pull request #2658 from Dokploy/canary
🚀 Release v0.25.3
2025-09-21 16:25:37 -06:00
Mauricio Siu
ea805c1520 Merge pull request #2612 from Dokploy/canary
🚀 Release v0.25.2
2025-09-15 23:44:43 -06:00
Mauricio Siu
976932fb03 Merge pull request #2557 from Dokploy/canary
🚀 Release v0.25.1
2025-09-07 14:03:25 -06:00
Mauricio Siu
ac8960efdd Merge pull request #2483 from Dokploy/canary
🚀 Release v0.25.0
2025-09-06 22:41:45 -06:00
Mauricio Siu
d6050ce05a Merge pull request #2408 from Dokploy/canary
🚀 Release v0.24.12
2025-08-19 00:44:43 -06:00
Mauricio Siu
5a46b879f5 Merge pull request #2390 from Dokploy/canary
🚀 Release v0.24.11
2025-08-17 15:26:03 -06:00
Mauricio Siu
222e4878bd Merge pull request #2360 from Dokploy/canary
🚀 Release v0.24.10
2025-08-10 23:28:58 -06:00
Mauricio Siu
fd267a64de Merge pull request #2354 from Dokploy/canary
🚀 Release v0.24.9
2025-08-10 06:13:01 -06:00
Mauricio Siu
fa3cdf148b Merge pull request #2324 from Dokploy/canary
🚀 Release v0.24.8
2025-08-04 00:28:40 -06:00
Mauricio Siu
74caf141f4 Merge pull request #2323 from Dokploy/canary
🚀 Release v0.24.7
2025-08-03 18:58:05 -06:00
Mauricio Siu
8b7d9c0896 Merge pull request #2303 from Dokploy/canary
🚀 Release v0.24.6
2025-08-03 02:42:22 -06:00
Mauricio Siu
13e20e9ef8 Merge pull request #2253 from Dokploy/canary
🚀 Release v0.24.5
2025-07-28 02:15:50 -06:00
Mauricio Siu
f9b0589070 Merge pull request #2219 from Dokploy/canary
🚀 Release v0.24.4
2025-07-20 20:40:02 -06:00
Mauricio Siu
b615d04ad2 Merge pull request #2193 from Dokploy/canary
🚀 Release v0.24.3
2025-07-13 23:44:03 -06:00
Mauricio Siu
6c4efa48b1 Merge pull request #2191 from Dokploy/canary
🚀 Release v0.24.2
2025-07-13 20:46:50 -06:00
Mauricio Siu
85d48aba2b Merge pull request #2183 from Dokploy/canary
🚀 Release v0.24.1
2025-07-13 13:57:09 -06:00
Mauricio Siu
3b138f8e8a Merge pull request #2143 from Dokploy/canary
🚀 Release v0.24.0
2025-07-06 21:40:17 -06:00
Mauricio Siu
b91067dc2a Merge pull request #2126 from Dokploy/canary
🚀 Release v0.23.7
2025-07-05 01:59:04 -06:00
Mauricio Siu
335a16b915 Merge pull request #2114 from Dokploy/canary
🚀 Release v0.23.6
2025-07-01 23:31:59 -06:00
Mauricio Siu
274f38029c Merge pull request #2103 from Dokploy/canary
🚀 Release v0.23.5
2025-06-29 14:14:02 -06:00
Mauricio Siu
4cbc91d3d0 Merge pull request #2091 from Dokploy/canary
🚀 Release v0.23.4
2025-06-27 00:09:51 -06:00
Mauricio Siu
10d17de186 Merge pull request #2070 from Dokploy/canary
🚀 Release v0.23.3
2025-06-22 18:36:40 +02:00
Mauricio Siu
65f0919fa7 Merge pull request #2068 from Dokploy/canary
🚀 Release v0.23.2
2025-06-22 16:46:53 +02:00
Mauricio Siu
9b7abfbed7 Merge pull request #2063 from Dokploy/canary
🚀 Release v0.23.1
2025-06-22 08:58:23 +02:00
Mauricio Siu
6676a86b34 Merge pull request #2061 from Dokploy/canary
🚀 Release v0.23.0
2025-06-22 08:27:01 +02:00
Mauricio Siu
d603654ac1 Merge pull request #1965 from Dokploy/canary
🚀 Release v0.22.7
2025-05-28 02:52:40 -06:00
Mauricio Siu
d9ffe519b0 Merge pull request #1920 from Dokploy/canary
🚀 Release v0.22.6
2025-05-18 02:32:42 -06:00
Mauricio Siu
fa91a74462 Merge pull request #1911 from Dokploy/canary
🚀 Release v0.22.5
2025-05-17 16:19:10 -06:00
Mauricio Siu
d7794286be Merge pull request #1871 from Dokploy/canary
🚀 Release v0.22.4
2025-05-10 20:59:48 -06:00
Mauricio Siu
f337dd7e01 Merge pull request #1847 from Dokploy/canary
🚀 Release v0.22.3
2025-05-06 23:57:22 -06:00
Mauricio Siu
5d5d95bbd3 Merge pull request #1836 from Dokploy/canary
🚀 Release v0.22.2
2025-05-06 02:56:01 -06:00
Mauricio Siu
7be1084a10 Merge pull request #1828 from Dokploy/canary
🚀 Release v0.22.1
2025-05-05 03:10:22 -06:00
Mauricio Siu
19a525fac1 Merge pull request #1824 from Dokploy/canary
🚀 Release v0.22.0
2025-05-04 23:22:21 -06:00
Mauricio Siu
7984497398 Merge pull request #1785 from Dokploy/canary
🚀 Release v0.21.8
2025-04-27 00:18:55 -06:00
81 changed files with 17441 additions and 9959 deletions

8403
api-1.json

File diff suppressed because it is too large Load Diff

View File

@@ -1,6 +1,3 @@
DATABASE_URL="postgres://dokploy:amukds4wi9001583845717ad2@localhost:5432/dokploy"
PORT=3000
NODE_ENV=development
# Managed Servers (Dokploy Cloud only) — API token from https://hpanel.hostinger.com/profile/api
HOSTINGER_API_KEY=

View File

@@ -0,0 +1,52 @@
import { getBuildComposeCommand } from "@dokploy/server/utils/builders/compose";
import { describe, expect, it, vi } from "vitest";
// Isolate the command builder from the compose-file I/O performed by
// writeDomainsToCompose; we only care about the docker invocation it emits.
vi.mock("@dokploy/server/utils/docker/domain", () => ({
writeDomainsToCompose: vi.fn().mockResolvedValue(""),
}));
const baseCompose = {
appName: "my-app",
sourceType: "raw",
command: "",
composePath: "docker-compose.yml",
composeType: "stack",
isolatedDeployment: false,
randomize: false,
suffix: "",
serverId: null,
env: "",
mounts: [],
domains: [],
environment: { project: { env: "" }, env: "" },
} as unknown as Parameters<typeof getBuildComposeCommand>[0];
// Regression coverage for #4401: the deploy command runs under `env -i`, which
// clears the environment except for the vars listed explicitly. HOME must be
// preserved so docker can resolve ~/.docker/config.json — otherwise
// `docker stack deploy --with-registry-auth` ships no credentials to the swarm
// and private-registry images fail to pull.
describe("getBuildComposeCommand registry auth (#4401)", () => {
it("preserves HOME for swarm stack deploys", async () => {
const command = await getBuildComposeCommand({
...baseCompose,
composeType: "stack",
});
expect(command).toContain("stack deploy");
expect(command).toContain("--with-registry-auth");
expect(command).toContain('env -i PATH="$PATH" HOME="$HOME"');
});
it("preserves HOME for docker compose deploys", async () => {
const command = await getBuildComposeCommand({
...baseCompose,
composeType: "docker-compose",
});
expect(command).toContain("compose -p my-app");
expect(command).toContain('env -i PATH="$PATH" HOME="$HOME"');
});
});

View File

@@ -103,6 +103,51 @@ describe("createDomainLabels", () => {
);
});
it("should add tls=true for certificateType none on websecure entrypoint", async () => {
const noneDomain = {
...baseDomain,
https: true,
certificateType: "none" as const,
};
const labels = await createDomainLabels(appName, noneDomain, "websecure");
expect(labels).toContain(
"traefik.http.routers.test-app-1-websecure.tls=true",
);
// no cert resolver should be set when relying on a default/custom cert
expect(labels).not.toContain(
"traefik.http.routers.test-app-1-websecure.tls.certresolver=letsencrypt",
);
});
it("should not add tls=true for certificateType none on web entrypoint", async () => {
const noneDomain = {
...baseDomain,
https: true,
certificateType: "none" as const,
};
const labels = await createDomainLabels(appName, noneDomain, "web");
expect(labels).not.toContain(
"traefik.http.routers.test-app-1-web.tls=true",
);
});
it("should add tls=true for certificateType none on a custom https entrypoint", async () => {
const noneDomain = {
...baseDomain,
https: true,
customEntrypoint: "websecure-custom",
certificateType: "none" as const,
};
const labels = await createDomainLabels(
appName,
noneDomain,
"websecure-custom",
);
expect(labels).toContain(
"traefik.http.routers.test-app-1-websecure-custom.tls=true",
);
});
it("should handle different ports correctly", async () => {
const customPortDomain = { ...baseDomain, port: 3000 };
const labels = await createDomainLabels(appName, customPortDomain, "web");

View File

@@ -0,0 +1,41 @@
import { shouldDeploy } from "@dokploy/server";
import { describe, expect, it } from "vitest";
describe("shouldDeploy", () => {
it("should deploy when no watch paths are configured", () => {
expect(shouldDeploy(null, ["src/index.ts"])).toBe(true);
expect(shouldDeploy([], ["src/index.ts"])).toBe(true);
});
it("should deploy when watch paths match modified files", () => {
expect(shouldDeploy(["src/**"], ["src/index.ts"])).toBe(true);
expect(shouldDeploy(["apps/web/**"], ["apps/web/page.tsx"])).toBe(true);
});
it("should not deploy when watch paths do not match", () => {
expect(shouldDeploy(["src/**"], ["docs/readme.md"])).toBe(false);
});
it("should not throw when modified files contain non-string values", () => {
expect(() =>
shouldDeploy(["src/**"], ["src/index.ts", undefined, null] as any),
).not.toThrow();
expect(
shouldDeploy(["src/**"], ["src/index.ts", undefined, null] as any),
).toBe(true);
});
it("should not throw when modified files are undefined or null", () => {
expect(() => shouldDeploy(["src/**"], undefined)).not.toThrow();
expect(() => shouldDeploy(["src/**"], null)).not.toThrow();
expect(shouldDeploy(["src/**"], undefined)).toBe(false);
expect(shouldDeploy(["src/**"], null)).toBe(false);
});
it("should not throw when every modified file is non-string", () => {
expect(() =>
shouldDeploy(["src/**"], [undefined, undefined] as any),
).not.toThrow();
expect(shouldDeploy(["src/**"], [undefined, undefined] as any)).toBe(false);
});
});

View File

@@ -58,7 +58,7 @@ beforeEach(() => {
vi.clearAllMocks();
});
describe("static roles bypass enterprise resources", () => {
describe("owner and admin bypass enterprise resources", () => {
it("owner bypasses deployment.read", async () => {
memberToReturn = mockMemberData("owner");
await expect(
@@ -73,15 +73,8 @@ describe("static roles bypass enterprise resources", () => {
).resolves.toBeUndefined();
});
it("member bypasses schedule.delete", async () => {
memberToReturn = mockMemberData("member");
await expect(
checkPermission(ctx, { schedule: ["delete"] }),
).resolves.toBeUndefined();
});
it("member bypasses multiple enterprise permissions at once", async () => {
memberToReturn = mockMemberData("member");
it("owner bypasses multiple enterprise permissions at once", async () => {
memberToReturn = mockMemberData("owner");
await expect(
checkPermission(ctx, {
deployment: ["read"],
@@ -92,6 +85,55 @@ describe("static roles bypass enterprise resources", () => {
});
});
describe("member is denied org-level enterprise resources (CVE: bypass via staticRoles)", () => {
it("member is denied registry.read", async () => {
memberToReturn = mockMemberData("member");
await expect(
checkPermission(ctx, { registry: ["read"] }),
).rejects.toThrow();
});
it("member is denied certificate.read", async () => {
memberToReturn = mockMemberData("member");
await expect(
checkPermission(ctx, { certificate: ["read"] }),
).rejects.toThrow();
});
it("member is denied destination.read", async () => {
memberToReturn = mockMemberData("member");
await expect(
checkPermission(ctx, { destination: ["read"] }),
).rejects.toThrow();
});
it("member is denied notification.read", async () => {
memberToReturn = mockMemberData("member");
await expect(
checkPermission(ctx, { notification: ["read"] }),
).rejects.toThrow();
});
it("member is denied auditLog.read", async () => {
memberToReturn = mockMemberData("member");
await expect(
checkPermission(ctx, { auditLog: ["read"] }),
).rejects.toThrow();
});
it("member is denied server.read", async () => {
memberToReturn = mockMemberData("member");
await expect(checkPermission(ctx, { server: ["read"] })).rejects.toThrow();
});
it("member is denied registry.create", async () => {
memberToReturn = mockMemberData("member");
await expect(
checkPermission(ctx, { registry: ["create"] }),
).rejects.toThrow();
});
});
describe("static roles validate free-tier resources", () => {
it("owner passes project.create", async () => {
memberToReturn = mockMemberData("owner");

View File

@@ -65,6 +65,8 @@ const baseSettings: WebServerSettings = {
cleanupCacheApplications: false,
cleanupCacheOnCompose: false,
cleanupCacheOnPreviews: false,
remoteServersOnly: false,
enforceSSO: false,
createdAt: null,
updatedAt: new Date(),
};

View File

@@ -78,4 +78,20 @@ describe("readValidDirectory (path traversal)", () => {
it("returns false for empty string (resolves to cwd)", () => {
expect(readValidDirectory("")).toBe(false);
});
it("returns true for Next.js dynamic route paths with square brackets", () => {
expect(
readValidDirectory(
`${BASE}/applications/myapp/code/app/api/[id]/route.ts`,
),
).toBe(true);
expect(
readValidDirectory(`${BASE}/applications/myapp/code/pages/[slug].tsx`),
).toBe(true);
expect(
readValidDirectory(
`${BASE}/applications/myapp/code/app/[...catch]/page.tsx`,
),
).toBe(true);
});
});

View File

@@ -224,7 +224,7 @@ export const ShowResources = ({ id, type }: Props) => {
<FormLabel>Memory Limit</FormLabel>
<TooltipProvider>
<Tooltip delayDuration={0}>
<TooltipTrigger>
<TooltipTrigger type="button">
<InfoIcon className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
@@ -263,7 +263,7 @@ export const ShowResources = ({ id, type }: Props) => {
<FormLabel>Memory Reservation</FormLabel>
<TooltipProvider>
<Tooltip delayDuration={0}>
<TooltipTrigger>
<TooltipTrigger type="button">
<InfoIcon className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
@@ -303,7 +303,7 @@ export const ShowResources = ({ id, type }: Props) => {
<FormLabel>CPU Limit</FormLabel>
<TooltipProvider>
<Tooltip delayDuration={0}>
<TooltipTrigger>
<TooltipTrigger type="button">
<InfoIcon className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
@@ -343,7 +343,7 @@ export const ShowResources = ({ id, type }: Props) => {
<FormLabel>CPU Reservation</FormLabel>
<TooltipProvider>
<Tooltip delayDuration={0}>
<TooltipTrigger>
<TooltipTrigger type="button">
<InfoIcon className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent>
@@ -379,7 +379,7 @@ export const ShowResources = ({ id, type }: Props) => {
<FormLabel className="text-base">Ulimits</FormLabel>
<TooltipProvider>
<Tooltip delayDuration={0}>
<TooltipTrigger>
<TooltipTrigger type="button">
<InfoIcon className="h-4 w-4 text-muted-foreground" />
</TooltipTrigger>
<TooltipContent className="max-w-xs">

View File

@@ -806,7 +806,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
<FormLabel>Middlewares</FormLabel>
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<TooltipTrigger type="button">
<div className="size-4 rounded-full bg-muted flex items-center justify-center text-[10px] font-bold">
?
</div>

View File

@@ -1,3 +1,4 @@
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { CheckIcon, ChevronsUpDown, HelpCircle, X } from "lucide-react";
import Link from "next/link";
@@ -5,7 +6,6 @@ import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
import { BitbucketIcon } from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block";
import { Badge } from "@/components/ui/badge";

View File

@@ -1,3 +1,4 @@
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { HelpCircle, KeyRoundIcon, LockIcon, X } from "lucide-react";
import Link from "next/link";
@@ -6,7 +7,6 @@ import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
import { GitIcon } from "@/components/icons/data-tools-icons";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";

View File

@@ -1,3 +1,4 @@
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react";
import Link from "next/link";
@@ -5,7 +6,6 @@ import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
import { GiteaIcon } from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block";
import { Badge } from "@/components/ui/badge";

View File

@@ -1,3 +1,4 @@
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react";
import Link from "next/link";
@@ -5,7 +6,6 @@ import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
import { GithubIcon } from "@/components/icons/data-tools-icons";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";

View File

@@ -1,3 +1,4 @@
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react";
import Link from "next/link";
@@ -5,7 +6,6 @@ import { useEffect, useMemo } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
import { GitlabIcon } from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block";
import { Badge } from "@/components/ui/badge";

View File

@@ -1,3 +1,4 @@
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
import Link from "next/link";
@@ -5,7 +6,6 @@ import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
import { BitbucketIcon } from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block";
import { Badge } from "@/components/ui/badge";
@@ -422,7 +422,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
<FormLabel>Watch Paths</FormLabel>
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<TooltipTrigger type="button">
<div className="size-4 rounded-full bg-muted flex items-center justify-center text-[10px] font-bold">
?
</div>

View File

@@ -1,3 +1,4 @@
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { HelpCircle, KeyRoundIcon, LockIcon, X } from "lucide-react";
import Link from "next/link";
@@ -6,7 +7,6 @@ import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
import { GitIcon } from "@/components/icons/data-tools-icons";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";

View File

@@ -1,3 +1,4 @@
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react";
import Link from "next/link";
@@ -5,7 +6,6 @@ import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
import { GiteaIcon } from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block";
import { Badge } from "@/components/ui/badge";

View File

@@ -449,7 +449,7 @@ export const SaveGithubProviderCompose = ({ composeId }: Props) => {
<FormLabel>Watch Paths</FormLabel>
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<TooltipTrigger type="button">
<div className="size-4 rounded-full bg-muted flex items-center justify-center text-[10px] font-bold">
?
</div>

View File

@@ -1,3 +1,4 @@
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { CheckIcon, ChevronsUpDown, X } from "lucide-react";
import Link from "next/link";
@@ -5,7 +6,6 @@ import { useEffect, useMemo } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { VALID_BRANCH_REGEX } from "@dokploy/server/utils/git-branch-validation";
import { GitlabIcon } from "@/components/icons/data-tools-icons";
import { AlertBlock } from "@/components/shared/alert-block";
import { Badge } from "@/components/ui/badge";
@@ -440,7 +440,7 @@ export const SaveGitlabProviderCompose = ({ composeId }: Props) => {
<FormLabel>Watch Paths</FormLabel>
<TooltipProvider>
<Tooltip>
<TooltipTrigger>
<TooltipTrigger type="button">
<div className="size-4 rounded-full bg-muted flex items-center justify-center text-[10px] font-bold">
?
</div>

View File

@@ -71,6 +71,9 @@ interface Props {
export const AddApplication = ({ environmentId, projectName }: Props) => {
const utils = api.useUtils();
const { data: isCloud } = api.settings.isCloud.useQuery();
const { data: webServerSettings } =
api.settings.getWebServerSettings.useQuery();
const showLocalOption = !isCloud && !webServerSettings?.remoteServersOnly;
const [visible, setVisible] = useState(false);
const slug = slugify(projectName);
const { data: servers } = api.server.withSSHKey.useQuery();
@@ -171,7 +174,8 @@ export const AddApplication = ({ environmentId, projectName }: Props) => {
<Tooltip>
<TooltipTrigger asChild>
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
Select a Server {!isCloud ? "(Optional)" : ""}
Select a Server{" "}
{showLocalOption ? "(Optional)" : ""}
<HelpCircle className="size-4 text-muted-foreground" />
</FormLabel>
</TooltipTrigger>
@@ -191,17 +195,19 @@ export const AddApplication = ({ environmentId, projectName }: Props) => {
<Select
onValueChange={field.onChange}
defaultValue={
field.value || (!isCloud ? "dokploy" : undefined)
field.value || (showLocalOption ? "dokploy" : undefined)
}
>
<SelectTrigger>
<SelectValue
placeholder={!isCloud ? "Dokploy" : "Select a Server"}
placeholder={
showLocalOption ? "Dokploy" : "Select a Server"
}
/>
</SelectTrigger>
<SelectContent>
<SelectGroup>
{!isCloud && (
{showLocalOption && (
<SelectItem value="dokploy">
<span className="flex items-center gap-2 justify-between w-full">
<span>Dokploy</span>
@@ -225,7 +231,8 @@ export const AddApplication = ({ environmentId, projectName }: Props) => {
</SelectItem>
))}
<SelectLabel>
Servers ({servers?.length + (!isCloud ? 1 : 0)})
Servers (
{servers?.length + (showLocalOption ? 1 : 0)})
</SelectLabel>
</SelectGroup>
</SelectContent>

View File

@@ -74,6 +74,9 @@ export const AddCompose = ({ environmentId, projectName }: Props) => {
const [visible, setVisible] = useState(false);
const slug = slugify(projectName);
const { data: isCloud } = api.settings.isCloud.useQuery();
const { data: webServerSettings } =
api.settings.getWebServerSettings.useQuery();
const showLocalOption = !isCloud && !webServerSettings?.remoteServersOnly;
const { data: servers } = api.server.withSSHKey.useQuery();
const { mutateAsync, isPending, error, isError } =
api.compose.create.useMutation();
@@ -182,7 +185,8 @@ export const AddCompose = ({ environmentId, projectName }: Props) => {
<Tooltip>
<TooltipTrigger asChild>
<FormLabel className="break-all w-fit flex flex-row gap-1 items-center">
Select a Server {!isCloud ? "(Optional)" : ""}
Select a Server{" "}
{showLocalOption ? "(Optional)" : ""}
<HelpCircle className="size-4 text-muted-foreground" />
</FormLabel>
</TooltipTrigger>
@@ -202,17 +206,19 @@ export const AddCompose = ({ environmentId, projectName }: Props) => {
<Select
onValueChange={field.onChange}
defaultValue={
field.value || (!isCloud ? "dokploy" : undefined)
field.value || (showLocalOption ? "dokploy" : undefined)
}
>
<SelectTrigger>
<SelectValue
placeholder={!isCloud ? "Dokploy" : "Select a Server"}
placeholder={
showLocalOption ? "Dokploy" : "Select a Server"
}
/>
</SelectTrigger>
<SelectContent>
<SelectGroup>
{!isCloud && (
{showLocalOption && (
<SelectItem value="dokploy">
<span className="flex items-center gap-2 justify-between w-full">
<span>Dokploy</span>
@@ -236,7 +242,8 @@ export const AddCompose = ({ environmentId, projectName }: Props) => {
</SelectItem>
))}
<SelectLabel>
Servers ({servers?.length + (!isCloud ? 1 : 0)})
Servers (
{servers?.length + (showLocalOption ? 1 : 0)})
</SelectLabel>
</SelectGroup>
</SelectContent>

View File

@@ -219,6 +219,9 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
const [visible, setVisible] = useState(false);
const slug = slugify(projectName);
const { data: isCloud } = api.settings.isCloud.useQuery();
const { data: webServerSettings } =
api.settings.getWebServerSettings.useQuery();
const showLocalOption = !isCloud && !webServerSettings?.remoteServersOnly;
const { data: servers } = api.server.withSSHKey.useQuery();
const libsqlMutation = api.libsql.create.useMutation();
const mariadbMutation = api.mariadb.create.useMutation();
@@ -470,19 +473,20 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
<Select
onValueChange={field.onChange}
defaultValue={
field.value || (!isCloud ? "dokploy" : undefined)
field.value ||
(showLocalOption ? "dokploy" : undefined)
}
>
<SelectTrigger>
<SelectValue
placeholder={
!isCloud ? "Dokploy" : "Select a Server"
showLocalOption ? "Dokploy" : "Select a Server"
}
/>
</SelectTrigger>
<SelectContent>
<SelectGroup>
{!isCloud && (
{showLocalOption && (
<SelectItem value="dokploy">
<span className="flex items-center gap-2 justify-between w-full">
<span>Dokploy</span>
@@ -501,7 +505,8 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
</SelectItem>
))}
<SelectLabel>
Servers ({servers?.length + (!isCloud ? 1 : 0)})
Servers (
{servers?.length + (showLocalOption ? 1 : 0)})
</SelectLabel>
</SelectGroup>
</SelectContent>

View File

@@ -1,4 +1,4 @@
import { CreditCard, FileText, Server } from "lucide-react";
import { CreditCard, FileText } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
import {
@@ -17,11 +17,6 @@ const navigationItems = [
href: "/dashboard/settings/billing",
icon: CreditCard,
},
{
name: "Managed Servers",
href: "/dashboard/settings/managed-servers",
icon: Server,
},
{
name: "Invoices",
href: "/dashboard/settings/invoices",

View File

@@ -9,7 +9,6 @@ import {
Loader2,
MinusIcon,
PlusIcon,
Server,
ShieldCheck,
} from "lucide-react";
import Link from "next/link";
@@ -83,11 +82,6 @@ const navigationItems = [
href: "/dashboard/settings/billing",
icon: CreditCard,
},
{
name: "Managed Servers",
href: "/dashboard/settings/managed-servers",
icon: Server,
},
{
name: "Invoices",
href: "/dashboard/settings/invoices",

View File

@@ -1,493 +0,0 @@
import {
AlertCircle,
CheckCircle2,
Clock,
CreditCard,
ExternalLink,
FileText,
Loader2,
Plus,
Server,
Trash2,
XCircle,
} from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
import { useState } from "react";
import { toast } from "sonner";
import { DialogAction } from "@/components/shared/dialog-action";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { Label } from "@/components/ui/label";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select";
import { cn } from "@/lib/utils";
import { api } from "@/utils/api";
const navigationItems = [
{
name: "Subscription",
href: "/dashboard/settings/billing",
icon: CreditCard,
},
{
name: "Managed Servers",
href: "/dashboard/settings/managed-servers",
icon: Server,
},
{
name: "Invoices",
href: "/dashboard/settings/invoices",
icon: FileText,
},
];
const STATUS_MAP: Record<
string,
{
label: string;
icon: React.ReactNode;
variant: "default" | "secondary" | "destructive" | "outline";
}
> = {
pending: {
label: "Pending",
icon: <Clock className="size-3" />,
variant: "secondary",
},
provisioning: {
label: "Provisioning",
icon: <Loader2 className="size-3 animate-spin" />,
variant: "secondary",
},
configuring: {
label: "Installing Dokploy",
icon: <Loader2 className="size-3 animate-spin" />,
variant: "secondary",
},
ready: {
label: "Ready",
icon: <CheckCircle2 className="size-3" />,
variant: "default",
},
error: {
label: "Error",
icon: <XCircle className="size-3" />,
variant: "destructive",
},
terminating: {
label: "Terminating",
icon: <Loader2 className="size-3 animate-spin" />,
variant: "secondary",
},
terminated: {
label: "Terminated",
icon: <AlertCircle className="size-3" />,
variant: "outline",
},
};
function formatSpecs(cpus: number, memoryMb: number, diskMb: number, bandwidthMb: number) {
const bandwidthTb = bandwidthMb / 1024 / 1024;
const bandwidthLabel = bandwidthTb >= 1 ? `${bandwidthTb.toFixed(0)} TB` : `${Math.round(bandwidthMb / 1024)} GB`;
return `${cpus} vCPU · ${Math.round(memoryMb / 1024)} GB RAM · ${Math.round(diskMb / 1024)} GB NVMe · ${bandwidthLabel} bandwidth`;
}
function centsToDisplay(cents: number) {
return (cents / 100).toFixed(2).replace(/\.00$/, "");
}
function OrderServerDialog({ onSuccess }: { onSuccess: () => void }) {
const [open, setOpen] = useState(false);
const [selectedPlan, setSelectedPlan] = useState<string>("");
const [selectedDc, setSelectedDc] = useState<string>("");
const [isAnnual, setIsAnnual] = useState(false);
const { data: plans, isLoading: loadingPlans } =
api.managedServer.getPlans.useQuery(undefined, { enabled: open });
const { data: dataCenters, isLoading: loadingDcs } =
api.managedServer.getDataCenters.useQuery(undefined, { enabled: open });
const isLoadingOptions = loadingPlans || loadingDcs;
const purchase = api.managedServer.purchase.useMutation({
onSuccess: () => {
toast.success("Server order placed! Provisioning will take ~5 minutes.");
setOpen(false);
onSuccess();
},
onError: (err) => {
toast.error(err.message);
},
});
const plan = plans?.find((p) => p.id === selectedPlan);
const displayPrice = (p: NonNullable<typeof plan>) =>
isAnnual
? `$${centsToDisplay(p.dokployPriceCentsAnnual)}/yr`
: `$${centsToDisplay(p.dokployPriceCentsMonthly)}/mo`;
const displayPriceSmall = (p: NonNullable<typeof plan>) =>
isAnnual
? `$${centsToDisplay(Math.round(p.dokployPriceCentsAnnual / 12))}/mo billed annually`
: `$${centsToDisplay(p.dokployPriceCentsAnnual)}/yr if annual`;
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>
<Button size="sm">
<Plus className="size-4 mr-2" />
Order Server
</Button>
</DialogTrigger>
<DialogContent className="max-w-lg max-h-[90vh] overflow-y-auto">
<DialogHeader>
<DialogTitle>Order a Managed Server</DialogTitle>
<DialogDescription>
We'll provision and configure a server for you automatically. Ready
in ~5 minutes.
</DialogDescription>
</DialogHeader>
<div className="space-y-4 pt-2">
{isLoadingOptions ? (
<div className="flex flex-col items-center justify-center py-8 gap-3 text-muted-foreground">
<Loader2 className="size-6 animate-spin" />
<p className="text-sm">Loading available plans...</p>
</div>
) : (
<div className="space-y-4">
{/* Billing period toggle */}
<div className="flex items-center gap-1 rounded-lg border p-1 bg-muted/40 w-fit">
<button
type="button"
onClick={() => setIsAnnual(false)}
className={cn(
"px-4 py-1.5 rounded-md text-sm font-medium transition-colors",
!isAnnual
? "bg-background shadow-sm text-foreground"
: "text-muted-foreground hover:text-foreground",
)}
>
Monthly
</button>
<button
type="button"
onClick={() => setIsAnnual(true)}
className={cn(
"px-4 py-1.5 rounded-md text-sm font-medium transition-colors flex items-center gap-1.5",
isAnnual
? "bg-background shadow-sm text-foreground"
: "text-muted-foreground hover:text-foreground",
)}
>
Annual
<span className="text-xs bg-green-500/15 text-green-600 dark:text-green-400 px-1.5 py-0.5 rounded font-semibold">
Save ~20%
</span>
</button>
</div>
{/* Plan selector */}
<div className="space-y-2">
<Label>Plan</Label>
<div className="grid gap-2">
{plans?.map((p) => (
<button
key={p.id}
type="button"
onClick={() => setSelectedPlan(p.id)}
className={cn(
"flex items-center justify-between rounded-lg border p-3 text-left transition-colors",
selectedPlan === p.id
? "border-primary bg-primary/5"
: "border-border hover:border-muted-foreground",
)}
>
<div>
<p className="font-medium text-sm">{p.name}</p>
<p className="text-xs text-muted-foreground">
{formatSpecs(p.cpus, p.memoryMb, p.diskMb, p.bandwidthMb)}
</p>
</div>
<div className="text-right">
<p className="font-semibold text-sm">
{displayPrice(p)}
</p>
<p className="text-xs text-muted-foreground">
{displayPriceSmall(p)}
</p>
</div>
</button>
))}
</div>
</div>
{/* Data center selector */}
<div className="space-y-2">
<Label>Data Center</Label>
<Select value={selectedDc} onValueChange={setSelectedDc}>
<SelectTrigger>
<SelectValue placeholder="Select a location..." />
</SelectTrigger>
<SelectContent position="popper" side="bottom" sideOffset={4} className="max-h-56 overflow-y-auto">
{dataCenters?.map((dc) => (
<SelectItem key={dc.id} value={String(dc.id)}>
{dc.city} — {dc.continent}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{plan && selectedDc && (
<div className="rounded-lg bg-muted p-3 text-sm space-y-1">
<div className="flex justify-between">
<span className="text-muted-foreground">Plan</span>
<span className="font-medium">{plan.name}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Billing</span>
<span className="font-medium">{isAnnual ? "Annual" : "Monthly"}</span>
</div>
<div className="flex justify-between">
<span className="text-muted-foreground">Total</span>
<span className="font-semibold">{displayPrice(plan)}</span>
</div>
</div>
)}
<Button
className="w-full"
disabled={!selectedPlan || !selectedDc || purchase.isPending}
onClick={() => {
if (!selectedPlan || !selectedDc) return;
purchase.mutate({
plan: selectedPlan,
dataCenterId: Number(selectedDc),
isAnnual,
});
}}
>
{purchase.isPending ? (
<>
<Loader2 className="size-4 mr-2 animate-spin" />
Placing order...
</>
) : (
"Order Server"
)}
</Button>
</div>
)}
</div>
</DialogContent>
</Dialog>
);
}
export const ShowManagedServers = () => {
const router = useRouter();
const utils = api.useUtils();
const { data: servers, isLoading } = api.managedServer.list.useQuery();
const syncStatus = api.managedServer.syncStatus.useMutation({
onSuccess: () => utils.managedServer.list.invalidate(),
});
const deleteServer = api.managedServer.delete.useMutation({
onSuccess: () => {
toast.success("Server terminated.");
utils.managedServer.list.invalidate();
},
onError: (err) => toast.error(err.message),
});
return (
<div className="w-full">
<Card className="bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
<div className="rounded-xl bg-background shadow-md">
<CardHeader>
<CardTitle className="text-xl flex flex-row gap-2">
<Server className="size-6 text-muted-foreground self-center" />
Billing
</CardTitle>
<CardDescription>
Manage your subscription and servers
</CardDescription>
</CardHeader>
<CardContent className="space-y-4 py-4 border-t">
<nav className="flex space-x-2 border-b">
{navigationItems.map((item) => {
const Icon = item.icon;
const isActive = router.pathname === item.href;
return (
<Link
key={item.name}
href={item.href}
className={cn(
"flex items-center gap-2 px-4 py-2 text-sm font-medium border-b-2 transition-colors",
isActive
? "border-primary text-primary"
: "border-transparent text-muted-foreground hover:text-primary hover:border-muted",
)}
>
<Icon className="h-4 w-4" />
{item.name}
</Link>
);
})}
</nav>
<div className="mt-6 space-y-4">
<div className="flex items-center justify-between">
<div>
<h3 className="font-semibold text-base">Managed Servers</h3>
<p className="text-sm text-muted-foreground">
Servers provisioned and managed by Dokploy Cloud
</p>
</div>
<OrderServerDialog
onSuccess={() => utils.managedServer.list.invalidate()}
/>
</div>
{isLoading ? (
<div className="flex justify-center py-8">
<Loader2 className="size-6 animate-spin text-muted-foreground" />
</div>
) : servers?.length === 0 ? (
<div className="text-center py-12 border rounded-lg border-dashed">
<Server className="size-10 mx-auto text-muted-foreground mb-3" />
<p className="text-sm font-medium">No managed servers yet</p>
<p className="text-xs text-muted-foreground mt-1">
Order a server and we'll provision and configure it for you
automatically.
</p>
</div>
) : (
<div className="space-y-3">
{servers?.map((s) => {
const status =
STATUS_MAP[s.status] ?? STATUS_MAP.error!;
const isProvisioning = [
"pending",
"provisioning",
"configuring",
].includes(s.status);
const planLabel = s.plan
.split("-")
.slice(-2)
.join(" ")
.toUpperCase();
return (
<div
key={s.managedServerId}
className="flex items-center justify-between rounded-lg border p-4"
>
<div className="flex items-center gap-3">
<Server className="size-5 text-muted-foreground shrink-0" />
<div className="space-y-0.5">
<div className="flex items-center gap-2">
<span className="font-medium text-sm">
{planLabel}
</span>
<Badge
variant={status?.variant}
className="flex items-center gap-1 text-xs h-5"
>
{status?.icon}
{status?.label}
</Badge>
</div>
<p className="text-xs text-muted-foreground">
{s.hostname ?? ""}
{s.ipAddress ? ` · ${s.ipAddress}` : ""}
</p>
</div>
</div>
<div className="flex items-center gap-2">
{isProvisioning && (
<Button
variant="ghost"
size="sm"
onClick={() =>
syncStatus.mutate({
managedServerId: s.managedServerId,
})
}
disabled={syncStatus.isPending}
>
<Loader2
className={cn(
"size-4",
syncStatus.isPending && "animate-spin",
)}
/>
</Button>
)}
{s.status === "ready" && s.server && (
<Button variant="outline" size="sm" asChild>
<Link
href={`/dashboard/settings/server?serverId=${s.serverId}`}
>
<ExternalLink className="size-3.5 mr-1.5" />
Open
</Link>
</Button>
)}
<DialogAction
title="Terminate Server"
description="This will permanently destroy the server and all data on it. This action cannot be undone."
type="destructive"
onClick={() =>
deleteServer.mutate({
managedServerId: s.managedServerId,
})
}
>
<Button
variant="ghost"
size="sm"
className="text-destructive hover:text-destructive"
>
<Trash2 className="size-4" />
</Button>
</DialogAction>
</div>
</div>
);
})}
</div>
)}
</div>
</CardContent>
</div>
</Card>
</div>
);
};

View File

@@ -0,0 +1,48 @@
import { HelpCircle } from "lucide-react";
import { toast } from "sonner";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { api } from "@/utils/api";
export const ToggleEnforceSSO = () => {
const { data, refetch } = api.settings.getWebServerSettings.useQuery();
const { mutateAsync } = api.settings.updateEnforceSSO.useMutation();
const handleToggle = async (checked: boolean) => {
try {
await mutateAsync({ enforceSSO: checked });
await refetch();
toast.success("Enforce SSO updated");
} catch {
toast.error("Error updating Enforce SSO");
}
};
return (
<div className="flex items-center gap-4">
<Switch checked={!!data?.enforceSSO} onCheckedChange={handleToggle} />
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Label className="text-primary flex items-center gap-1.5 cursor-pointer">
Enforce SSO
<HelpCircle className="size-4 text-muted-foreground" />
</Label>
</TooltipTrigger>
<TooltipContent side="top" className="max-w-sm">
<p>
When enabled, the email/password login form is hidden and users
must sign in exclusively through SSO.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
);
};

View File

@@ -0,0 +1,53 @@
import { HelpCircle } from "lucide-react";
import { toast } from "sonner";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { api } from "@/utils/api";
export const ToggleRemoteServersOnly = () => {
const { data, refetch } = api.settings.getWebServerSettings.useQuery();
const { mutateAsync } = api.settings.updateRemoteServersOnly.useMutation();
const handleToggle = async (checked: boolean) => {
try {
await mutateAsync({ remoteServersOnly: checked });
await refetch();
toast.success("Remote Servers Only updated");
} catch {
toast.error("Error updating Remote Servers Only");
}
};
return (
<div className="flex items-center gap-4">
<Switch
checked={!!data?.remoteServersOnly}
onCheckedChange={handleToggle}
/>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<Label className="text-primary flex items-center gap-1.5 cursor-pointer">
Remote Servers Only
<HelpCircle className="size-4 text-muted-foreground" />
</Label>
</TooltipTrigger>
<TooltipContent side="top" className="max-w-sm">
<p>
When enabled, all services (applications, databases, compose) must
be deployed to a remote server. Deploying directly to the Dokploy
host VM is not allowed.
</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
);
};

View File

@@ -131,10 +131,10 @@ export const ShowServers = () => {
className="relative hover:shadow-lg transition-shadow flex flex-col bg-transparent"
>
<CardHeader className="pb-3">
<div className="flex items-start justify-between">
<div className="flex items-center gap-2">
<ServerIcon className="size-5 text-muted-foreground" />
<CardTitle className="text-lg">
<div className="flex items-start justify-between gap-2">
<div className="flex min-w-0 items-center gap-2">
<ServerIcon className="size-5 shrink-0 text-muted-foreground" />
<CardTitle className="text-lg break-words min-w-0">
{server.name}
</CardTitle>
</div>
@@ -145,7 +145,7 @@ export const ShowServers = () => {
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
className="h-8 w-8 p-0"
className="h-8 w-8 shrink-0 p-0"
>
<span className="sr-only">
More options

View File

@@ -1,4 +1,6 @@
import copy from "copy-to-clipboard";
import { CopyIcon, ServerIcon } from "lucide-react";
import { toast } from "sonner";
import {
Card,
CardContent,
@@ -7,8 +9,6 @@ import {
CardTitle,
} from "@/components/ui/card";
import { api } from "@/utils/api";
import copy from "copy-to-clipboard";
import { toast } from "sonner";
import { ShowDokployActions } from "./servers/actions/show-dokploy-actions";
import { ShowStorageActions } from "./servers/actions/show-storage-actions";
import { ShowTraefikActions } from "./servers/actions/show-traefik-actions";

View File

@@ -29,10 +29,15 @@ type SSOEmailForm = z.infer<typeof ssoEmailSchema>;
interface SignInWithSSOProps {
/** Content shown when SSO is collapsed (e.g. email/password form) */
children: React.ReactNode;
children?: React.ReactNode;
/** When true, SSO is the only option — no fallback to email/password */
enforce?: boolean;
}
export function SignInWithSSO({ children }: SignInWithSSOProps) {
export function SignInWithSSO({
children,
enforce = false,
}: SignInWithSSOProps) {
const [expanded, setExpanded] = useState(false);
const form = useForm<SSOEmailForm>({
@@ -72,7 +77,7 @@ export function SignInWithSSO({ children }: SignInWithSSOProps) {
<LogIn className="mr-2 size-4" />
Sign in with SSO
</Button>
{children}
{!enforce && children}
</div>
);
}
@@ -113,13 +118,15 @@ export function SignInWithSSO({ children }: SignInWithSSOProps) {
</FormItem>
)}
/>
<button
type="button"
onClick={() => setExpanded(false)}
className="text-xs text-muted-foreground hover:underline"
>
Use email and password instead
</button>
{!enforce && (
<button
type="button"
onClick={() => setExpanded(false)}
className="text-xs text-muted-foreground hover:underline"
>
Use email and password instead
</button>
)}
</form>
</Form>
</div>

View File

@@ -167,7 +167,13 @@ export const CodeEditor = ({
? css()
: language === "shell"
? StreamLanguage.define(shell)
: StreamLanguage.define(properties),
: StreamLanguage.define({
...properties,
// The legacy properties mode lacks comment metadata, so
// CodeMirror's toggle-comment shortcut (Mod-/) has no comment
// token to use. Declare `#` as the line comment for env editors.
languageData: { commentTokens: { line: "#" } },
}),
props.lineWrapping ? EditorView.lineWrapping : [],
language === "yaml"
? autocompletion({

View File

@@ -1,22 +0,0 @@
CREATE TYPE "public"."managedServerStatus" AS ENUM('pending', 'provisioning', 'configuring', 'ready', 'error', 'terminating', 'terminated');--> statement-breakpoint
CREATE TABLE "managed_server" (
"managedServerId" text PRIMARY KEY NOT NULL,
"organizationId" text NOT NULL,
"serverId" text,
"plan" text NOT NULL,
"status" "managedServerStatus" DEFAULT 'pending' NOT NULL,
"hostingerVmId" integer,
"hostingerSubscriptionId" text,
"dataCenterId" integer NOT NULL,
"ipAddress" text,
"hostname" text,
"stripeSubscriptionId" text,
"stripePriceId" text,
"rootPassword" text,
"errorMessage" text,
"createdAt" text NOT NULL,
"updatedAt" text NOT NULL
);
--> statement-breakpoint
ALTER TABLE "managed_server" ADD CONSTRAINT "managed_server_organizationId_organization_id_fk" FOREIGN KEY ("organizationId") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
ALTER TABLE "managed_server" ADD CONSTRAINT "managed_server_serverId_server_serverId_fk" FOREIGN KEY ("serverId") REFERENCES "public"."server"("serverId") ON DELETE set null ON UPDATE no action;

View File

@@ -0,0 +1 @@
ALTER TABLE "webServerSettings" ADD COLUMN "remoteServersOnly" boolean DEFAULT false NOT NULL;

View File

@@ -0,0 +1 @@
ALTER TABLE "webServerSettings" ADD COLUMN "enforceSSO" boolean DEFAULT false NOT NULL;

View File

@@ -0,0 +1,11 @@
ALTER TABLE "schedule" DROP CONSTRAINT "schedule_userId_user_id_fk";
--> statement-breakpoint
ALTER TABLE "schedule" ADD COLUMN "organizationId" text;--> statement-breakpoint
ALTER TABLE "schedule" ADD CONSTRAINT "schedule_organizationId_organization_id_fk" FOREIGN KEY ("organizationId") REFERENCES "public"."organization"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
UPDATE "schedule" s
SET "organizationId" = m."organization_id"
FROM "member" m
WHERE s."scheduleType" = 'dokploy-server'
AND s."userId" = m."user_id"
AND m."role" = 'owner';--> statement-breakpoint
ALTER TABLE "schedule" DROP COLUMN "userId";

View File

@@ -1,5 +1,5 @@
{
"id": "20e31523-69b6-4261-ac1d-c1dde8d6d8b7",
"id": "de1ea564-75f5-431f-adb8-7c4db357dde5",
"prevId": "887c0c81-4af9-477a-ab29-b3ad16f08451",
"version": "7",
"dialect": "postgresql",
@@ -3886,144 +3886,6 @@
"checkConstraints": {},
"isRLSEnabled": false
},
"public.managed_server": {
"name": "managed_server",
"schema": "",
"columns": {
"managedServerId": {
"name": "managedServerId",
"type": "text",
"primaryKey": true,
"notNull": true
},
"organizationId": {
"name": "organizationId",
"type": "text",
"primaryKey": false,
"notNull": true
},
"serverId": {
"name": "serverId",
"type": "text",
"primaryKey": false,
"notNull": false
},
"plan": {
"name": "plan",
"type": "text",
"primaryKey": false,
"notNull": true
},
"status": {
"name": "status",
"type": "managedServerStatus",
"typeSchema": "public",
"primaryKey": false,
"notNull": true,
"default": "'pending'"
},
"hostingerVmId": {
"name": "hostingerVmId",
"type": "integer",
"primaryKey": false,
"notNull": false
},
"hostingerSubscriptionId": {
"name": "hostingerSubscriptionId",
"type": "text",
"primaryKey": false,
"notNull": false
},
"dataCenterId": {
"name": "dataCenterId",
"type": "integer",
"primaryKey": false,
"notNull": true
},
"ipAddress": {
"name": "ipAddress",
"type": "text",
"primaryKey": false,
"notNull": false
},
"hostname": {
"name": "hostname",
"type": "text",
"primaryKey": false,
"notNull": false
},
"stripeSubscriptionId": {
"name": "stripeSubscriptionId",
"type": "text",
"primaryKey": false,
"notNull": false
},
"stripePriceId": {
"name": "stripePriceId",
"type": "text",
"primaryKey": false,
"notNull": false
},
"rootPassword": {
"name": "rootPassword",
"type": "text",
"primaryKey": false,
"notNull": false
},
"errorMessage": {
"name": "errorMessage",
"type": "text",
"primaryKey": false,
"notNull": false
},
"createdAt": {
"name": "createdAt",
"type": "text",
"primaryKey": false,
"notNull": true
},
"updatedAt": {
"name": "updatedAt",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {
"managed_server_organizationId_organization_id_fk": {
"name": "managed_server_organizationId_organization_id_fk",
"tableFrom": "managed_server",
"tableTo": "organization",
"columnsFrom": [
"organizationId"
],
"columnsTo": [
"id"
],
"onDelete": "cascade",
"onUpdate": "no action"
},
"managed_server_serverId_server_serverId_fk": {
"name": "managed_server_serverId_server_serverId_fk",
"tableFrom": "managed_server",
"tableTo": "server",
"columnsFrom": [
"serverId"
],
"columnsTo": [
"serverId"
],
"onDelete": "set null",
"onUpdate": "no action"
}
},
"compositePrimaryKeys": {},
"uniqueConstraints": {},
"policies": {},
"checkConstraints": {},
"isRLSEnabled": false
},
"public.mariadb": {
"name": "mariadb",
"schema": "",
@@ -8159,6 +8021,13 @@
"notNull": false,
"default": "'{\"appName\":null,\"appDescription\":null,\"logoUrl\":null,\"faviconUrl\":null,\"customCss\":null,\"loginLogoUrl\":null,\"supportUrl\":null,\"docsUrl\":null,\"errorPageTitle\":null,\"errorPageDescription\":null,\"metaTitle\":null,\"footerText\":null}'::jsonb"
},
"remoteServersOnly": {
"name": "remoteServersOnly",
"type": "boolean",
"primaryKey": false,
"notNull": true,
"default": false
},
"cleanupCacheApplications": {
"name": "cleanupCacheApplications",
"type": "boolean",
@@ -8299,19 +8168,6 @@
"gitea"
]
},
"public.managedServerStatus": {
"name": "managedServerStatus",
"schema": "public",
"values": [
"pending",
"provisioning",
"configuring",
"ready",
"error",
"terminating",
"terminated"
]
},
"public.mountType": {
"name": "mountType",
"schema": "public",
@@ -8466,4 +8322,4 @@
"schemas": {},
"tables": {}
}
}
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -1174,8 +1174,22 @@
{
"idx": 167,
"version": "7",
"when": 1778657133470,
"tag": "0167_dizzy_solo",
"when": 1780122576214,
"tag": "0167_fresh_goliath",
"breakpoints": true
},
{
"idx": 168,
"version": "7",
"when": 1780122833339,
"tag": "0168_long_justice",
"breakpoints": true
},
{
"idx": 169,
"version": "7",
"when": 1780127552074,
"tag": "0169_parched_johnny_storm",
"breakpoints": true
}
]

View File

@@ -1,6 +1,6 @@
{
"name": "dokploy",
"version": "v0.29.4",
"version": "v0.29.7",
"private": true,
"license": "Apache-2.0",
"type": "module",
@@ -123,7 +123,7 @@
"lucide-react": "^0.469.0",
"micromatch": "4.0.8",
"nanoid": "3.3.11",
"next": "^16.2.0",
"next": "16.2.6",
"next-themes": "^0.2.1",
"nextjs-toploader": "^3.9.17",
"node-os-utils": "2.0.1",

View File

@@ -1,6 +1,6 @@
import copy from "copy-to-clipboard";
import { validateRequest } from "@dokploy/server/lib/auth";
import { createServerSideHelpers } from "@trpc/react-query/server";
import copy from "copy-to-clipboard";
import { HelpCircle, ServerOff } from "lucide-react";
import type {
GetServerSidePropsContext,
@@ -10,8 +10,8 @@ import Head from "next/head";
import Link from "next/link";
import { useRouter } from "next/router";
import { type ReactElement, useState } from "react";
import superjson from "superjson";
import { toast } from "sonner";
import superjson from "superjson";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment";
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
import { DeleteService } from "@/components/dashboard/compose/delete-service";

View File

@@ -1,6 +1,6 @@
import copy from "copy-to-clipboard";
import { validateRequest } from "@dokploy/server/lib/auth";
import { createServerSideHelpers } from "@trpc/react-query/server";
import copy from "copy-to-clipboard";
import { HelpCircle, ServerOff } from "lucide-react";
import type {
GetServerSidePropsContext,
@@ -10,8 +10,8 @@ import Head from "next/head";
import Link from "next/link";
import { useRouter } from "next/router";
import { type ReactElement, useState } from "react";
import superjson from "superjson";
import { toast } from "sonner";
import superjson from "superjson";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment";
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
import { DeleteService } from "@/components/dashboard/compose/delete-service";

View File

@@ -1,6 +1,6 @@
import copy from "copy-to-clipboard";
import { validateRequest } from "@dokploy/server/lib/auth";
import { createServerSideHelpers } from "@trpc/react-query/server";
import copy from "copy-to-clipboard";
import { HelpCircle, ServerOff } from "lucide-react";
import type {
GetServerSidePropsContext,
@@ -10,8 +10,8 @@ import Head from "next/head";
import Link from "next/link";
import { useRouter } from "next/router";
import { type ReactElement, useState } from "react";
import superjson from "superjson";
import { toast } from "sonner";
import superjson from "superjson";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment";
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
import { DeleteService } from "@/components/dashboard/compose/delete-service";

View File

@@ -10,8 +10,8 @@ import Head from "next/head";
import Link from "next/link";
import { useRouter } from "next/router";
import { type ReactElement, useState } from "react";
import superjson from "superjson";
import { toast } from "sonner";
import superjson from "superjson";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment";
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
import { DeleteService } from "@/components/dashboard/compose/delete-service";

View File

@@ -1,6 +1,6 @@
import copy from "copy-to-clipboard";
import { validateRequest } from "@dokploy/server/lib/auth";
import { createServerSideHelpers } from "@trpc/react-query/server";
import copy from "copy-to-clipboard";
import { HelpCircle, ServerOff } from "lucide-react";
import type {
GetServerSidePropsContext,
@@ -10,8 +10,8 @@ import Head from "next/head";
import Link from "next/link";
import { useRouter } from "next/router";
import { type ReactElement, useState } from "react";
import superjson from "superjson";
import { toast } from "sonner";
import superjson from "superjson";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment";
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
import { DeleteService } from "@/components/dashboard/compose/delete-service";

View File

@@ -1,6 +1,6 @@
import copy from "copy-to-clipboard";
import { validateRequest } from "@dokploy/server/lib/auth";
import { createServerSideHelpers } from "@trpc/react-query/server";
import copy from "copy-to-clipboard";
import { HelpCircle, ServerOff } from "lucide-react";
import type {
GetServerSidePropsContext,
@@ -10,8 +10,8 @@ import Head from "next/head";
import Link from "next/link";
import { useRouter } from "next/router";
import { type ReactElement, useState } from "react";
import superjson from "superjson";
import { toast } from "sonner";
import superjson from "superjson";
import { ShowEnvironment } from "@/components/dashboard/application/environment/show-environment";
import { ShowDockerLogs } from "@/components/dashboard/application/logs/show";
import { DeleteService } from "@/components/dashboard/compose/delete-service";

View File

@@ -5,18 +5,13 @@ import type { ReactElement } from "react";
import { ShowSchedules } from "@/components/dashboard/application/schedules/show-schedules";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { Card } from "@/components/ui/card";
import { api } from "@/utils/api";
function SchedulesPage() {
const { data: user } = api.user.get.useQuery();
return (
<div className="w-full">
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-8xl mx-auto min-h-[45vh]">
<div className="rounded-xl bg-background shadow-md h-full">
<ShowSchedules
scheduleType="dokploy-server"
id={user?.user.id || ""}
/>
<ShowSchedules scheduleType="dokploy-server" id="dokploy-server" />
</div>
</Card>
</div>

View File

@@ -1,39 +0,0 @@
import { IS_CLOUD } from "@dokploy/server/constants";
import { validateRequest } from "@dokploy/server/lib/auth";
import type { GetServerSidePropsContext } from "next";
import type { ReactElement } from "react";
import { ShowManagedServers } from "@/components/dashboard/settings/billing/show-managed-servers";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
const Page = () => {
return <ShowManagedServers />;
};
export default Page;
Page.getLayout = (page: ReactElement) => {
return <DashboardLayout metaName="Managed Servers">{page}</DashboardLayout>;
};
export async function getServerSideProps(
ctx: GetServerSidePropsContext,
) {
if (!IS_CLOUD) {
return {
redirect: {
permanent: false,
destination: "/dashboard/home",
},
};
}
const { user } = await validateRequest(ctx.req);
if (!user || user.role !== "owner") {
return {
redirect: {
permanent: false,
destination: "/",
},
};
}
return { props: {} };
}

View File

@@ -1,15 +1,27 @@
import { validateRequest } from "@dokploy/server";
import { IS_CLOUD, validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next";
import type { ReactElement } from "react";
import superjson from "superjson";
import { ToggleEnforceSSO } from "@/components/dashboard/settings/servers/actions/toggle-enforce-sso";
import { ToggleRemoteServersOnly } from "@/components/dashboard/settings/servers/actions/toggle-remote-servers-only";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { EnterpriseFeatureGate } from "@/components/proprietary/enterprise-feature-gate";
import { SSOSettings } from "@/components/proprietary/sso/sso-settings";
import { Card } from "@/components/ui/card";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { appRouter } from "@/server/api/root";
const Page = () => {
interface Props {
isCloud: boolean;
}
const Page = ({ isCloud }: Props) => {
return (
<div className="w-full">
<div className="h-full rounded-xl max-w-5xl mx-auto flex flex-col gap-4">
@@ -29,6 +41,33 @@ const Page = () => {
</div>
</div>
</Card>
{!isCloud && (
<Card className="h-full bg-sidebar p-2.5 rounded-xl mx-auto w-full">
<div className="rounded-xl bg-background shadow-md">
<EnterpriseFeatureGate
lockedProps={{
title: "Self-hosted Restrictions",
description:
"Deployment and authentication restrictions are part of Dokploy Enterprise. Add a valid license to configure them.",
ctaLabel: "Go to License",
}}
>
<CardHeader>
<CardTitle className="text-xl">
Self-hosted Restrictions
</CardTitle>
<CardDescription>
Control deployment targets and authentication behavior.
</CardDescription>
</CardHeader>
<CardContent className="flex flex-col gap-4">
<ToggleRemoteServersOnly />
<ToggleEnforceSSO />
</CardContent>
</EnterpriseFeatureGate>
</div>
</Card>
)}
</div>
</div>
);
@@ -76,6 +115,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
return {
props: {
trpcState: helpers.dehydrate(),
isCloud: IS_CLOUD,
},
};
}

View File

@@ -1,4 +1,8 @@
import { IS_CLOUD, isAdminPresent } from "@dokploy/server";
import {
getWebServerSettings,
IS_CLOUD,
isAdminPresent,
} from "@dokploy/server";
import { validateRequest } from "@dokploy/server/lib/auth";
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { REGEXP_ONLY_DIGITS } from "input-otp";
@@ -52,8 +56,9 @@ type LoginForm = z.infer<typeof LoginSchema>;
interface Props {
IS_CLOUD: boolean;
enforceSSO: boolean;
}
export default function Home({ IS_CLOUD }: Props) {
export default function Home({ IS_CLOUD, enforceSSO }: Props) {
const router = useRouter();
const { config: whitelabeling } = useWhitelabelingPublic();
const { data: showSignInWithSSO } = api.sso.showSignInWithSSO.useQuery();
@@ -247,7 +252,9 @@ export default function Home({ IS_CLOUD }: Props) {
<CardContent className="p-0">
{!isTwoFactor ? (
<>
{showSignInWithSSO ? (
{enforceSSO ? (
<SignInWithSSO enforce />
) : showSignInWithSSO ? (
<SignInWithSSO>{loginContent}</SignInWithSSO>
) : (
loginContent
@@ -417,6 +424,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
return {
props: {
IS_CLOUD: IS_CLOUD,
enforceSSO: false,
},
};
}
@@ -442,9 +450,12 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
};
}
const webServerSettings = await getWebServerSettings();
return {
props: {
hasAdmin,
enforceSSO: webServerSettings?.enforceSSO ?? false,
},
};
}

View File

@@ -31,7 +31,6 @@ import { projectRouter } from "./routers/project";
import { auditLogRouter } from "./routers/proprietary/audit-log";
import { customRoleRouter } from "./routers/proprietary/custom-role";
import { licenseKeyRouter } from "./routers/proprietary/license-key";
import { managedServerRouter } from "./routers/proprietary/managed-server";
import { ssoRouter } from "./routers/proprietary/sso";
import { whitelabelingRouter } from "./routers/proprietary/whitelabeling";
import { redirectsRouter } from "./routers/redirects";
@@ -103,7 +102,6 @@ export const appRouter = createTRPCRouter({
environment: environmentRouter,
tag: tagRouter,
patch: patchRouter,
managedServer: managedServerRouter,
});
// export type definition of API

View File

@@ -9,6 +9,7 @@ import {
getAccessibleServerIds,
getApplicationStats,
getContainerLogs,
getWebServerSettings,
IS_CLOUD,
mechanizeDockerContainer,
readConfig,
@@ -87,7 +88,11 @@ export const applicationRouter = createTRPCRouter({
await checkServiceAccess(ctx, project.projectId, "create");
if (IS_CLOUD && !input.serverId) {
const webServerSettings = await getWebServerSettings();
if (
(IS_CLOUD || webServerSettings?.remoteServersOnly) &&
!input.serverId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You need to use a server to create an application",

View File

@@ -91,7 +91,11 @@ export const composeRouter = createTRPCRouter({
await checkServiceAccess(ctx, project.projectId, "create");
if (IS_CLOUD && !input.serverId) {
const webServerSettings = await getWebServerSettings();
if (
(IS_CLOUD || webServerSettings?.remoteServersOnly) &&
!input.serverId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You need to use a server to create a compose",
@@ -585,7 +589,11 @@ export const composeRouter = createTRPCRouter({
await checkServiceAccess(ctx, environment.projectId, "create");
if (IS_CLOUD && !input.serverId) {
const webServerSettings = await getWebServerSettings();
if (
(IS_CLOUD || webServerSettings?.remoteServersOnly) &&
!input.serverId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You need to use a server to create a compose",

View File

@@ -8,6 +8,7 @@ import {
findProjectById,
getAccessibleServerIds,
getContainerLogs,
getWebServerSettings,
IS_CLOUD,
rebuildDatabase,
removeLibsqlById,
@@ -51,7 +52,11 @@ export const libsqlRouter = createTRPCRouter({
await checkServiceAccess(ctx, project.projectId, "create");
if (IS_CLOUD && !input.serverId) {
const webServerSettings = await getWebServerSettings();
if (
(IS_CLOUD || webServerSettings?.remoteServersOnly) &&
!input.serverId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You need to use a server to create a Libsql",

View File

@@ -12,6 +12,7 @@ import {
getAccessibleServerIds,
getContainerLogs,
getServiceContainerCommand,
getWebServerSettings,
IS_CLOUD,
rebuildDatabase,
removeMariadbById,
@@ -62,7 +63,11 @@ export const mariadbRouter = createTRPCRouter({
await checkServiceAccess(ctx, project.projectId, "create");
if (IS_CLOUD && !input.serverId) {
const webServerSettings = await getWebServerSettings();
if (
(IS_CLOUD || webServerSettings?.remoteServersOnly) &&
!input.serverId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You need to use a server to create a Mariadb",

View File

@@ -12,6 +12,7 @@ import {
getAccessibleServerIds,
getContainerLogs,
getServiceContainerCommand,
getWebServerSettings,
IS_CLOUD,
rebuildDatabase,
removeMongoById,
@@ -61,7 +62,11 @@ export const mongoRouter = createTRPCRouter({
await checkServiceAccess(ctx, project.projectId, "create");
if (IS_CLOUD && !input.serverId) {
const webServerSettings = await getWebServerSettings();
if (
(IS_CLOUD || webServerSettings?.remoteServersOnly) &&
!input.serverId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You need to use a server to create a mongo",

View File

@@ -12,6 +12,7 @@ import {
getAccessibleServerIds,
getContainerLogs,
getServiceContainerCommand,
getWebServerSettings,
IS_CLOUD,
rebuildDatabase,
removeMySqlById,
@@ -62,7 +63,11 @@ export const mysqlRouter = createTRPCRouter({
await checkServiceAccess(ctx, project.projectId, "create");
if (IS_CLOUD && !input.serverId) {
const webServerSettings = await getWebServerSettings();
if (
(IS_CLOUD || webServerSettings?.remoteServersOnly) &&
!input.serverId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You need to use a server to create a MySQL",

View File

@@ -13,6 +13,7 @@ import {
getContainerLogs,
getMountPath,
getServiceContainerCommand,
getWebServerSettings,
IS_CLOUD,
rebuildDatabase,
removePostgresById,
@@ -63,7 +64,11 @@ export const postgresRouter = createTRPCRouter({
await checkServiceAccess(ctx, project.projectId, "create");
if (IS_CLOUD && !input.serverId) {
const webServerSettings = await getWebServerSettings();
if (
(IS_CLOUD || webServerSettings?.remoteServersOnly) &&
!input.serverId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You need to use a server to create a Postgres",

View File

@@ -1,247 +0,0 @@
import {
createServer,
IS_CLOUD,
serverSetup,
} from "@dokploy/server";
import {
apiCreateManagedServer,
apiDeleteManagedServer,
apiFindOneManagedServer,
} from "@dokploy/server/db/schema/managed-server";
import {
createManagedServer,
deleteManagedServer,
findManagedServerById,
findManagedServersByOrg,
updateManagedServer,
} from "@dokploy/server/services/managed-server";
import {
getHostingerDataCenters,
getHostingerVm,
getManagedServerPlans,
purchaseHostingerVps,
stopHostingerVm,
UBUNTU_22_TEMPLATE_ID,
} from "@dokploy/server/utils/hostinger";
import { TRPCError } from "@trpc/server";
import { nanoid } from "nanoid";
import { adminProcedure, createTRPCRouter } from "../../trpc";
export const managedServerRouter = createTRPCRouter({
getPlans: adminProcedure.query(async () => {
if (!IS_CLOUD) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Managed servers are only available in Dokploy Cloud",
});
}
return getManagedServerPlans();
}),
getDataCenters: adminProcedure.query(async () => {
if (!IS_CLOUD) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Managed servers are only available in Dokploy Cloud",
});
}
return getHostingerDataCenters();
}),
list: adminProcedure.query(async ({ ctx }) => {
if (!IS_CLOUD) return [];
return findManagedServersByOrg(ctx.session.activeOrganizationId);
}),
one: adminProcedure
.input(apiFindOneManagedServer)
.query(async ({ input, ctx }) => {
if (!IS_CLOUD) {
throw new TRPCError({ code: "BAD_REQUEST", message: "Cloud only" });
}
const record = await findManagedServerById(input.managedServerId);
if (record.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
return record;
}),
purchase: adminProcedure
.input(apiCreateManagedServer)
.mutation(async ({ input, ctx }) => {
if (!IS_CLOUD) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Managed servers are only available in Dokploy Cloud",
});
}
const plans = await getManagedServerPlans();
const plan = plans.find((p) => p.id === input.plan);
if (!plan) {
throw new TRPCError({ code: "BAD_REQUEST", message: "Invalid plan" });
}
const hostname =
`dokploy-${ctx.session.activeOrganizationId.slice(0, 8)}-${nanoid(6)}`.toLowerCase();
const managedRecord = await createManagedServer({
organizationId: ctx.session.activeOrganizationId,
plan: input.plan,
dataCenterId: input.dataCenterId,
status: "provisioning",
});
const hostingerItemId = input.isAnnual
? plan.hostingerItemIdAnnual
: plan.hostingerItemIdMonthly;
provisionManagedServer(
managedRecord.managedServerId,
hostingerItemId,
input.dataCenterId,
hostname,
ctx.session.activeOrganizationId,
).catch(async (err) => {
await updateManagedServer(managedRecord.managedServerId, {
status: "error",
errorMessage: err?.message ?? "Unknown error during provisioning",
});
});
return managedRecord;
}),
delete: adminProcedure
.input(apiDeleteManagedServer)
.mutation(async ({ input, ctx }) => {
if (!IS_CLOUD) {
throw new TRPCError({ code: "BAD_REQUEST", message: "Cloud only" });
}
const record = await findManagedServerById(input.managedServerId);
if (record.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
await updateManagedServer(input.managedServerId, {
status: "terminating",
});
if (record.hostingerVmId) {
try {
await stopHostingerVm(record.hostingerVmId);
} catch (_) {
// Best-effort
}
}
await deleteManagedServer(input.managedServerId);
return { ok: true };
}),
syncStatus: adminProcedure
.input(apiFindOneManagedServer)
.mutation(async ({ input, ctx }) => {
if (!IS_CLOUD) {
throw new TRPCError({ code: "BAD_REQUEST", message: "Cloud only" });
}
const record = await findManagedServerById(input.managedServerId);
if (record.organizationId !== ctx.session.activeOrganizationId) {
throw new TRPCError({ code: "UNAUTHORIZED" });
}
if (!record.hostingerVmId) return record;
const vm = await getHostingerVm(record.hostingerVmId);
const ipAddress = vm.ipv4?.[0]?.address ?? record.ipAddress;
await updateManagedServer(input.managedServerId, {
ipAddress: ipAddress ?? undefined,
hostname: vm.hostname ?? undefined,
status:
vm.state === "running"
? record.serverId
? "ready"
: "configuring"
: record.status,
});
return findManagedServerById(input.managedServerId);
}),
});
async function provisionManagedServer(
managedServerId: string,
hostingerItemId: string,
dataCenterId: number,
hostname: string,
organizationId: string,
) {
const result = await purchaseHostingerVps({
item_id: hostingerItemId,
payment_method_id: 0,
setup: {
template_id: UBUNTU_22_TEMPLATE_ID,
data_center_id: dataCenterId,
hostname,
enable_backups: false,
},
coupons: [],
});
const vm = result.virtual_machine;
await updateManagedServer(managedServerId, {
hostingerVmId: vm.id,
hostingerSubscriptionId: vm.subscription_id ?? undefined,
ipAddress: vm.ipv4?.[0]?.address ?? undefined,
hostname: vm.hostname ?? undefined,
status: "configuring",
});
await waitForVmRunning(vm.id!, managedServerId);
const finalVm = await getHostingerVm(vm.id!);
const finalIp = finalVm.ipv4?.[0]?.address;
if (!finalIp) {
throw new Error("VM is running but has no IPv4 address");
}
const serverRecord = await createServer(
{
name: `Managed • ${hostname}`,
description: "Managed server provisioned by Dokploy Cloud",
ipAddress: finalIp,
port: 22,
username: "root",
serverType: "deploy",
},
organizationId,
);
await updateManagedServer(managedServerId, {
serverId: serverRecord.serverId,
ipAddress: finalIp,
});
await serverSetup(serverRecord.serverId);
await updateManagedServer(managedServerId, { status: "ready" });
}
async function waitForVmRunning(
vmId: number,
_managedServerId: string,
maxAttempts = 30,
intervalMs = 10_000,
) {
for (let i = 0; i < maxAttempts; i++) {
await new Promise((r) => setTimeout(r, intervalMs));
const vm = await getHostingerVm(vmId);
if (vm.state === "running") return;
if (vm.state === "error") {
throw new Error("VM entered error state");
}
}
throw new Error("Timed out waiting for VM to become running");
}

View File

@@ -8,6 +8,7 @@ import {
requestToHeaders,
} from "@dokploy/server/index";
import { auth } from "@dokploy/server/lib/auth";
import { getWebServerSettings } from "@dokploy/server/services/web-server-settings";
import { TRPCError } from "@trpc/server";
import { and, asc, eq } from "drizzle-orm";
import { z } from "zod";
@@ -43,6 +44,13 @@ export const ssoRouter = createTRPCRouter({
owner.user.enableEnterpriseFeatures && owner.user.isValidEnterpriseLicense
);
}),
enforceSSO: publicProcedure.query(async () => {
if (IS_CLOUD) {
return false;
}
const settings = await getWebServerSettings();
return settings?.enforceSSO ?? false;
}),
listProviders: enterpriseProcedure.query(async ({ ctx }) => {
const providers = await db.query.ssoProvider.findMany({
where: and(

View File

@@ -11,6 +11,7 @@ import {
getAccessibleServerIds,
getContainerLogs,
getServiceContainerCommand,
getWebServerSettings,
IS_CLOUD,
rebuildDatabase,
removeRedisById,
@@ -59,7 +60,11 @@ export const redisRouter = createTRPCRouter({
await checkServiceAccess(ctx, project.projectId, "create");
if (IS_CLOUD && !input.serverId) {
const webServerSettings = await getWebServerSettings();
if (
(IS_CLOUD || webServerSettings?.remoteServersOnly) &&
!input.serverId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You need to use a server to create a Redis",

View File

@@ -75,7 +75,12 @@ export const scheduleRouter = createTRPCRouter({
}
}
}
const newSchedule = await createSchedule(input);
const newSchedule = await createSchedule({
...input,
...(input.scheduleType === "dokploy-server" && {
organizationId: ctx.session.activeOrganizationId,
}),
});
if (newSchedule?.enabled) {
if (IS_CLOUD) {
@@ -162,17 +167,6 @@ export const scheduleRouter = createTRPCRouter({
});
}
}
if (
existingSchedule.scheduleType === "dokploy-server" &&
existingSchedule.userId &&
existingSchedule.userId !== ctx.user.id
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You can only manage your own host-level schedules.",
});
}
}
const updatedSchedule = await updateSchedule(input);
@@ -256,17 +250,6 @@ export const scheduleRouter = createTRPCRouter({
});
}
}
if (
scheduleItem.scheduleType === "dokploy-server" &&
scheduleItem.userId &&
scheduleItem.userId !== ctx.user.id
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You can only manage your own host-level schedules.",
});
}
}
await deleteSchedule(input.scheduleId);
@@ -323,21 +306,27 @@ export const scheduleRouter = createTRPCRouter({
}
}
if (
input.scheduleType === "dokploy-server" &&
input.id !== ctx.user.id
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You can only list your own host-level schedules.",
});
if (input.scheduleType === "dokploy-server") {
const member = await findMemberByUserId(
ctx.user.id,
ctx.session.activeOrganizationId,
);
if (member.role !== "owner" && member.role !== "admin") {
throw new TRPCError({
code: "FORBIDDEN",
message: "Only owners and admins can list host-level schedules.",
});
}
}
}
const where = {
application: eq(schedules.applicationId, input.id),
compose: eq(schedules.composeId, input.id),
server: eq(schedules.serverId, input.id),
"dokploy-server": eq(schedules.userId, input.id),
"dokploy-server": eq(
schedules.organizationId,
ctx.session.activeOrganizationId,
),
};
return db.query.schedules.findMany({
where: where[input.scheduleType],
@@ -376,17 +365,6 @@ export const scheduleRouter = createTRPCRouter({
});
}
}
if (
schedule.scheduleType === "dokploy-server" &&
schedule.userId &&
schedule.userId !== ctx.user.id
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You don't have access to this schedule.",
});
}
}
return schedule;
}),
@@ -439,17 +417,6 @@ export const scheduleRouter = createTRPCRouter({
});
}
}
if (
scheduleItem.scheduleType === "dokploy-server" &&
scheduleItem.userId &&
scheduleItem.userId !== ctx.user.id
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You can only manage your own host-level schedules.",
});
}
}
try {
await runCommand(input.scheduleId);

View File

@@ -45,7 +45,7 @@ export const securityRouter = createTRPCRouter({
.mutation(async ({ input, ctx }) => {
const security = await findSecurityById(input.securityId);
await checkServicePermissionAndAccess(ctx, security.applicationId, {
service: ["delete"],
service: ["create"],
});
const result = await deleteSecurityById(input.securityId);
await audit(ctx, {

View File

@@ -77,6 +77,7 @@ import { appRouter } from "../root";
import {
adminProcedure,
createTRPCRouter,
enterpriseProcedure,
protectedProcedure,
publicProcedure,
} from "../trpc";
@@ -445,6 +446,50 @@ export const settingsRouter = createTRPCRouter({
return true;
}),
updateRemoteServersOnly: enterpriseProcedure
.input(z.object({ remoteServersOnly: z.boolean() }))
.mutation(async ({ input, ctx }) => {
if (IS_CLOUD) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "This feature is only available for self-hosted instances",
});
}
await updateWebServerSettings({
remoteServersOnly: input.remoteServersOnly,
});
await audit(ctx, {
action: "update",
resourceType: "settings",
resourceName: "remote-servers-only",
});
return true;
}),
updateEnforceSSO: enterpriseProcedure
.input(z.object({ enforceSSO: z.boolean() }))
.mutation(async ({ input, ctx }) => {
if (IS_CLOUD) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "This feature is only available for self-hosted instances",
});
}
await updateWebServerSettings({
enforceSSO: input.enforceSSO,
});
await audit(ctx, {
action: "update",
resourceType: "settings",
resourceName: "enforce-sso",
});
return true;
}),
readTraefikConfig: adminProcedure.query(() => {
if (IS_CLOUD) {
return true;

View File

@@ -61,7 +61,6 @@
"drizzle-dbml-generator": "0.10.0",
"drizzle-orm": "0.45.1",
"drizzle-zod": "0.5.1",
"hostinger-api-sdk": "^0.0.17",
"lodash": "4.17.21",
"micromatch": "4.0.8",
"nanoid": "3.3.11",

View File

@@ -15,7 +15,6 @@ export * from "./gitea";
export * from "./github";
export * from "./gitlab";
export * from "./libsql";
export * from "./managed-server";
export * from "./mariadb";
export * from "./mongo";
export * from "./mount";

View File

@@ -1,72 +0,0 @@
import { relations } from "drizzle-orm";
import { integer, pgEnum, pgTable, text } from "drizzle-orm/pg-core";
import { nanoid } from "nanoid";
import { z } from "zod";
import { organization } from "./account";
import { server } from "./server";
export const managedServerStatus = pgEnum("managedServerStatus", [
"pending",
"provisioning",
"configuring",
"ready",
"error",
"terminating",
"terminated",
]);
export const managedServer = pgTable("managed_server", {
managedServerId: text("managedServerId")
.notNull()
.primaryKey()
.$defaultFn(() => nanoid()),
organizationId: text("organizationId")
.notNull()
.references(() => organization.id, { onDelete: "cascade" }),
serverId: text("serverId").references(() => server.serverId, {
onDelete: "set null",
}),
/** Hostinger catalog item id, e.g. "hostingercom-vps-kvm2" */
plan: text("plan").notNull(),
status: managedServerStatus("status").notNull().default("pending"),
hostingerVmId: integer("hostingerVmId"),
hostingerSubscriptionId: text("hostingerSubscriptionId"),
dataCenterId: integer("dataCenterId").notNull(),
ipAddress: text("ipAddress"),
hostname: text("hostname"),
stripeSubscriptionId: text("stripeSubscriptionId"),
stripePriceId: text("stripePriceId"),
rootPassword: text("rootPassword"),
errorMessage: text("errorMessage"),
createdAt: text("createdAt")
.notNull()
.$defaultFn(() => new Date().toISOString()),
updatedAt: text("updatedAt")
.notNull()
.$defaultFn(() => new Date().toISOString()),
});
export const managedServerRelations = relations(managedServer, ({ one }) => ({
organization: one(organization, {
fields: [managedServer.organizationId],
references: [organization.id],
}),
server: one(server, {
fields: [managedServer.serverId],
references: [server.serverId],
}),
}));
export const apiCreateManagedServer = z.object({
plan: z.string().min(1),
dataCenterId: z.number().int().positive(),
isAnnual: z.boolean().default(false),
});
export const apiFindOneManagedServer = z.object({
managedServerId: z.string().min(1),
});
export const apiDeleteManagedServer = z.object({
managedServerId: z.string().min(1),
});

View File

@@ -3,11 +3,11 @@ import { boolean, pgEnum, pgTable, text } from "drizzle-orm/pg-core";
import { createInsertSchema } from "drizzle-zod";
import { nanoid } from "nanoid";
import { z } from "zod";
import { organization } from "./account";
import { applications } from "./application";
import { compose } from "./compose";
import { deployments } from "./deployment";
import { server } from "./server";
import { user } from "./user";
import { generateAppName } from "./utils";
export const shellTypes = pgEnum("shellType", ["bash", "sh"]);
@@ -46,7 +46,7 @@ export const schedules = pgTable("schedule", {
serverId: text("serverId").references(() => server.serverId, {
onDelete: "cascade",
}),
userId: text("userId").references(() => user.id, {
organizationId: text("organizationId").references(() => organization.id, {
onDelete: "cascade",
}),
enabled: boolean("enabled").notNull().default(true),
@@ -71,9 +71,9 @@ export const schedulesRelations = relations(schedules, ({ one, many }) => ({
fields: [schedules.serverId],
references: [server.serverId],
}),
user: one(user, {
fields: [schedules.userId],
references: [user.id],
organization: one(organization, {
fields: [schedules.organizationId],
references: [organization.id],
}),
deployments: many(deployments),
}));

View File

@@ -96,6 +96,10 @@ export const webServerSettings = pgTable("webServerSettings", {
metaTitle: null,
footerText: null,
}),
// Deployment Configuration (self-hosted only)
remoteServersOnly: boolean("remoteServersOnly").notNull().default(false),
// Auth Configuration (self-hosted only)
enforceSSO: boolean("enforceSSO").notNull().default(false),
// Cache Cleanup Configuration
cleanupCacheApplications: boolean("cleanupCacheApplications")
.notNull()
@@ -155,6 +159,8 @@ export const apiUpdateWebServerSettings = createSchema.partial().extend({
cleanupCacheApplications: z.boolean().optional(),
cleanupCacheOnPreviews: z.boolean().optional(),
cleanupCacheOnCompose: z.boolean().optional(),
remoteServersOnly: z.boolean().optional(),
enforceSSO: z.boolean().optional(),
});
export const apiAssignDomain = z

View File

@@ -43,7 +43,6 @@ export * from "./services/registry";
export * from "./services/rollbacks";
export * from "./services/schedule";
export * from "./services/security";
export * from "./services/managed-server";
export * from "./services/server";
export * from "./services/settings";
export * from "./services/ssh-key";
@@ -101,6 +100,7 @@ export * from "./utils/docker/types";
export * from "./utils/docker/utils";
export * from "./utils/filesystem/directory";
export * from "./utils/filesystem/ssh";
export * from "./utils/git-branch-validation";
export * from "./utils/gpu-setup";
export * from "./utils/notifications/build-error";
export * from "./utils/notifications/build-success";
@@ -109,7 +109,6 @@ export * from "./utils/notifications/docker-cleanup";
export * from "./utils/notifications/dokploy-restart";
export * from "./utils/notifications/server-threshold";
export * from "./utils/notifications/utils";
export * from "./utils/git-branch-validation";
export * from "./utils/process/execAsync";
export * from "./utils/process/spawnAsync";
export * from "./utils/providers/bitbucket";

View File

@@ -1,54 +0,0 @@
import { db } from "@dokploy/server/db";
import { managedServer } from "@dokploy/server/db/schema/managed-server";
import { TRPCError } from "@trpc/server";
import { eq } from "drizzle-orm";
export type ManagedServer = typeof managedServer.$inferSelect;
export const createManagedServer = async (
input: typeof managedServer.$inferInsert,
) => {
const record = await db
.insert(managedServer)
.values(input)
.returning()
.then((r) => r[0]);
if (!record) throw new TRPCError({ code: "INTERNAL_SERVER_ERROR" });
return record;
};
export const findManagedServerById = async (managedServerId: string) => {
const record = await db.query.managedServer.findFirst({
where: eq(managedServer.managedServerId, managedServerId),
with: { server: true },
});
if (!record)
throw new TRPCError({ code: "NOT_FOUND", message: "Managed server not found" });
return record;
};
export const findManagedServersByOrg = async (organizationId: string) => {
return db.query.managedServer.findMany({
where: eq(managedServer.organizationId, organizationId),
with: { server: true },
orderBy: (t, { desc }) => [desc(t.createdAt)],
});
};
export const updateManagedServer = async (
managedServerId: string,
data: Partial<typeof managedServer.$inferInsert>,
) => {
return db
.update(managedServer)
.set({ ...data, updatedAt: new Date().toISOString() })
.where(eq(managedServer.managedServerId, managedServerId))
.returning()
.then((r) => r[0]);
};
export const deleteManagedServer = async (managedServerId: string) => {
return db
.delete(managedServer)
.where(eq(managedServer.managedServerId, managedServerId));
};

View File

@@ -80,9 +80,10 @@ export const checkPermission = async (
const { id: userId } = ctx.user;
const { activeOrganizationId: organizationId } = ctx.session;
const memberRecord = await findMemberByUserId(userId, organizationId);
const isStaticRole = memberRecord.role in staticRoles;
if (isStaticRole) {
const isPrivilegedStaticRole =
memberRecord.role === "owner" || memberRecord.role === "admin";
if (isPrivilegedStaticRole) {
const allEnterprise = Object.keys(permissions).every((r) =>
enterpriseOnlyResources.has(r),
);
@@ -164,6 +165,8 @@ const getLegacyOverrides = (
},
sshKeys: {
read: !!memberRecord.canAccessToSSHKeys,
create: !!memberRecord.canAccessToSSHKeys,
delete: !!memberRecord.canAccessToSSHKeys,
},
gitProviders: {
read: !!memberRecord.canAccessToGitProviders,

View File

@@ -85,6 +85,9 @@ export const findScheduleOrganizationId = async (scheduleId: string) => {
if (schedule?.server) {
return schedule?.server?.organization?.id;
}
if (schedule?.organizationId) {
return schedule.organizationId;
}
return null;
};

View File

@@ -54,7 +54,7 @@ Compose Type: ${composeType} ✅`;
cd "${projectPath}";
${compose.isolatedDeployment ? `docker network inspect ${compose.appName} >/dev/null 2>&1 || docker network create ${compose.composeType === "stack" ? "--driver overlay" : ""} --attachable ${compose.appName}` : ""}
env -i PATH="$PATH" ${exportEnvCommand} docker ${command.split(" ").join(" ")} 2>&1 || { echo "Error: ❌ Docker command failed"; exit 1; }
env -i PATH="$PATH" HOME="$HOME" ${exportEnvCommand} docker ${command.split(" ").join(" ")} 2>&1 || { echo "Error: ❌ Docker command failed"; exit 1; }
${compose.isolatedDeployment ? `docker network connect ${compose.appName} $(docker ps --filter "name=dokploy-traefik" -q) >/dev/null 2>&1` : ""}
echo "Docker Compose Deployed: ✅";

View File

@@ -337,6 +337,10 @@ export const createDomainLabels = (
labels.push(
`traefik.http.routers.${routerName}.tls.certresolver=${customCertResolver}`,
);
} else if (certificateType === "none" && https) {
// No cert resolver, but HTTPS is enabled (default/custom certificate):
// explicitly enable TLS so Traefik serves the router over HTTPS.
labels.push(`traefik.http.routers.${routerName}.tls=true`);
}
}

View File

@@ -1,164 +0,0 @@
import {
BillingCatalogApi,
Configuration,
VPSDataCentersApi,
VPSVirtualMachineApi,
} from "hostinger-api-sdk";
export type {
BillingV1CatalogCatalogItemResource as HostingerCatalogItem,
VPSV1DataCenterDataCenterResource as HostingerDataCenter,
VPSV1VirtualMachinePurchaseRequest as HostingerPurchaseRequest,
VPSV1VirtualMachineVirtualMachineResource as HostingerVM,
} from "hostinger-api-sdk";
// Correct base URL — api.hostinger.com returns 530, developers.hostinger.com is the real gateway
const HOSTINGER_BASE_PATH = "https://developers.hostinger.com";
function getConfig() {
const apiKey = process.env.HOSTINGER_API_KEY;
if (!apiKey) throw new Error("HOSTINGER_API_KEY is not set");
return new Configuration({
basePath: HOSTINGER_BASE_PATH,
accessToken: apiKey,
});
}
function getVmApi() {
return new VPSVirtualMachineApi(getConfig());
}
export async function getHostingerDataCenters() {
try {
const api = new VPSDataCentersApi(getConfig());
const res = await api.getDataCenterListV1();
return res.data;
} catch (error) {
console.log(error);
}
}
export async function getHostingerVpsCatalog() {
const api = new BillingCatalogApi(getConfig());
const res = await api.getCatalogItemListV1("VPS");
return res.data;
}
export async function purchaseHostingerVps(
body: import("hostinger-api-sdk").VPSV1VirtualMachinePurchaseRequest,
) {
const api = getVmApi();
const res = await api.purchaseNewVirtualMachineV1(body);
return res.data;
}
export async function getHostingerVm(vmId: number) {
const api = getVmApi();
const res = await api.getVirtualMachineDetailsV1(vmId);
return res.data;
}
export async function stopHostingerVm(vmId: number) {
const api = getVmApi();
await api.stopVirtualMachineV1(vmId);
}
/** Ubuntu 22.04 LTS template ID on Hostinger */
export const UBUNTU_22_TEMPLATE_ID = 1009;
/**
* Markup multiplier applied to Hostinger's catalog price to get Dokploy's user price.
* Hostinger KVM2 = ~$24.49/mo → Dokploy charges $45/mo (~84% markup).
*/
const MARKUP = 1.84;
export interface ManagedServerPlan {
id: string;
name: string;
hostingerItemIdMonthly: string;
hostingerItemIdAnnual: string;
cpus: number;
memoryMb: number;
diskMb: number;
bandwidthMb: number;
/** Price in cents Hostinger charges us monthly */
hostingerPriceCentsMonthly: number;
/** Price in cents we charge the user monthly */
dokployPriceCentsMonthly: number;
/** Price in cents we charge the user annually */
dokployPriceCentsAnnual: number;
}
/** KVM plan IDs offered through Dokploy (excludes Game Panel plans) */
const OFFERED_PLAN_IDS = [
"hostingercom-vps-kvm1",
"hostingercom-vps-kvm2",
"hostingercom-vps-kvm4",
"hostingercom-vps-kvm8",
];
/**
* Fetches live VPS plans from Hostinger catalog and applies Dokploy markup.
* Only returns standard KVM plans (not Game Panel variants).
*/
export async function getManagedServerPlans(): Promise<ManagedServerPlan[]> {
const catalog = await getHostingerVpsCatalog();
const plans: ManagedServerPlan[] = [];
for (const item of catalog) {
if (!OFFERED_PLAN_IDS.includes(item.id ?? "")) continue;
const meta = item.metadata as Record<string, string> | null;
const cpus = Number(meta?.cpus ?? 0);
const memoryMb = Number(meta?.memory ?? 0);
const diskMb = Number(meta?.disk_space ?? 0);
const bandwidthMb = Number(meta?.bandwidth ?? 0);
const monthlyPrice = item.prices?.find(
(p) => p.period === 1 && p.period_unit === "month",
);
const annualPrice = item.prices?.find(
(p) => p.period === 1 && p.period_unit === "year",
);
if (!monthlyPrice) continue;
const hostingerMonthly = monthlyPrice.price ?? 0;
const hostingerAnnual = annualPrice?.price ?? hostingerMonthly * 12;
// Apply markup and round to nearest $0.50 (50 cents)
const dokployMonthly = Math.ceil((hostingerMonthly * MARKUP) / 50) * 50;
const dokployAnnual = Math.ceil((hostingerAnnual * MARKUP) / 50) * 50;
// Derive hostinger item IDs for monthly and annual billing
const hostingerItemIdMonthly = monthlyPrice.id ?? `${item.id}-usd-1m`;
const hostingerItemIdAnnual = annualPrice?.id ?? `${item.id}-usd-1y`;
// Map hostinger plan names to friendly names
const friendlyNames: Record<string, string> = {
"hostingercom-vps-kvm1": "Starter",
"hostingercom-vps-kvm2": "Basic",
"hostingercom-vps-kvm4": "Growth",
"hostingercom-vps-kvm8": "Scale",
};
plans.push({
id: item.id ?? "",
name: friendlyNames[item.id ?? ""] ?? item.name ?? item.id ?? "",
hostingerItemIdMonthly,
hostingerItemIdAnnual,
cpus,
memoryMb,
diskMb,
bandwidthMb,
hostingerPriceCentsMonthly: hostingerMonthly,
dokployPriceCentsMonthly: dokployMonthly,
dokployPriceCentsAnnual: dokployAnnual,
});
}
return plans;
}
export type ManagedServerPlanId = string;

View File

@@ -11,7 +11,7 @@ export const initSchedules = async () => {
server: true,
application: true,
compose: true,
user: true,
organization: true,
},
});

View File

@@ -2,8 +2,11 @@ import micromatch from "micromatch";
export const shouldDeploy = (
watchPaths: string[] | null,
modifiedFiles: string[],
modifiedFiles: (string | null | undefined)[] | null | undefined,
): boolean => {
if (!watchPaths || watchPaths?.length === 0) return true;
return micromatch.some(modifiedFiles, watchPaths);
const files = (modifiedFiles ?? []).filter(
(file): file is string => typeof file === "string",
);
return micromatch.some(files, watchPaths);
};

View File

@@ -40,7 +40,7 @@ export const readValidDirectory = (
directory: string,
serverId?: string | null,
) => {
if (!/^[\w/. :-]{1,500}$/.test(directory)) {
if (!/^[\w/. :[\]-]{1,500}$/.test(directory)) {
return false;
}

245
pnpm-lock.yaml generated
View File

@@ -51,7 +51,7 @@ importers:
version: 4.12.2
inngest:
specifier: 3.40.1
version: 3.40.1(encoding@0.1.13)(h3@1.15.1)(hono@4.12.2)(next@16.2.0(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(typescript@5.9.3)
version: 3.40.1(encoding@0.1.13)(h3@1.15.1)(hono@4.12.2)(next@16.2.6(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(typescript@5.9.3)
pino:
specifier: 9.4.0
version: 9.4.0
@@ -115,10 +115,10 @@ importers:
version: 2.0.30(zod@4.3.6)
'@better-auth/api-key':
specifier: 1.5.4
version: 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.4(71a760b327c31dd12606432855d01199))
version: 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.4(4c76b7da14d170b1922a58fb44839507))
'@better-auth/sso':
specifier: 1.5.4
version: 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.4(71a760b327c31dd12606432855d01199))(better-call@2.0.2(zod@4.3.6))
version: 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.4(4c76b7da14d170b1922a58fb44839507))(better-call@2.0.2(zod@4.3.6))
'@codemirror/autocomplete':
specifier: ^6.18.6
version: 6.20.0
@@ -244,7 +244,7 @@ importers:
version: 11.10.0(@trpc/server@11.10.0(typescript@5.9.3))(typescript@5.9.3)
'@trpc/next':
specifier: ^11.10.0
version: 11.10.0(@tanstack/react-query@5.90.21(react@18.2.0))(@trpc/client@11.10.0(@trpc/server@11.10.0(typescript@5.9.3))(typescript@5.9.3))(@trpc/react-query@11.10.0(@tanstack/react-query@5.90.21(react@18.2.0))(@trpc/client@11.10.0(@trpc/server@11.10.0(typescript@5.9.3))(typescript@5.9.3))(@trpc/server@11.10.0(typescript@5.9.3))(react@18.2.0)(typescript@5.9.3))(@trpc/server@11.10.0(typescript@5.9.3))(next@16.2.0(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)
version: 11.10.0(@tanstack/react-query@5.90.21(react@18.2.0))(@trpc/client@11.10.0(@trpc/server@11.10.0(typescript@5.9.3))(typescript@5.9.3))(@trpc/react-query@11.10.0(@tanstack/react-query@5.90.21(react@18.2.0))(@trpc/client@11.10.0(@trpc/server@11.10.0(typescript@5.9.3))(typescript@5.9.3))(@trpc/server@11.10.0(typescript@5.9.3))(react@18.2.0)(typescript@5.9.3))(@trpc/server@11.10.0(typescript@5.9.3))(next@16.2.6(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)
'@trpc/react-query':
specifier: ^11.10.0
version: 11.10.0(@tanstack/react-query@5.90.21(react@18.2.0))(@trpc/client@11.10.0(@trpc/server@11.10.0(typescript@5.9.3))(typescript@5.9.3))(@trpc/server@11.10.0(typescript@5.9.3))(react@18.2.0)(typescript@5.9.3)
@@ -280,7 +280,7 @@ importers:
version: 5.1.1(encoding@0.1.13)
better-auth:
specifier: 1.5.4
version: 1.5.4(71a760b327c31dd12606432855d01199)
version: 1.5.4(4c76b7da14d170b1922a58fb44839507)
bl:
specifier: 6.0.11
version: 6.0.11
@@ -342,14 +342,14 @@ importers:
specifier: 3.3.11
version: 3.3.11
next:
specifier: ^16.2.0
version: 16.2.0(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
specifier: 16.2.6
version: 16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
next-themes:
specifier: ^0.2.1
version: 0.2.1(next@16.2.0(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
version: 0.2.1(next@16.2.6(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
nextjs-toploader:
specifier: ^3.9.17
version: 3.9.17(next@16.2.0(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
version: 3.9.17(next@16.2.6(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
node-os-utils:
specifier: 2.0.1
version: 2.0.1
@@ -539,10 +539,10 @@ importers:
version: 5.9.3
vite-tsconfig-paths:
specifier: 4.3.2
version: 4.3.2(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(tsx@4.16.2)(yaml@2.8.1))
version: 4.3.2(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.13)(jiti@1.21.7)(tsx@4.16.2)(yaml@2.8.1))
vitest:
specifier: ^4.0.18
version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(jiti@2.6.1)(tsx@4.16.2)(yaml@2.8.1)
version: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(jiti@1.21.7)(tsx@4.16.2)(yaml@2.8.1)
apps/schedules:
dependencies:
@@ -630,10 +630,10 @@ importers:
version: 2.0.30(zod@4.3.6)
'@better-auth/api-key':
specifier: 1.5.4
version: 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.4(48b68ecaf84f5e14652b8d87fbbd7ca9))
version: 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.4(9312701f7238452a4ac943b2bb39c424))
'@better-auth/sso':
specifier: 1.5.4
version: 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.4(48b68ecaf84f5e14652b8d87fbbd7ca9))(better-call@2.0.2(zod@4.3.6))
version: 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.4(9312701f7238452a4ac943b2bb39c424))(better-call@2.0.2(zod@4.3.6))
'@better-auth/utils':
specifier: 0.3.1
version: 0.3.1
@@ -672,7 +672,7 @@ importers:
version: 5.1.1(encoding@0.1.13)
better-auth:
specifier: 1.5.4
version: 1.5.4(48b68ecaf84f5e14652b8d87fbbd7ca9)
version: 1.5.4(9312701f7238452a4ac943b2bb39c424)
better-call:
specifier: 2.0.2
version: 2.0.2(zod@4.3.6)
@@ -700,9 +700,6 @@ importers:
drizzle-zod:
specifier: 0.5.1
version: 0.5.1(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(zod@4.3.6)
hostinger-api-sdk:
specifier: ^0.0.17
version: 0.0.17
lodash:
specifier: 4.17.21
version: 4.17.21
@@ -778,7 +775,7 @@ importers:
devDependencies:
'@better-auth/cli':
specifier: 1.4.21
version: 1.4.21(@better-fetch/fetch@1.1.21)(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(better-call@2.0.2(zod@4.3.6))(drizzle-kit@0.31.9)(jose@6.1.3)(kysely@0.28.11)(mongodb@7.1.0(socks@2.8.8))(mysql2@3.15.3)(nanostores@1.1.1)(next@16.2.0(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(jiti@2.6.1)(tsx@4.16.2)(yaml@2.8.1))
version: 1.4.21(@better-fetch/fetch@1.1.21)(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(better-call@2.0.2(zod@4.3.6))(drizzle-kit@0.31.9)(jose@6.1.3)(kysely@0.28.11)(mongodb@7.1.0(socks@2.8.8))(mysql2@3.15.3)(nanostores@1.1.1)(next@16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(jiti@2.6.1)(tsx@4.16.2)(yaml@2.8.1))
'@types/adm-zip':
specifier: ^0.5.7
version: 0.5.7
@@ -1881,53 +1878,53 @@ packages:
cpu: [x64]
os: [win32]
'@next/env@16.2.0':
resolution: {integrity: sha512-OZIbODWWAi0epQRCRjNe1VO45LOFBzgiyqmTLzIqWq6u1wrxKnAyz1HH6tgY/Mc81YzIjRPoYsPAEr4QV4l9TA==}
'@next/env@16.2.6':
resolution: {integrity: sha512-gd8HoHN4ufj73WmR3JmVolrpJR47ILK6LouP5xElPglaVxir6e1a7VzvTvDWkOoPXT9rkkTzyCxBu4yeZfZwcw==}
'@next/swc-darwin-arm64@16.2.0':
resolution: {integrity: sha512-/JZsqKzKt01IFoiLLAzlNqys7qk2F3JkcUhj50zuRhKDQkZNOz9E5N6wAQWprXdsvjRP4lTFj+/+36NSv5AwhQ==}
'@next/swc-darwin-arm64@16.2.6':
resolution: {integrity: sha512-ZJGkkcNfYgrrMkqOdZ7zoLa1TOy0qpcMfk/z4Mh/FKUz40gVO+HNQWqmLxf67Z5WB64DRp0dhEbyHfel+6sJUg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [darwin]
'@next/swc-darwin-x64@16.2.0':
resolution: {integrity: sha512-/hV8erWq4SNlVgglUiW5UmQ5Hwy5EW/AbbXlJCn6zkfKxTy/E/U3V8U1Ocm2YCTUoFgQdoMxRyRMOW5jYy4ygg==}
'@next/swc-darwin-x64@16.2.6':
resolution: {integrity: sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ==}
engines: {node: '>= 10'}
cpu: [x64]
os: [darwin]
'@next/swc-linux-arm64-gnu@16.2.0':
resolution: {integrity: sha512-GkjL/Q7MWOwqWR9zoxu1TIHzkOI2l2BHCf7FzeQG87zPgs+6WDh+oC9Sw9ARuuL/FUk6JNCgKRkA6rEQYadUaw==}
'@next/swc-linux-arm64-gnu@16.2.6':
resolution: {integrity: sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@next/swc-linux-arm64-musl@16.2.0':
resolution: {integrity: sha512-1ffhC6KY5qWLg5miMlKJp3dZbXelEfjuXt1qcp5WzSCQy36CV3y+JT7OC1WSFKizGQCDOcQbfkH/IjZP3cdRNA==}
'@next/swc-linux-arm64-musl@16.2.6':
resolution: {integrity: sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [linux]
'@next/swc-linux-x64-gnu@16.2.0':
resolution: {integrity: sha512-FmbDcZQ8yJRq93EJSL6xaE0KK/Rslraf8fj1uViGxg7K4CKBCRYSubILJPEhjSgZurpcPQq12QNOJQ0DRJl6Hg==}
'@next/swc-linux-x64-gnu@16.2.6':
resolution: {integrity: sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@next/swc-linux-x64-musl@16.2.0':
resolution: {integrity: sha512-HzjIHVkmGAwRbh/vzvoBWWEbb8BBZPxBvVbDQDvzHSf3D8RP/4vjw7MNLDXFF9Q1WEzeQyEj2zdxBtVAHu5Oyw==}
'@next/swc-linux-x64-musl@16.2.6':
resolution: {integrity: sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g==}
engines: {node: '>= 10'}
cpu: [x64]
os: [linux]
'@next/swc-win32-arm64-msvc@16.2.0':
resolution: {integrity: sha512-UMiFNQf5H7+1ZsZPxEsA064WEuFbRNq/kEXyepbCnSErp4f5iut75dBA8UeerFIG3vDaQNOfCpevnERPp2V+nA==}
'@next/swc-win32-arm64-msvc@16.2.6':
resolution: {integrity: sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg==}
engines: {node: '>= 10'}
cpu: [arm64]
os: [win32]
'@next/swc-win32-x64-msvc@16.2.0':
resolution: {integrity: sha512-DRrNJKW+/eimrZgdhVN1uvkN1OI4j6Lpefwr44jKQ0YQzztlmOBUUzHuV5GxOMPK3nmodAYElUVCY8ZXo/IWeA==}
'@next/swc-win32-x64-msvc@16.2.6':
resolution: {integrity: sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA==}
engines: {node: '>= 10'}
cpu: [x64]
os: [win32]
@@ -4174,6 +4171,7 @@ packages:
'@ungap/structured-clone@1.3.0':
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
deprecated: Potential CWE-502 - Update to 1.3.1 or higher
'@vercel/oidc@3.1.0':
resolution: {integrity: sha512-Fw28YZpRnA3cAHHDlkt7xQHiJ0fcL+NRcIqsocZQUSmbzeIKRpwttJjik5ZGanXP+vlA4SbTg+AbA3bP363l+w==}
@@ -5769,9 +5767,6 @@ packages:
resolution: {integrity: sha512-gJnaDHXKDayjt8ue0n8Gs0A007yKXj4Xzb8+cNjZeYsSzzwKc0Lr+OZgYwVfB0pHfUs17EPoLvrOsEaJ9mj+Tg==}
engines: {node: '>=16.9.0'}
hostinger-api-sdk@0.0.17:
resolution: {integrity: sha512-PGIS2P4bwwvztlUHTdXYia7sAJsmDd9qsSE2tr8wDMAAjYow0J979w4dHcuHfC4ovo8nZoj3btqTDJtIIeSPYw==}
html-to-text@9.0.5:
resolution: {integrity: sha512-qY60FjREgVZL03vJU6IfMV4GDjGBIoOyvuFdpBDIX9yTlDw0TjxVBQp+P8NvpdIXNJvfWBTNul7fsAQJq2FNpg==}
engines: {node: '>=14'}
@@ -6571,8 +6566,8 @@ packages:
react: '*'
react-dom: '*'
next@16.2.0:
resolution: {integrity: sha512-NLBVrJy1pbV1Yn00L5sU4vFyAHt5XuSjzrNyFnxo6Com0M0KrL6hHM5B99dbqXb2bE9pm4Ow3Zl1xp6HVY9edQ==}
next@16.2.6:
resolution: {integrity: sha512-qOVgKJg1+At15NpeUP+eJgCHvTCgXsogweq87Ri/Ix7PkqQHg4sdaXmSFqKlgaIXE4kW0g25LE68W87UANlHtw==}
engines: {node: '>=20.9.0'}
hasBin: true
peerDependencies:
@@ -8670,21 +8665,21 @@ snapshots:
'@balena/dockerignore@1.0.2': {}
'@better-auth/api-key@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.4(48b68ecaf84f5e14652b8d87fbbd7ca9))':
'@better-auth/api-key@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.4(4c76b7da14d170b1922a58fb44839507))':
dependencies:
'@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1)
'@better-auth/utils': 0.3.1
better-auth: 1.5.4(48b68ecaf84f5e14652b8d87fbbd7ca9)
better-auth: 1.5.4(4c76b7da14d170b1922a58fb44839507)
zod: 4.3.6
'@better-auth/api-key@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.4(71a760b327c31dd12606432855d01199))':
'@better-auth/api-key@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.4(9312701f7238452a4ac943b2bb39c424))':
dependencies:
'@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1)
'@better-auth/utils': 0.3.1
better-auth: 1.5.4(71a760b327c31dd12606432855d01199)
better-auth: 1.5.4(9312701f7238452a4ac943b2bb39c424)
zod: 4.3.6
'@better-auth/cli@1.4.21(@better-fetch/fetch@1.1.21)(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(better-call@2.0.2(zod@4.3.6))(drizzle-kit@0.31.9)(jose@6.1.3)(kysely@0.28.11)(mongodb@7.1.0(socks@2.8.8))(mysql2@3.15.3)(nanostores@1.1.1)(next@16.2.0(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(jiti@2.6.1)(tsx@4.16.2)(yaml@2.8.1))':
'@better-auth/cli@1.4.21(@better-fetch/fetch@1.1.21)(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(better-call@2.0.2(zod@4.3.6))(drizzle-kit@0.31.9)(jose@6.1.3)(kysely@0.28.11)(mongodb@7.1.0(socks@2.8.8))(mysql2@3.15.3)(nanostores@1.1.1)(next@16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(jiti@2.6.1)(tsx@4.16.2)(yaml@2.8.1))':
dependencies:
'@babel/core': 7.29.0
'@babel/preset-react': 7.28.5(@babel/core@7.29.0)
@@ -8696,7 +8691,7 @@ snapshots:
'@mrleebo/prisma-ast': 0.13.1
'@prisma/client': 5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))
'@types/pg': 8.16.0
better-auth: 1.4.21(97e31320bc7dc8a33b04861de973b388)
better-auth: 1.4.21(23a7835d44d42e8df1218333453e2b24)
better-sqlite3: 12.6.2
c12: 3.3.3
chalk: 5.6.2
@@ -8830,24 +8825,24 @@ snapshots:
'@prisma/client': 5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))
prisma: 7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)
'@better-auth/sso@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.4(48b68ecaf84f5e14652b8d87fbbd7ca9))(better-call@2.0.2(zod@4.3.6))':
'@better-auth/sso@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.4(4c76b7da14d170b1922a58fb44839507))(better-call@2.0.2(zod@4.3.6))':
dependencies:
'@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1)
'@better-auth/utils': 0.3.1
'@better-fetch/fetch': 1.1.21
better-auth: 1.5.4(48b68ecaf84f5e14652b8d87fbbd7ca9)
better-auth: 1.5.4(4c76b7da14d170b1922a58fb44839507)
better-call: 2.0.2(zod@4.3.6)
fast-xml-parser: 5.5.1
jose: 6.1.3
samlify: 2.10.2
zod: 4.3.6
'@better-auth/sso@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.4(71a760b327c31dd12606432855d01199))(better-call@2.0.2(zod@4.3.6))':
'@better-auth/sso@1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(better-auth@1.5.4(9312701f7238452a4ac943b2bb39c424))(better-call@2.0.2(zod@4.3.6))':
dependencies:
'@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1)
'@better-auth/utils': 0.3.1
'@better-fetch/fetch': 1.1.21
better-auth: 1.5.4(71a760b327c31dd12606432855d01199)
better-auth: 1.5.4(9312701f7238452a4ac943b2bb39c424)
better-call: 2.0.2(zod@4.3.6)
fast-xml-parser: 5.5.1
jose: 6.1.3
@@ -9518,30 +9513,30 @@ snapshots:
'@msgpackr-extract/msgpackr-extract-win32-x64@3.0.3':
optional: true
'@next/env@16.2.0': {}
'@next/env@16.2.6': {}
'@next/swc-darwin-arm64@16.2.0':
'@next/swc-darwin-arm64@16.2.6':
optional: true
'@next/swc-darwin-x64@16.2.0':
'@next/swc-darwin-x64@16.2.6':
optional: true
'@next/swc-linux-arm64-gnu@16.2.0':
'@next/swc-linux-arm64-gnu@16.2.6':
optional: true
'@next/swc-linux-arm64-musl@16.2.0':
'@next/swc-linux-arm64-musl@16.2.6':
optional: true
'@next/swc-linux-x64-gnu@16.2.0':
'@next/swc-linux-x64-gnu@16.2.6':
optional: true
'@next/swc-linux-x64-musl@16.2.0':
'@next/swc-linux-x64-musl@16.2.6':
optional: true
'@next/swc-win32-arm64-msvc@16.2.0':
'@next/swc-win32-arm64-msvc@16.2.6':
optional: true
'@next/swc-win32-x64-msvc@16.2.0':
'@next/swc-win32-x64-msvc@16.2.6':
optional: true
'@noble/ciphers@2.1.1': {}
@@ -12032,11 +12027,11 @@ snapshots:
'@trpc/server': 11.10.0(typescript@5.9.3)
typescript: 5.9.3
'@trpc/next@11.10.0(@tanstack/react-query@5.90.21(react@18.2.0))(@trpc/client@11.10.0(@trpc/server@11.10.0(typescript@5.9.3))(typescript@5.9.3))(@trpc/react-query@11.10.0(@tanstack/react-query@5.90.21(react@18.2.0))(@trpc/client@11.10.0(@trpc/server@11.10.0(typescript@5.9.3))(typescript@5.9.3))(@trpc/server@11.10.0(typescript@5.9.3))(react@18.2.0)(typescript@5.9.3))(@trpc/server@11.10.0(typescript@5.9.3))(next@16.2.0(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)':
'@trpc/next@11.10.0(@tanstack/react-query@5.90.21(react@18.2.0))(@trpc/client@11.10.0(@trpc/server@11.10.0(typescript@5.9.3))(typescript@5.9.3))(@trpc/react-query@11.10.0(@tanstack/react-query@5.90.21(react@18.2.0))(@trpc/client@11.10.0(@trpc/server@11.10.0(typescript@5.9.3))(typescript@5.9.3))(@trpc/server@11.10.0(typescript@5.9.3))(react@18.2.0)(typescript@5.9.3))(@trpc/server@11.10.0(typescript@5.9.3))(next@16.2.6(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)':
dependencies:
'@trpc/client': 11.10.0(@trpc/server@11.10.0(typescript@5.9.3))(typescript@5.9.3)
'@trpc/server': 11.10.0(typescript@5.9.3)
next: 16.2.0(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
next: 16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
typescript: 5.9.3
@@ -12327,7 +12322,6 @@ snapshots:
magic-string: 0.30.21
optionalDependencies:
vite: 7.3.1(@types/node@24.10.13)(jiti@1.21.7)(tsx@4.16.2)(yaml@2.8.1)
optional: true
'@vitest/mocker@4.0.18(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(tsx@4.16.2)(yaml@2.8.1))':
dependencies:
@@ -12336,6 +12330,7 @@ snapshots:
magic-string: 0.30.21
optionalDependencies:
vite: 7.3.1(@types/node@24.10.13)(jiti@2.6.1)(tsx@4.16.2)(yaml@2.8.1)
optional: true
'@vitest/pretty-format@4.0.18':
dependencies:
@@ -12540,7 +12535,7 @@ snapshots:
before-after-hook@2.2.3: {}
better-auth@1.4.21(97e31320bc7dc8a33b04861de973b388):
better-auth@1.4.21(23a7835d44d42e8df1218333453e2b24):
dependencies:
'@better-auth/core': 1.4.21(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@1.1.8(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.0)
'@better-auth/telemetry': 1.4.21(@better-auth/core@1.4.21(@better-auth/utils@0.3.0)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))
@@ -12561,49 +12556,14 @@ snapshots:
drizzle-orm: 0.41.0(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))
mongodb: 7.1.0(socks@2.8.8)
mysql2: 3.15.3
next: 16.2.0(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
pg: 8.18.0
prisma: 7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(jiti@1.21.7)(tsx@4.16.2)(yaml@2.8.1)
better-auth@1.5.4(48b68ecaf84f5e14652b8d87fbbd7ca9):
dependencies:
'@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1)
'@better-auth/drizzle-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))
'@better-auth/kysely-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(kysely@0.28.11)
'@better-auth/memory-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)
'@better-auth/mongo-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(mongodb@7.1.0(socks@2.8.8))
'@better-auth/prisma-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))
'@better-auth/telemetry': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))
'@better-auth/utils': 0.3.1
'@better-fetch/fetch': 1.1.21
'@noble/ciphers': 2.1.1
'@noble/hashes': 2.0.1
better-call: 1.3.2(zod@4.3.6)
defu: 6.1.4
jose: 6.1.3
kysely: 0.28.11
nanostores: 1.1.1
zod: 4.3.6
optionalDependencies:
'@prisma/client': 5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))
better-sqlite3: 12.6.2
drizzle-kit: 0.31.9
drizzle-orm: 0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))
mongodb: 7.1.0(socks@2.8.8)
mysql2: 3.15.3
next: 16.2.0(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
next: 16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
pg: 8.18.0
prisma: 7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(jiti@2.6.1)(tsx@4.16.2)(yaml@2.8.1)
transitivePeerDependencies:
- '@cloudflare/workers-types'
better-auth@1.5.4(71a760b327c31dd12606432855d01199):
better-auth@1.5.4(4c76b7da14d170b1922a58fb44839507):
dependencies:
'@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1)
'@better-auth/drizzle-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))
@@ -12629,7 +12589,7 @@ snapshots:
drizzle-orm: 0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))
mongodb: 7.1.0(socks@2.8.8)
mysql2: 3.15.3
next: 16.2.0(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
next: 16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
pg: 8.18.0
prisma: 7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)
react: 18.2.0
@@ -12638,6 +12598,41 @@ snapshots:
transitivePeerDependencies:
- '@cloudflare/workers-types'
better-auth@1.5.4(9312701f7238452a4ac943b2bb39c424):
dependencies:
'@better-auth/core': 1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@1.3.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1)
'@better-auth/drizzle-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(drizzle-orm@0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))
'@better-auth/kysely-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(kysely@0.28.11)
'@better-auth/memory-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)
'@better-auth/mongo-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(mongodb@7.1.0(socks@2.8.8))
'@better-auth/prisma-adapter': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))(@better-auth/utils@0.3.1)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))
'@better-auth/telemetry': 1.5.4(@better-auth/core@1.5.4(@better-auth/utils@0.3.1)(@better-fetch/fetch@1.1.21)(better-call@2.0.2(zod@4.3.6))(jose@6.1.3)(kysely@0.28.11)(nanostores@1.1.1))
'@better-auth/utils': 0.3.1
'@better-fetch/fetch': 1.1.21
'@noble/ciphers': 2.1.1
'@noble/hashes': 2.0.1
better-call: 1.3.2(zod@4.3.6)
defu: 6.1.4
jose: 6.1.3
kysely: 0.28.11
nanostores: 1.1.1
zod: 4.3.6
optionalDependencies:
'@prisma/client': 5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))
better-sqlite3: 12.6.2
drizzle-kit: 0.31.9
drizzle-orm: 0.45.1(@electric-sql/pglite@0.3.15)(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)))(@types/pg@8.16.0)(better-sqlite3@12.6.2)(kysely@0.28.11)(mysql2@3.15.3)(pg@8.18.0)(postgres@3.4.4)(prisma@7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3))
mongodb: 7.1.0(socks@2.8.8)
mysql2: 3.15.3
next: 16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
pg: 8.18.0
prisma: 7.4.1(@types/react@18.3.5)(better-sqlite3@12.6.2)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)(typescript@5.9.3)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
vitest: 4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(jiti@2.6.1)(tsx@4.16.2)(yaml@2.8.1)
transitivePeerDependencies:
- '@cloudflare/workers-types'
better-call@1.1.8(zod@4.3.6):
dependencies:
'@better-auth/utils': 0.3.1
@@ -13790,12 +13785,6 @@ snapshots:
hono@4.12.2: {}
hostinger-api-sdk@0.0.17:
dependencies:
axios: 1.13.5
transitivePeerDependencies:
- debug
html-to-text@9.0.5:
dependencies:
'@selderee/plugin-htmlparser2': 0.11.0
@@ -13893,7 +13882,7 @@ snapshots:
inline-style-parser@0.2.7: {}
inngest@3.40.1(encoding@0.1.13)(h3@1.15.1)(hono@4.12.2)(next@16.2.0(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(typescript@5.9.3):
inngest@3.40.1(encoding@0.1.13)(h3@1.15.1)(hono@4.12.2)(next@16.2.6(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(typescript@5.9.3):
dependencies:
'@bufbuild/protobuf': 2.11.0
'@inngest/ai': 0.1.7
@@ -13920,7 +13909,7 @@ snapshots:
optionalDependencies:
h3: 1.15.1
hono: 4.12.2
next: 16.2.0(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
next: 16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
typescript: 5.9.3
transitivePeerDependencies:
- encoding
@@ -14716,15 +14705,15 @@ snapshots:
neotraverse@0.6.18: {}
next-themes@0.2.1(next@16.2.0(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
next-themes@0.2.1(next@16.2.6(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
next: 16.2.0(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
next: 16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
react: 18.2.0
react-dom: 18.2.0(react@18.2.0)
next@16.2.0(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
next@16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
'@next/env': 16.2.0
'@next/env': 16.2.6
'@swc/helpers': 0.5.15
baseline-browser-mapping: 2.10.0
caniuse-lite: 1.0.30001774
@@ -14733,23 +14722,23 @@ snapshots:
react-dom: 18.2.0(react@18.2.0)
styled-jsx: 5.1.6(@babel/core@7.29.0)(react@18.2.0)
optionalDependencies:
'@next/swc-darwin-arm64': 16.2.0
'@next/swc-darwin-x64': 16.2.0
'@next/swc-linux-arm64-gnu': 16.2.0
'@next/swc-linux-arm64-musl': 16.2.0
'@next/swc-linux-x64-gnu': 16.2.0
'@next/swc-linux-x64-musl': 16.2.0
'@next/swc-win32-arm64-msvc': 16.2.0
'@next/swc-win32-x64-msvc': 16.2.0
'@next/swc-darwin-arm64': 16.2.6
'@next/swc-darwin-x64': 16.2.6
'@next/swc-linux-arm64-gnu': 16.2.6
'@next/swc-linux-arm64-musl': 16.2.6
'@next/swc-linux-x64-gnu': 16.2.6
'@next/swc-linux-x64-musl': 16.2.6
'@next/swc-win32-arm64-msvc': 16.2.6
'@next/swc-win32-x64-msvc': 16.2.6
'@opentelemetry/api': 1.9.0
sharp: 0.34.5
transitivePeerDependencies:
- '@babel/core'
- babel-plugin-macros
nextjs-toploader@3.9.17(next@16.2.0(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
nextjs-toploader@3.9.17(next@16.2.6(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0))(react-dom@18.2.0(react@18.2.0))(react@18.2.0):
dependencies:
next: 16.2.0(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
next: 16.2.6(@babel/core@7.29.0)(@opentelemetry/api@1.9.0)(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
nprogress: 0.2.0
prop-types: 15.8.1
react: 18.2.0
@@ -16396,13 +16385,13 @@ snapshots:
d3-time: 3.1.0
d3-timer: 3.0.1
vite-tsconfig-paths@4.3.2(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(tsx@4.16.2)(yaml@2.8.1)):
vite-tsconfig-paths@4.3.2(typescript@5.9.3)(vite@7.3.1(@types/node@24.10.13)(jiti@1.21.7)(tsx@4.16.2)(yaml@2.8.1)):
dependencies:
debug: 4.4.3
globrex: 0.1.2
tsconfck: 3.1.6(typescript@5.9.3)
optionalDependencies:
vite: 7.3.1(@types/node@24.10.13)(jiti@2.6.1)(tsx@4.16.2)(yaml@2.8.1)
vite: 7.3.1(@types/node@24.10.13)(jiti@1.21.7)(tsx@4.16.2)(yaml@2.8.1)
transitivePeerDependencies:
- supports-color
- typescript
@@ -16421,7 +16410,6 @@ snapshots:
jiti: 1.21.7
tsx: 4.16.2
yaml: 2.8.1
optional: true
vite@7.3.1(@types/node@24.10.13)(jiti@2.6.1)(tsx@4.16.2)(yaml@2.8.1):
dependencies:
@@ -16437,6 +16425,7 @@ snapshots:
jiti: 2.6.1
tsx: 4.16.2
yaml: 2.8.1
optional: true
vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(jiti@1.21.7)(tsx@4.16.2)(yaml@2.8.1):
dependencies:
@@ -16475,7 +16464,6 @@ snapshots:
- terser
- tsx
- yaml
optional: true
vitest@4.0.18(@opentelemetry/api@1.9.0)(@types/node@24.10.13)(jiti@2.6.1)(tsx@4.16.2)(yaml@2.8.1):
dependencies:
@@ -16514,6 +16502,7 @@ snapshots:
- terser
- tsx
- yaml
optional: true
w3c-keyname@2.2.8: {}