- Sanitize customCss in _document.tsx to prevent XSS breakouts by stripping </style> tags.
- Remove SSR favicon fetching to prevent SSRF and internal network scanning, falling back to client-side fetching.
OVH matches access rules per exact path: a `GET /domain/zone/*` rule grants the
subtree but not the bare `GET /domain/zone` that listZones and testConnection
call. Verified against a live account with a consumer key carrying that single
wildcard rule:
GET /domain/zone -> 403 This call has not been granted
GET /domain/zone/ -> 200
GET /domain/zone/{zone}/record -> 200
The form only asked for rights on `/domain/zone/*`, so a token created by
following it could not list zones at all, and the failure surfaced as a bare
"This call has not been granted" that points nowhere.
The hint now lists the five rights verbatim, and a token missing the root one
gets an error that names it instead of echoing OVH's message.
Reported by @narcisonunez on #5258.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
listZones used /1/product, which lego uses but the API docs don't: it is the
legacy singular route, it returns no pagination metadata, and it 401s for at
least some tokens. The documented /1/products returns `total`, `pages`,
`items_per_page` and `page`, and defaults to 15 domains per page, so an account
with more domains than that would silently lose zones.
Switch to the plural endpoint and walk every page. Verified against a live
account: forcing per_page=2 collects all 5 domains across 3 pages with no
duplicates.
Also inline the single-use createdId helper.
Both reported by @narcisonunez on #5257.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Changing a record's type deletes the record then recreates it with the new type,
because OVH's update payload carries no fieldType. If the creation failed the
name was left with nothing and no rollback.
The delete still has to come first, since OVH rejects a CNAME that would sit
alongside other data on the same name. So on a failed creation the original
record is put back from the copy already fetched before the delete, and the
original error is rethrown. If the restore fails too, the error names the record
that has to be recreated by hand.
Reported by Greptile on #5258.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
toSource always writes the apex as ".", but listRecords already accepted "" and
"@" as apex spellings on read. The upsert lookup compared sources strictly, so a
record stored under one of the other spellings would not have matched and the
upsert would have created a duplicate apex record instead of updating it.
Normalize the candidate's source before comparing, so read and match agree.
Reported by Greptile on #5257.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Verified against a live Infomaniak account: the API returns `source: "."` for
apex records, not "" as assumed.
Reading them back produced a doubled dot ("..example.com"), and writing "" meant
an apex upsert never matched the existing record, so it would have created a
duplicate apex record instead of updating it.
toSource now emits "." for the apex and toFqdn accepts ".", "" and "@" so a
hand-written record still round-trips.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds OVHcloud alongside Cloudflare, AWS Route53 and Porkbun, following the
existing DnsClient interface in packages/server/src/utils/dns/.
- ovh.ts implements listZones, listRecords, upsertRecord, updateRecord,
deleteRecord and testConnection against the /domain/zone endpoints of the
OVHcloud API, on any of its seven regional endpoints (ovh-eu/ca/us, kimsufi
and soyoustart).
- A new `ovh` value was added to the DnsProviderType enum along with an
ovhDnsConfigSchema (endpoint, applicationKey, applicationSecret, consumerKey)
in the discriminated union, plus the Drizzle migration for the enum change.
- The application secret and the consumer key are masked/merged like the other
providers' secrets in services/dns-provider.ts.
- UI: OVHcloud icon, an endpoint selector and the three credential fields in the
DNS provider dialog, plus registration in the provider selector.
Three OVH-specific behaviours are handled explicitly:
- Requests are signed with `$1$` + sha1(applicationSecret+consumerKey+method+
url+body+timestamp). The timestamp comes from the API's own clock via an
unauthenticated GET /auth/time, since a host clock a few seconds off would get
every call rejected; the measured drift is cached per endpoint for an hour.
- OVH only applies zone changes once the zone is explicitly refreshed, so every
successful create, update and delete is followed by POST /domain/zone/{zone}
/refresh.
- The record update payload carries no fieldType, so changing a record's type
replaces the record (DELETE then POST) and returns the new id.
The record listing endpoint returns ids only, so each record is fetched
individually with the fan-out capped at 8 concurrent requests.
Also fills in the missing Porkbun label in show-dns-providers.tsx, which fell
back to displaying the raw enum value.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Adds Infomaniak alongside Cloudflare, AWS Route53 and Porkbun, following the
existing DnsClient interface in packages/server/src/utils/dns/.
- infomaniak.ts implements listZones, listRecords, upsertRecord, updateRecord,
deleteRecord and testConnection against the Infomaniak API. Zones come from
/1/product?service_name=domain and records from the v2 /2/zones/{zone}/records
endpoints, which are keyed by zone name rather than by product id.
- A new `infomaniak` value was added to the DnsProviderType enum along with an
infomaniakDnsConfigSchema (apiToken) in the discriminated union, plus the
Drizzle migration for the enum change.
- The token is masked/merged like the other providers in services/dns-provider.ts.
- UI: Infomaniak icon and API Token field in the DNS provider dialog, plus
registration in the provider selector.
Infomaniak's `source` is relative to the zone (empty for the apex), so record
names are translated between Dokploy's fully-qualified format and Infomaniak's
subdomain-only format internally, with the trailing dot handled. TXT targets are
stored quoted by the API and are unquoted on read / quoted on write so that
editing a record does not stack quotes on every save.
Also fills in the missing Porkbun label in show-dns-providers.tsx, which fell
back to displaying the raw enum value.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
gitlab.one, github.one, gitea.one and bitbucket.one returned the full
DB record (OAuth access/refresh tokens, client secrets, private keys,
webhook secrets, app passwords) to any org member who merely had
access to *use* a shared provider (sharedWithOrganization: true),
not just its owner or an org owner/admin.
Add canViewGitProviderSecrets() and null out the secret fields in
each .one response when the caller isn't the provider owner or an
org owner/admin.
Show the services attached to a remote server directly in its delete
confirmation modal, with a link to each service and a per-service
delete action, instead of only showing a generic 'has active
services' blocker.
writeTraefikConfigRemote moved from an execAsyncRemote echo command to
writeFileRemote (SFTP) a while back; the test still mocked the old
execAsyncRemote path and asserted on an echo command that no longer runs.
Adds a modal to select and import multiple secrets from an assigned
vault provider at once, instead of typing each ${{vault.x.y}} reference
by hand. Existing keys are skipped by default and can be overridden per
row; also adds an "Access all" shortcut to the vault provider assignment
picker.
removeJob() for type 'schedule' computed the BullMQ repeatable key
using only the cron pattern, without the timezone. scheduleJob()
always registers the repeatable job with tz set (job.timezone || UTC),
so BullMQ's repeat key (name:jobId:endDate:tz:pattern) never matched
on removal — removeRepeatable silently no-op'd and the job kept
firing forever, surviving disable, delete, and reboots.
Forward timezone through removeJob (queue.ts), the /update-backup
lookup (index.ts), and the disable/delete call sites in the
schedule router.
Fixes#5233. Updating an enabled schedule (cron/timezone change) called
schedule() (create-backup), which only adds a new repeatable job and
never removes the one registered under the old cron pattern, leaving
an orphaned BullMQ job running in Redis indefinitely. Switched to
updateJob() (update-backup), which looks up the currently registered
repeatable pattern, removes it, then re-adds — same pattern already
used in backup.ts for backups.
--project-directory was added unconditionally in 87b914996 to fix
relative bind mount resolution for git-based compose deploys with a
nested composePath (#5181). It also moves where build.context and the
generated .env resolve, breaking any compose file in a subdirectory
that has context: . alongside its Dockerfile (#5230), or interpolates
env vars (#5242).
Only pin --project-directory when the compose actually has mounts
configured; otherwise build.context and .env resolve against the
compose file's own directory like plain docker compose. Also pass
--env-file explicitly pointing at the generated .env next to the
compose file, so it's found even when --project-directory is pinned
(credit: tonnenpinguin, PR #5235).
Fixes#5230Fixes#5242
The loading branch added in the previous commit split the fallback cell
two ways: spinner while in flight, "No results." otherwise. That second
branch also catches the failure case -- when readStatsLogs errors,
statsLogs stays undefined and isLoading goes false, so a failed request
renders as a successful empty response.
The query's isError/error were not destructured. The cell now branches
three ways and reports the error through AlertBlock, matching how
ShowTraefikSystem surfaces a failed readDirectories query.
Trims and lowercases the CLI email arg to match the normalization
used elsewhere for user emails, so a differently-cased or
whitespace-padded email no longer falsely reports "User not found".
Previously reset-password only reset the owner account's password.
Passing an email as an argument now resets that specific user's
password instead, while omitting it keeps the existing owner-reset
behavior.
Also scopes the update to the credential (password-based) account
row via providerId, and fixes the success check to verify a row was
actually updated instead of always reporting success.
Summing across all active subscriptions could mix amounts with
different billing intervals (isAnnualCurrent only reflects the
matched plan subscription), producing an inconsistent total.
getCurrentPlanForUser and getProducts only inspected subscriptions.data[0],
so a customer with more than one active Stripe subscription (e.g. Startup
plus a separately purchased additional server) could have their plan
resolved from the wrong subscription, resulting in currentPlan !== "startup"
and the HubSpot chat bubble not rendering.