Compare commits

...

57 Commits

Author SHA1 Message Date
Mauricio Siu
62aeed5aed fix(esbuild): update path for migrate-auth-secret script
- Changed the path of the `migrate-auth-secret` script from the root directory to the `scripts` folder for better organization and clarity in the project structure.
2026-05-11 11:34:21 -06:00
Mauricio Siu
5e021797f3 feat(validation): standardize branch name validation across provider schemas
- Added a regex validation for branch names in Bitbucket, Git, Gitea, GitHub, and GitLab provider schemas to ensure consistent and valid branch formats.
- Refactored the branch validation logic to improve readability and maintainability across the schemas.
- Enhanced input integrity by ensuring all provider schemas adhere to the same branch name validation rules.
2026-05-11 11:22:05 -06:00
Mauricio Siu
1c6fdc1b43 Merge pull request #4374 from Dokploy/fix/better-auth-secret-hardcoded
fix(security): replace hardcoded BETTER_AUTH_SECRET with Docker secret support
2026-05-09 02:10:41 -06:00
autofix-ci[bot]
6270bad9af [autofix.ci] apply automated fixes 2026-05-09 08:08:34 +00:00
Mauricio Siu
9c71458eff feat(auth): implement migration script for auth secret and refactor secret handling
- Added a new script `migrate-auth-secret.ts` to facilitate the migration of 2FA secrets when changing the BETTER_AUTH_SECRET.
- Updated `package.json` to include a command for running the migration script.
- Refactored the handling of BETTER_AUTH_SECRET to improve security by removing the hardcoded default and introducing a fallback mechanism using environment variables or Docker secrets.
- Updated the authentication logic to utilize the new `betterAuthSecret` function for retrieving the secret.
2026-05-09 02:08:04 -06:00
Mauricio Siu
547ba2d04b feat(validation): enhance registry URL validation in schema
- Introduced a new validation schema for registry URLs, ensuring they conform to a specific format (hostname[:port]) and disallow shell metacharacters.
- Updated the `createSchema`, `apiCreateRegistry`, and `apiTestRegistry` functions to utilize the new registry URL validation.
- Improved security and input integrity for registry URL fields.
- Updated the `removeRegistry` function to escape the registry URL during logout to prevent command injection vulnerabilities.
2026-05-09 01:09:50 -06:00
Mauricio Siu
b9e97eb321 feat(validation): enhance destination path validation in file upload schema
- Updated the `destinationPath` field in the upload file schema to include a regex validation, ensuring only alphanumeric characters, dots, dashes, underscores, and forward slashes are allowed.
- Added a corresponding regex check in the `uploadFileToContainer` function to validate the destination path before processing, improving input integrity and preventing errors.
2026-05-09 00:57:12 -06:00
Mauricio Siu
a4e2317f3e feat(deployment): enhance log retrieval by encoding log path in base64
- Updated the WebSocket server to encode the log path in base64 before executing the tail command on the remote server.
- Added validation to ensure the directory name adheres to a specified regex pattern, improving input integrity for directory paths.
2026-05-09 00:01:45 -06:00
Mauricio Siu
06a349152f fix(traefik): update remote config writing to use base64 encoding
- Modified the `writeConfigRemote` function to encode the Traefik configuration in base64 before saving it to the remote YAML file.
- This change ensures that the configuration is correctly handled and prevents potential issues with special characters in the config.
2026-05-08 23:54:40 -06:00
Mauricio Siu
fef2de1ec5 feat(validation): add branch name validation across provider schemas
- Introduced a regex validation for branch names in Bitbucket, Git, Gitea, GitHub, and GitLab provider schemas to ensure valid branch formats.
- Updated the corresponding schemas to include the new validation rule, enhancing input integrity and preventing potential errors.
- Added a utility for branch validation in the server utils.
2026-05-08 23:50:38 -06:00
Mauricio Siu
b20ff64cbf chore(package): bump version to v0.29.3 2026-05-08 23:27:47 -06:00
Mauricio Siu
5177580d51 Merge pull request #4371 from Dokploy/feat/schedule-description
feat(schedules): add optional description field
2026-05-08 23:19:12 -06:00
Mauricio Siu
d3292a2810 feat(schedules): add optional description field to schedule form and display
- Updated the schedule form schema to include an optional 'description' field.
- Enhanced the form to allow users to input a description for each schedule.
- Modified the schedule display component to show the description if available.
- Added a database migration to include the 'description' column in the schedule table.
2026-05-08 23:15:04 -06:00
Mauricio Siu
0f526af2c8 Merge pull request #4370 from Dokploy/fix/template-isolated-deployment
feat(templates): support isolated = false opt-out in template.toml
2026-05-08 19:34:07 -06:00
autofix-ci[bot]
72f5d711c8 [autofix.ci] apply automated fixes 2026-05-09 01:32:34 +00:00
Mauricio Siu
ffd51cf32f feat(templates): add isolated deployment configuration to CompleteTemplate
Introduced an optional 'isolated' boolean property in the CompleteTemplate interface to manage isolated deployment settings. Added tests to verify default behavior (isolated=true) and explicit settings (isolated=true/false) in the deployment configuration.

This change enhances template flexibility for deployment configurations.
2026-05-08 19:32:05 -06:00
Mauricio Siu
e8b3d7ba7d test(templates): add unit tests for isolated deployment config field 2026-05-08 19:26:34 -06:00
Mauricio Siu
c182755591 feat(templates): support isolated = false opt-out in template.toml
Templates using network_mode: host (e.g. cloudflared) can now declare
isolated = false in their [config] section to prevent Dokploy from
injecting networks into the compose, which would cause a Docker error.

Default behavior (isolated = true) is unchanged for all existing templates.

Fixes #4366
2026-05-08 19:22:00 -06:00
Mauricio Siu
8227a48ef4 Merge pull request #4368 from Dokploy/fix/replace-traefik-me-with-sslip-io
fix: replace traefik.me with sslip.io for auto-generated domains
2026-05-08 19:05:33 -06:00
Mauricio Siu
f5ddc36f24 fix: replace traefik.me with sslip.io for auto-generated domains
Fixes #4365 — traefik.me had availability issues. sslip.io uses the same
IP-in-subdomain format, supports both IPv4 and IPv6, and is more reliable.
2026-05-08 19:04:24 -06:00
Mauricio Siu
d5d8914bf6 Merge pull request #4358 from nhridoy/fix/layout
fix: UI Responsiveness for both mobile, Tab and desktop Screens
2026-05-08 18:49:32 -06:00
autofix-ci[bot]
bf0890a6b0 [autofix.ci] apply automated fixes 2026-05-09 00:48:16 +00:00
Mauricio Siu
4e07669464 Merge branch 'canary' into fix/layout 2026-05-08 18:47:39 -06:00
Mauricio Siu
4a3fa6e63f fix: reorder imports and clean up unused ones across various components 2026-05-08 18:45:44 -06:00
autofix-ci[bot]
14af5d293a [autofix.ci] apply automated fixes 2026-05-07 20:41:26 +00:00
Mauricio Siu
746bb3ddc6 Merge pull request #4338 from BradPerbs/fix/remove-debug-console-logs
fix: remove leftover debug console.log statements
2026-05-07 14:35:28 -06:00
Mauricio Siu
b13308dc69 Merge pull request #4294 from berkay-digital/feat/copy-ai-log-analysis
feat: add copy button to AI log analysis result
2026-05-07 13:46:07 -06:00
Mauricio Siu
16746a1609 Merge pull request #4345 from amit-y11/fix/project-service-card-alignment
fix: align card footers to bottom on project and service cards
2026-05-07 13:35:26 -06:00
Nahidujjaman Hridoy
bca62d43d2 fix: ui responsiveness for mobile, tab and desktop screens
Signed-off-by: Nahidujjaman Hridoy <hridoyboss12@gmail.com>
2026-05-07 17:49:49 +06:00
Nahidujjaman Hridoy
d502f4a206 fix: ui responsiveness for mobile, tab and desktop screens
Signed-off-by: Nahidujjaman Hridoy <hridoyboss12@gmail.com>
2026-05-07 17:49:16 +06:00
Nahidujjaman Hridoy
de7d6f8147 fix: responsiveness in components/dashboard/settings/web-domain.tsx
Signed-off-by: Nahidujjaman Hridoy <hridoyboss12@gmail.com>
2026-05-07 13:38:10 +06:00
Nahidujjaman Hridoy
9d6bc4cd18 fix: broken layout in project/[projectId]/environment/[environmentId].tsx
Signed-off-by: Nahidujjaman Hridoy <hridoyboss12@gmail.com>
2026-05-07 12:26:27 +06:00
Nahidujjaman Hridoy
65b27af0f5 fix: broken layout in project/[projectId]/environment/[environmentId].tsx
Signed-off-by: Nahidujjaman Hridoy <hridoyboss12@gmail.com>
2026-05-07 12:26:17 +06:00
Nahidujjaman Hridoy
6165114bc3 fix: broken layout in project/[projectId]/environment/[environmentId].tsx
Signed-off-by: Nahidujjaman Hridoy <hridoyboss12@gmail.com>
2026-05-07 12:24:13 +06:00
Nahidujjaman Hridoy
d3109359fb fix: broken layout in project/[projectId]/environment/[environmentId].tsx
Signed-off-by: Nahidujjaman Hridoy <hridoyboss12@gmail.com>
2026-05-06 22:57:17 +06:00
Nahidujjaman Hridoy
58f527d029 fix: broken layout in project/[projectId]/environment/[environmentId].tsx
Signed-off-by: Nahidujjaman Hridoy <hridoyboss12@gmail.com>
2026-05-06 22:23:44 +06:00
Amit
1ed41fe2f8 fix: align card footers to bottom on project and service cards
Cards with descriptions appeared taller than those without, causing
the 'Created at' and service count text to be misaligned across cards
in the same row.

- Add flex-col and mt-auto to project cards so footer sticks to bottom
- Add h-full to service card Link and Card so they fill the grid cell
2026-05-02 22:40:38 +05:30
Mauricio Siu
9b416b3699 Merge pull request #4341 from Dokploy/fix/sidebar-mobile-close-on-navigation
fix(sidebar): close mobile sidebar on navigation
2026-05-01 13:15:45 -06:00
Mauricio Siu
096b8b33fc fix(sidebar): close mobile sidebar on navigation
Closes #4340
2026-05-01 13:15:20 -06:00
BPx33
741792883a fix: remove leftover debug console.log statements
Three stray debug console.log calls had been left in production code paths:

- restore-backup.tsx: logged the full form payload on every backup restore
  submission.
- add-database.tsx: logged the libsql `enableNamespaces` field value from
  inside a `render` callback, firing on every re-render of the parent form.
- project.ts (duplicateProject mutation): logged the freshly-created
  target project/environment row on the server on every duplicate.

No behaviour change; strictly removes log noise.
2026-05-01 16:24:28 +08:00
Mauricio Siu
e0c6ed699d Merge pull request #4336 from Dokploy/fix/template-fetch-timeout-error-handling
fix(templates): add fetch timeout and handle network errors gracefully
2026-04-30 18:52:45 -06:00
Mauricio Siu
5f5ed0f2c2 fix(templates): add fetch timeout and handle network errors gracefully
Add 10s AbortSignal timeout to all template fetch calls so they fail
cleanly instead of hanging indefinitely when templates.dokploy.com is
unreachable. Add try/catch to getTags endpoint which was missing error
handling, causing a 500 instead of returning an empty list.

Closes #4282
2026-04-30 18:52:16 -06:00
Mauricio Siu
b9ff576682 Merge pull request #4335 from Dokploy/fix/auth-redirect-permanent-cache
fix: use temporary redirects for auth checks in getServerSideProps
2026-04-30 18:47:17 -06:00
Mauricio Siu
c854a38adb fix: use temporary redirects for auth checks in getServerSideProps
Replace permanent (301) redirects with temporary (302) redirects across
all pages that check authentication state in getServerSideProps.

Permanent redirects are cached by the browser indefinitely, causing a
bug where users had to manually refresh after login: the browser would
cache the unauthenticated redirect (dashboard → login page) and replay
it even after a successful login, preventing navigation to the dashboard.

Fixes #4220
2026-04-30 18:46:12 -06:00
Mauricio Siu
5fb365c08b chore(package.json): add peerDependencyRules to ignore missing dependencies 2026-04-30 18:31:07 -06:00
Mauricio Siu
15296d5c85 fix(compose-file-editor): simplify form reset logic in ComposeFileEditor component 2026-04-30 09:50:27 -06:00
Mauricio Siu
0e5fc584b2 Merge pull request #4278 from mixelburg/fix/webhook-401-missing-signature
fix(webhook): return 401 when signature header is missing
2026-04-28 18:30:48 -06:00
Mauricio Siu
cc7ea5108b Merge pull request #4325 from Dokploy/fix/3909-healthcheck-interval
fix: reduce healthcheck frequency to lower memory pressure
2026-04-28 18:27:20 -06:00
Mauricio Siu
8f3d824ea6 fix: reduce healthcheck frequency to lower memory pressure
Closes #3909

The previous 10s interval triggered a DB query (SELECT 1) 360 times/hour,
keeping Node.js heap under constant pressure and preventing the GC from
reclaiming memory efficiently. Increasing to 30s reduces query load by 3x.
Also adds start-period=60s to avoid false failures during startup.
2026-04-28 18:26:52 -06:00
Mauricio Siu
0bdcbf5827 Merge pull request #4323 from Dokploy/fix/forgot-password-email-max-length
fix: enforce 255-char max length on forgot password email field
2026-04-28 18:09:35 -06:00
autofix-ci[bot]
34564aec84 [autofix.ci] apply automated fixes 2026-04-29 00:09:17 +00:00
Mauricio Siu
ed006dc5f9 fix: enforce email length validation in reset password form
- Added a maximum length constraint of 255 characters for the email input field.
- Updated validation schema to include a message for exceeding the maximum length.
2026-04-28 18:08:37 -06:00
berkay-digital
ad490dca3f feat: add copy button to AI log analysis result
Allows users to quickly copy the AI-generated log analysis to their
clipboard from the analyze-logs popover, matching the copy UX used in
the deployment and docker logs views.

Made-with: Cursor
2026-04-23 15:19:13 +02:00
autofix-ci[bot]
ce703ef478 [autofix.ci] apply automated fixes 2026-04-22 00:05:08 +00:00
OpenClaw Bot
fc6df3ae05 fix(webhook): cast signature to string to fix TS2345 2026-04-22 00:04:44 +00:00
autofix-ci[bot]
8fb517152a [autofix.ci] apply automated fixes 2026-04-21 22:04:36 +00:00
Maks Pikov
ba3591b3ac fix(webhook): return 401 when signature header is missing 2026-04-21 22:03:55 +00:00
126 changed files with 9122 additions and 383 deletions

View File

@@ -4,5 +4,8 @@
"editor.codeActionsOnSave": {
"source.fixAll.biome": "explicit",
"source.organizeImports.biome": "explicit"
},
"[typescript]": {
"editor.defaultFormatter": "biomejs.biome"
}
}

View File

@@ -66,7 +66,7 @@ COPY --from=buildpacksio/pack:0.39.1 /usr/local/bin/pack /usr/local/bin/pack
EXPOSE 3000
HEALTHCHECK --interval=10s --timeout=3s --retries=10 \
HEALTHCHECK --interval=30s --timeout=5s --start-period=60s --retries=5 \
CMD curl -fs http://localhost:3000/api/trpc/settings.health || exit 1
CMD ["sh", "-c", "pnpm run wait-for-postgres && exec pnpm start"]

View File

@@ -494,4 +494,49 @@ describe("processTemplate", () => {
expect(result.mounts).toHaveLength(1);
});
});
describe("isolated deployment config", () => {
it("should default to isolated=true when not specified", () => {
const template: CompleteTemplate = {
metadata: {} as any,
variables: {},
config: {
domains: [],
env: {},
},
};
expect(template.config.isolated).toBeUndefined();
// undefined !== false => isolatedDeployment = true
expect(template.config.isolated !== false).toBe(true);
});
it("should be isolated when isolated=true is explicitly set", () => {
const template: CompleteTemplate = {
metadata: {} as any,
variables: {},
config: {
isolated: true,
domains: [],
env: {},
},
};
expect(template.config.isolated !== false).toBe(true);
});
it("should disable isolated deployment when isolated=false", () => {
const template: CompleteTemplate = {
metadata: {} as any,
variables: {},
config: {
isolated: false,
domains: [],
env: {},
},
};
expect(template.config.isolated !== false).toBe(false);
});
});
});

View File

@@ -30,9 +30,7 @@ describe("helpers functions", () => {
const domain = processValue("${domain}", {}, mockSchema);
expect(domain.startsWith(`${mockSchema.projectName}-`)).toBeTruthy();
expect(
domain.endsWith(
`${mockSchema.serverIp.replaceAll(".", "-")}.traefik.me`,
),
domain.endsWith(`${mockSchema.serverIp.replaceAll(".", "-")}.sslip.io`),
).toBeTruthy();
});
});

View File

@@ -21,9 +21,9 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import type { RouterOutputs } from "@/utils/api";
import type { ValidationStates } from "./show-domains";
import { AddDomain } from "./handle-domain";
import { DnsHelperModal } from "./dns-helper-modal";
import { AddDomain } from "./handle-domain";
import type { ValidationStates } from "./show-domains";
export type Domain =
| RouterOutputs["domain"]["byApplicationId"][0]
@@ -168,7 +168,7 @@ export const createColumns = ({
{domain.certificateType}
</Badge>
)}
{!domain.host.includes("traefik.me") && (
{!domain.host.includes("sslip.io") && (
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
@@ -256,7 +256,7 @@ export const createColumns = ({
return (
<div className="flex items-center gap-2">
{!domain.host.includes("traefik.me") && (
{!domain.host.includes("sslip.io") && (
<DnsHelperModal
domain={{
host: domain.host,

View File

@@ -225,7 +225,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
const https = form.watch("https");
const domainType = form.watch("domainType");
const host = form.watch("host");
const isTraefikMeDomain = host?.includes("traefik.me") || false;
const isTraefikMeDomain = host?.includes("sslip.io") || false;
useEffect(() => {
if (data) {
@@ -513,7 +513,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
render={({ field }) => (
<FormItem>
{!canGenerateTraefikMeDomains &&
field.value.includes("traefik.me") && (
field.value.includes("sslip.io") && (
<AlertBlock type="warning">
You need to set an IP address in your{" "}
<Link
@@ -524,12 +524,12 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
? "Remote Servers -> Server -> Edit Server -> Update IP Address"
: "Web Server -> Server -> Update Server IP"}
</Link>{" "}
to make your traefik.me domain work.
to make your sslip.io domain work.
</AlertBlock>
)}
{isTraefikMeDomain && (
<AlertBlock type="info">
<strong>Note:</strong> traefik.me is a public HTTP
<strong>Note:</strong> sslip.io is a public HTTP
service and does not support SSL/HTTPS. HTTPS and
certificate options will not have any effect.
</AlertBlock>
@@ -567,7 +567,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
sideOffset={5}
className="max-w-[10rem]"
>
<p>Generate traefik.me domain</p>
<p>Generate sslip.io domain</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>

View File

@@ -425,7 +425,7 @@ export const ShowDomains = ({ id, type }: Props) => {
</Badge>
)}
<div className="flex gap-2 flex-wrap">
{!item.host.includes("traefik.me") && (
{!item.host.includes("sslip.io") && (
<DnsHelperModal
domain={{
host: item.host,

View File

@@ -5,6 +5,7 @@ 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";
@@ -57,7 +58,10 @@ const BitbucketProviderSchema = z.object({
slug: z.string().optional(),
})
.required(),
branch: z.string().min(1, "Branch is required"),
branch: z
.string()
.min(1, "Branch is required")
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
bitbucketId: z.string().min(1, "Bitbucket Provider is required"),
watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().optional(),

View File

@@ -6,6 +6,7 @@ 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";
@@ -41,7 +42,10 @@ const GitProviderSchema = z.object({
repositoryURL: z.string().min(1, {
message: "Repository URL is required",
}),
branch: z.string().min(1, "Branch required"),
branch: z
.string()
.min(1, "Branch required")
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
sshKey: z.string().optional(),
watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false),
@@ -107,110 +111,103 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
return (
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="flex flex-col gap-4"
>
<div className="grid md:grid-cols-2 gap-4">
<div className="flex items-end col-span-2 gap-4">
<div className="grow">
<FormField
control={form.control}
name="repositoryURL"
render={({ field }) => (
<FormItem>
<div className="flex items-center justify-between">
<FormLabel>Repository URL</FormLabel>
{field.value?.startsWith("https://") && (
<Link
href={field.value}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary"
>
<GitIcon className="h-4 w-4" />
<span>View Repository</span>
</Link>
)}
</div>
<FormControl>
<Input placeholder="Repository URL" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
{sshKeys && sshKeys.length > 0 ? (
<FormField
control={form.control}
name="sshKey"
render={({ field }) => (
<FormItem className="basis-40">
<FormLabel className="w-full inline-flex justify-between">
SSH Key
<LockIcon className="size-4 text-muted-foreground" />
</FormLabel>
<FormControl>
<Select
key={field.value}
onValueChange={field.onChange}
defaultValue={field.value}
value={field.value}
>
<SelectTrigger>
<SelectValue placeholder="Select a key" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{sshKeys?.map((sshKey) => (
<SelectItem
key={sshKey.sshKeyId}
value={sshKey.sshKeyId}
>
{sshKey.name}
</SelectItem>
))}
<SelectItem value="none">None</SelectItem>
<SelectLabel>Keys ({sshKeys?.length})</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
</FormItem>
)}
/>
) : (
<Button
variant="secondary"
onClick={() => router.push("/dashboard/settings/ssh-keys")}
type="button"
>
<KeyRoundIcon className="size-4" /> Add SSH Key
</Button>
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4">
<div className="grid grid-cols-2 lg:grid-cols-4 gap-4 items-start">
<FormField
control={form.control}
name="repositoryURL"
render={({ field }) => (
<FormItem className="col-span-2 lg:col-span-3">
<div className="flex items-center justify-between h-5">
<FormLabel>Repository URL</FormLabel>
{field.value?.startsWith("https://") && (
<Link
href={field.value}
target="_blank"
rel="noopener noreferrer"
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary"
>
<GitIcon className="h-4 w-4" />
<span>View Repository</span>
</Link>
)}
</div>
<FormControl>
<Input placeholder="Repository URL" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
</div>
<div className="space-y-4">
/>
{sshKeys && sshKeys.length > 0 ? (
<FormField
control={form.control}
name="branch"
name="sshKey"
render={({ field }) => (
<FormItem>
<FormLabel>Branch</FormLabel>
<FormItem className="col-span-2 lg:col-span-1">
<FormLabel className="w-full inline-flex justify-between">
SSH Key
<LockIcon className="size-4 text-muted-foreground" />
</FormLabel>
<FormControl>
<Input placeholder="Branch" {...field} />
<Select
key={field.value}
onValueChange={field.onChange}
defaultValue={field.value}
value={field.value}
>
<SelectTrigger>
<SelectValue placeholder="Select a key" />
</SelectTrigger>
<SelectContent>
<SelectGroup>
{sshKeys?.map((sshKey) => (
<SelectItem
key={sshKey.sshKeyId}
value={sshKey.sshKeyId}
>
{sshKey.name}
</SelectItem>
))}
<SelectItem value="none">None</SelectItem>
<SelectLabel>Keys ({sshKeys?.length})</SelectLabel>
</SelectGroup>
</SelectContent>
</Select>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
</div>
) : (
<Button
variant="secondary"
onClick={() => router.push("/dashboard/settings/ssh-keys")}
type="button"
className="col-span-2 lg:col-span-1 lg:mt-7"
>
<KeyRoundIcon className="size-4" /> Add SSH Key
</Button>
)}
<FormField
control={form.control}
name="branch"
render={({ field }) => (
<FormItem className="col-span-2">
<FormLabel>Branch</FormLabel>
<FormControl>
<Input placeholder="Branch" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="buildPath"
render={({ field }) => (
<FormItem>
<FormItem className="col-span-2">
<FormLabel>Build Path</FormLabel>
<FormControl>
<Input placeholder="/" {...field} />
@@ -223,7 +220,7 @@ export const SaveGitProvider = ({ applicationId }: Props) => {
control={form.control}
name="watchPaths"
render={({ field }) => (
<FormItem className="md:col-span-2">
<FormItem className="col-span-2 lg:col-span-4">
<div className="flex items-center gap-2">
<FormLabel>Watch Paths</FormLabel>
<TooltipProvider>

View File

@@ -5,6 +5,7 @@ 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";
@@ -72,7 +73,10 @@ const GiteaProviderSchema = z.object({
owner: z.string().min(1, "Owner is required"),
})
.required(),
branch: z.string().min(1, "Branch is required"),
branch: z
.string()
.min(1, "Branch is required")
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
giteaId: z.string().min(1, "Gitea Provider is required"),
watchPaths: z.array(z.string()).default([]),
enableSubmodules: z.boolean().optional(),

View File

@@ -5,6 +5,7 @@ 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";
@@ -55,7 +56,10 @@ const GithubProviderSchema = z.object({
owner: z.string().min(1, "Owner is required"),
})
.required(),
branch: z.string().min(1, "Branch is required"),
branch: z
.string()
.min(1, "Branch is required")
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
githubId: z.string().min(1, "Github Provider is required"),
watchPaths: z.array(z.string()).optional(),
triggerType: z.enum(["push", "tag"]).default("push"),

View File

@@ -5,6 +5,7 @@ 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";
@@ -58,7 +59,10 @@ const GitlabProviderSchema = z.object({
id: z.number().nullable(),
})
.required(),
branch: z.string().min(1, "Branch is required"),
branch: z
.string()
.min(1, "Branch is required")
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
gitlabId: z.string().min(1, "Gitlab Provider is required"),
watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false),

View File

@@ -58,7 +58,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
<CardHeader>
<CardTitle className="text-xl">Deploy Settings</CardTitle>
</CardHeader>
<CardContent className="flex flex-row gap-4 flex-wrap">
<CardContent className="grid grid-cols-2 lg:flex lg:flex-row lg:flex-wrap gap-4">
<TooltipProvider delayDuration={0} disableHoverableContent={false}>
{canDeploy && (
<DialogAction
@@ -274,14 +274,14 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
>
<Button
variant="outline"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2"
className="flex items-center gap-1.5 focus-visible:ring-2 focus-visible:ring-offset-2 col-span-2"
>
<Terminal className="size-4 mr-1" />
Open Terminal
</Button>
</DockerTerminalModal>
{canUpdateService && (
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
<div className="flex flex-row items-center gap-2 justify-between rounded-md px-4 py-2 border col-span-2 md:col-span-1">
<span className="text-sm font-medium">Autodeploy</span>
<Switch
aria-label="Toggle autodeploy"
@@ -305,7 +305,7 @@ export const ShowGeneralApplication = ({ applicationId }: Props) => {
)}
{canUpdateService && (
<div className="flex flex-row items-center gap-2 rounded-md px-4 py-2 border">
<div className="flex flex-row items-center gap-2 justify-between rounded-md px-4 py-2 border col-span-2 md:col-span-1">
<span className="text-sm font-medium">Clean Cache</span>
<Switch
aria-label="Toggle clean cache"

View File

@@ -87,7 +87,7 @@ export const AddPreviewDomain = ({
});
const host = form.watch("host");
const isTraefikMeDomain = host?.includes("traefik.me") || false;
const isTraefikMeDomain = host?.includes("sslip.io") || false;
useEffect(() => {
if (data) {
@@ -162,7 +162,7 @@ export const AddPreviewDomain = ({
<FormItem>
{isTraefikMeDomain && (
<AlertBlock type="info">
<strong>Note:</strong> traefik.me is a public HTTP
<strong>Note:</strong> sslip.io is a public HTTP
service and does not support SSL/HTTPS. HTTPS and
certificate options will not have any effect.
</AlertBlock>
@@ -202,7 +202,7 @@ export const AddPreviewDomain = ({
sideOffset={5}
className="max-w-[10rem]"
>
<p>Generate traefik.me domain</p>
<p>Generate sslip.io domain</p>
</TooltipContent>
</Tooltip>
</TooltipProvider>

View File

@@ -88,7 +88,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
const form = useForm<Schema>({
defaultValues: {
env: "",
wildcardDomain: "*.traefik.me",
wildcardDomain: "*.sslip.io",
port: 3000,
previewLimit: 3,
previewLabels: [],
@@ -102,7 +102,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
const previewHttps = form.watch("previewHttps");
const wildcardDomain = form.watch("wildcardDomain");
const isTraefikMeDomain = wildcardDomain?.includes("traefik.me") || false;
const isTraefikMeDomain = wildcardDomain?.includes("sslip.io") || false;
useEffect(() => {
setIsEnabled(data?.isPreviewDeploymentsActive || false);
@@ -114,7 +114,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
env: data.previewEnv || "",
buildArgs: data.previewBuildArgs || "",
buildSecrets: data.previewBuildSecrets || "",
wildcardDomain: data.previewWildcard || "*.traefik.me",
wildcardDomain: data.previewWildcard || "*.sslip.io",
port: data.previewPort || 3000,
previewLabels: data.previewLabels || [],
previewLimit: data.previewLimit || 3,
@@ -173,7 +173,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
<div className="grid gap-4">
{isTraefikMeDomain && (
<AlertBlock type="info">
<strong>Note:</strong> traefik.me is a public HTTP service and
<strong>Note:</strong> sslip.io is a public HTTP service and
does not support SSL/HTTPS. HTTPS and certificate options will
not have any effect.
</AlertBlock>
@@ -192,7 +192,7 @@ export const ShowPreviewSettings = ({ applicationId }: Props) => {
<FormItem>
<FormLabel>Wildcard Domain</FormLabel>
<FormControl>
<Input placeholder="*.traefik.me" {...field} />
<Input placeholder="*.sslip.io" {...field} />
</FormControl>
<FormMessage />
</FormItem>

View File

@@ -80,6 +80,7 @@ export const commonCronExpressions = [
const formSchema = z
.object({
name: z.string().min(1, "Name is required"),
description: z.string().optional(),
cronExpression: z.string().min(1, "Cron expression is required"),
shellType: z.enum(["bash", "sh"]).default("bash"),
command: z.string(),
@@ -224,6 +225,7 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
resolver: standardSchemaResolver(formSchema),
defaultValues: {
name: "",
description: "",
cronExpression: "",
shellType: "bash",
command: "",
@@ -263,6 +265,7 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
if (scheduleId && schedule) {
form.reset({
name: schedule.name,
description: schedule.description || "",
cronExpression: schedule.cronExpression,
shellType: schedule.shellType,
command: schedule.command,
@@ -479,6 +482,26 @@ export const HandleSchedules = ({ id, scheduleId, scheduleType }: Props) => {
)}
/>
<FormField
control={form.control}
name="description"
render={({ field }) => (
<FormItem>
<FormLabel>Description</FormLabel>
<FormControl>
<Input
placeholder="Backs up the database every day at midnight"
{...field}
/>
</FormControl>
<FormDescription>
Optional description of what this schedule does
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<ScheduleFormField
name="cronExpression"
formControl={form.control}

View File

@@ -125,6 +125,11 @@ export const ShowSchedules = ({ id, scheduleType = "application" }: Props) => {
{schedule.enabled ? "Enabled" : "Disabled"}
</Badge>
</div>
{schedule.description && (
<p className="text-xs text-muted-foreground/70 [overflow-wrap:anywhere] line-clamp-2">
{schedule.description}
</p>
)}
<div className="flex items-center gap-2 text-sm text-muted-foreground flex-wrap">
<Badge
variant="outline"

View File

@@ -2,6 +2,10 @@ import { Loader2, MoreHorizontal, RefreshCw } from "lucide-react";
import dynamic from "next/dynamic";
import { useState } from "react";
import { toast } from "sonner";
import { ShowContainerConfig } from "@/components/dashboard/docker/config/show-container-config";
import { ShowContainerMounts } from "@/components/dashboard/docker/mounts/show-container-mounts";
import { ShowContainerNetworks } from "@/components/dashboard/docker/networks/show-container-networks";
import { DockerTerminalModal } from "@/components/dashboard/docker/terminal/docker-terminal-modal";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
@@ -36,10 +40,6 @@ import {
TableRow,
} from "@/components/ui/table";
import { api } from "@/utils/api";
import { ShowContainerConfig } from "@/components/dashboard/docker/config/show-container-config";
import { ShowContainerMounts } from "@/components/dashboard/docker/mounts/show-container-mounts";
import { ShowContainerNetworks } from "@/components/dashboard/docker/networks/show-container-networks";
import { DockerTerminalModal } from "@/components/dashboard/docker/terminal/docker-terminal-modal";
const DockerLogsId = dynamic(
() =>

View File

@@ -49,12 +49,12 @@ export const ComposeFileEditor = ({ composeId }: Props) => {
const composeFile = form.watch("composeFile");
useEffect(() => {
if (data && !composeFile) {
if (data) {
form.reset({
composeFile: data.composeFile || "",
});
}
}, [form, form.reset, data]);
}, [form, data]);
useEffect(() => {
if (data?.composeFile !== undefined) {

View File

@@ -5,6 +5,7 @@ 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";
@@ -57,7 +58,10 @@ const BitbucketProviderSchema = z.object({
slug: z.string().optional(),
})
.required(),
branch: z.string().min(1, "Branch is required"),
branch: z
.string()
.min(1, "Branch is required")
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
bitbucketId: z.string().min(1, "Bitbucket Provider is required"),
watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false),

View File

@@ -6,6 +6,7 @@ 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";
@@ -41,7 +42,10 @@ const GitProviderSchema = z.object({
repositoryURL: z.string().min(1, {
message: "Repository URL is required",
}),
branch: z.string().min(1, "Branch required"),
branch: z
.string()
.min(1, "Branch required")
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
sshKey: z.string().optional(),
watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false),

View File

@@ -1,10 +1,11 @@
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import { CheckIcon, ChevronsUpDown, Plus, X, HelpCircle } from "lucide-react";
import { CheckIcon, ChevronsUpDown, HelpCircle, Plus, X } from "lucide-react";
import Link from "next/link";
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";
@@ -57,7 +58,10 @@ const GiteaProviderSchema = z.object({
owner: z.string().min(1, "Owner is required"),
})
.required(),
branch: z.string().min(1, "Branch is required"),
branch: z
.string()
.min(1, "Branch is required")
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
giteaId: z.string().min(1, "Gitea Provider is required"),
watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false),

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";
@@ -55,7 +56,10 @@ const GithubProviderSchema = z.object({
owner: z.string().min(1, "Owner is required"),
})
.required(),
branch: z.string().min(1, "Branch is required"),
branch: z
.string()
.min(1, "Branch is required")
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
githubId: z.string().min(1, "Github Provider is required"),
watchPaths: z.array(z.string()).optional(),
triggerType: z.enum(["push", "tag"]).default("push"),

View File

@@ -5,6 +5,7 @@ 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";
@@ -58,7 +59,10 @@ const GitlabProviderSchema = z.object({
gitlabPathNamespace: z.string().min(1),
})
.required(),
branch: z.string().min(1, "Branch is required"),
branch: z
.string()
.min(1, "Branch is required")
.regex(VALID_BRANCH_REGEX, "Invalid branch name"),
gitlabId: z.string().min(1, "Gitlab Provider is required"),
watchPaths: z.array(z.string()).optional(),
enableSubmodules: z.boolean().default(false),

View File

@@ -288,7 +288,6 @@ export const RestoreBackup = ({
toast.error("Please select a database type");
return;
}
console.log({ data });
setIsDeploying(true);
};

View File

@@ -1,5 +1,14 @@
"use client";
import { Bot, Loader2, RotateCcw, Settings, X } from "lucide-react";
import copy from "copy-to-clipboard";
import {
Bot,
Check,
Copy,
Loader2,
RotateCcw,
Settings,
X,
} from "lucide-react";
import Link from "next/link";
import { useState } from "react";
import ReactMarkdown from "react-markdown";
@@ -30,6 +39,7 @@ const MAX_LOG_LINES = 200;
export function AnalyzeLogs({ logs, context }: Props) {
const [open, setOpen] = useState(false);
const [aiId, setAiId] = useState<string>("");
const [copied, setCopied] = useState(false);
const { data: providers } = api.ai.getEnabledProviders.useQuery(undefined, {
enabled: open,
});
@@ -52,6 +62,15 @@ export function AnalyzeLogs({ logs, context }: Props) {
mutate({ aiId, logs: logsText, context });
};
const handleCopy = () => {
if (!data?.analysis) return;
const success = copy(data.analysis);
if (success) {
setCopied(true);
setTimeout(() => setCopied(false), 2000);
}
};
return (
<Popover
open={open}
@@ -168,6 +187,18 @@ export function AnalyzeLogs({ logs, context }: Props) {
)}
Re-analyze
</Button>
<Button
size="sm"
variant="outline"
onClick={handleCopy}
title="Copy analysis to clipboard"
>
{copied ? (
<Check className="h-3.5 w-3.5" />
) : (
<Copy className="h-3.5 w-3.5" />
)}
</Button>
<Button
size="sm"
variant="ghost"

View File

@@ -1,3 +1,4 @@
import { Badge } from "@/components/ui/badge";
import {
Dialog,
DialogContent,
@@ -15,7 +16,6 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Badge } from "@/components/ui/badge";
import { api } from "@/utils/api";
interface Props {

View File

@@ -1,3 +1,4 @@
import { Badge } from "@/components/ui/badge";
import {
Dialog,
DialogContent,
@@ -15,7 +16,6 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Badge } from "@/components/ui/badge";
import { api } from "@/utils/api";
interface Props {

View File

@@ -26,8 +26,8 @@ import {
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import {
uploadFileToContainerSchema,
type UploadFileToContainer,
uploadFileToContainerSchema,
} from "@/utils/schema";
interface Props {

View File

@@ -1,10 +1,10 @@
import { toast } from "sonner";
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
import { UpdateDatabasePassword } from "@/components/shared/update-database-password";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { api } from "@/utils/api";
import { toast } from "sonner";
interface Props {
mariadbId: string;

View File

@@ -1,10 +1,10 @@
import { toast } from "sonner";
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
import { UpdateDatabasePassword } from "@/components/shared/update-database-password";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { api } from "@/utils/api";
import { toast } from "sonner";
interface Props {
mongoId: string;

View File

@@ -1,10 +1,10 @@
import { toast } from "sonner";
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
import { UpdateDatabasePassword } from "@/components/shared/update-database-password";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { api } from "@/utils/api";
import { toast } from "sonner";
interface Props {
mysqlId: string;

View File

@@ -1,10 +1,10 @@
import { toast } from "sonner";
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
import { UpdateDatabasePassword } from "@/components/shared/update-database-password";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { api } from "@/utils/api";
import { toast } from "sonner";
interface Props {
postgresId: string;

View File

@@ -632,7 +632,6 @@ export const AddDatabase = ({ environmentId, projectName }: Props) => {
control={form.control}
name="enableNamespaces"
render={({ field }) => {
console.log(field.value);
return (
<FormItem>
<FormLabel>Enable Namespaces</FormLabel>

View File

@@ -1,6 +1,6 @@
import {
BookText,
Bookmark,
BookText,
CheckIcon,
ChevronsUpDown,
Globe,

View File

@@ -344,7 +344,7 @@ export const ShowProjects = () => {
}
}}
>
<Card className="group relative w-full h-full bg-transparent transition-colors hover:bg-border">
<Card className="group relative w-full h-full bg-transparent transition-colors hover:bg-border flex flex-col">
<CardHeader>
<CardTitle className="flex items-center justify-between gap-2 overflow-clip">
<span className="flex flex-col gap-1.5 ">
@@ -491,7 +491,7 @@ export const ShowProjects = () => {
</div>
</CardTitle>
</CardHeader>
<CardFooter className="pt-4">
<CardFooter className="pt-4 mt-auto">
<div className="space-y-1 text-xs flex flex-row justify-between max-sm:flex-wrap w-full gap-2 sm:gap-4">
<DateTooltip date={project.createdAt}>
Created

View File

@@ -1,10 +1,10 @@
import { toast } from "sonner";
import { ToggleVisibilityInput } from "@/components/shared/toggle-visibility-input";
import { UpdateDatabasePassword } from "@/components/shared/update-database-password";
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { api } from "@/utils/api";
import { toast } from "sonner";
interface Props {
redisId: string;

View File

@@ -25,7 +25,6 @@ import {
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { NumberInput } from "@/components/ui/input";
import {
Dialog,
DialogContent,
@@ -34,6 +33,7 @@ import {
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import { NumberInput } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Progress } from "@/components/ui/progress";
import { Switch } from "@/components/ui/switch";

View File

@@ -1,3 +1,5 @@
import { HelpCircle } from "lucide-react";
import { toast } from "sonner";
import { Label } from "@/components/ui/label";
import { Switch } from "@/components/ui/switch";
import {
@@ -7,8 +9,6 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { api } from "@/utils/api";
import { HelpCircle } from "lucide-react";
import { toast } from "sonner";
interface Props {
serverId?: string;

View File

@@ -3,6 +3,7 @@ import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
import { EnterpriseFeatureLocked } from "@/components/proprietary/enterprise-feature-gate";
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import { Checkbox } from "@/components/ui/checkbox";
@@ -26,7 +27,6 @@ import {
FormMessage,
} from "@/components/ui/form";
import { Switch } from "@/components/ui/switch";
import { EnterpriseFeatureLocked } from "@/components/proprietary/enterprise-feature-gate";
import { api, type RouterOutputs } from "@/utils/api";
/** Shape returned by project.allForPermissions (admin only). Used for the permissions UI. */

View File

@@ -141,14 +141,14 @@ export const WebDomain = () => {
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
className="grid w-full gap-4 md:grid-cols-2"
className="grid w-full gap-4 grid-cols-2"
>
<FormField
control={form.control}
name="domain"
render={({ field }) => {
return (
<FormItem>
<FormItem className="col-span-2 md:col-span-1">
<FormLabel>Domain</FormLabel>
<FormControl>
<Input
@@ -168,7 +168,7 @@ export const WebDomain = () => {
name="letsEncryptEmail"
render={({ field }) => {
return (
<FormItem>
<FormItem className="col-span-2 md:col-span-1">
<FormLabel>Let's Encrypt Email</FormLabel>
<FormControl>
<Input
@@ -209,7 +209,7 @@ export const WebDomain = () => {
name="certificateType"
render={({ field }) => {
return (
<FormItem className="md:col-span-2">
<FormItem className="col-span-2">
<FormLabel>Certificate Provider</FormLabel>
<Select
onValueChange={field.onChange}

View File

@@ -868,6 +868,19 @@ function SidebarLogo() {
);
}
function MobileCloser() {
const pathname = usePathname();
const { setOpenMobile, isMobile } = useSidebar();
useEffect(() => {
if (isMobile) {
setOpenMobile(false);
}
}, [pathname, isMobile, setOpenMobile]);
return null;
}
export default function Page({ children }: Props) {
const [defaultOpen, setDefaultOpen] = useState<boolean | undefined>(
undefined,
@@ -933,6 +946,7 @@ export default function Page({ children }: Props) {
} as React.CSSProperties
}
>
<MobileCloser />
<Sidebar collapsible="icon" variant="floating">
<SidebarHeader>
{/* <SidebarMenuButton

View File

@@ -342,7 +342,7 @@ export const AdvanceBreadcrumb = () => {
className="h-auto px-2 py-1.5 hover:bg-accent gap-2"
>
<FolderInput className="size-4 text-muted-foreground" />
<span className="font-medium max-w-[150px] truncate">
<span className="font-medium max-w-[50px] md:max-w-[150px] truncate">
{currentProject?.name || "Select Project"}
</span>
<ChevronDown className="size-4 text-muted-foreground" />
@@ -478,7 +478,7 @@ export const AdvanceBreadcrumb = () => {
aria-expanded={environmentOpen}
className="h-auto px-2 py-1.5 hover:bg-accent gap-2"
>
<span className="font-medium max-w-[150px] truncate">
<span className="font-medium max-w-[50px] md:max-w-[150px] truncate">
{currentEnvironment?.name || "production"}
</span>
<ChevronDown className="size-4 text-muted-foreground" />
@@ -533,7 +533,7 @@ export const AdvanceBreadcrumb = () => {
)}
{projectEnvironments && projectEnvironments.length === 1 && (
<p className="text-sm font-normal ml-1">
<p className="text-sm font-normal ml-1 max-w-[50px] md:max-w-[150px] truncate">
{currentEnvironment?.name || "production"}
</p>
)}
@@ -551,7 +551,7 @@ export const AdvanceBreadcrumb = () => {
className="h-auto px-2 py-1.5 hover:bg-accent gap-2"
>
{getServiceIcon(currentService.type)}
<span className="font-medium max-w-[150px] truncate">
<span className="font-medium max-w-[50px] md:max-w-[150px] truncate">
{currentService.name}
</span>
<ChevronDown className="size-4 text-muted-foreground" />
@@ -617,7 +617,7 @@ export const AdvanceBreadcrumb = () => {
<Button
variant="ghost"
size="icon"
className="size-7 ml-1"
className="size-7 ml-1 hidden md:flex"
onClick={() => {
router.push(
`/dashboard/project/${projectId}/environment/${environmentId}`,

View File

@@ -63,6 +63,7 @@ const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
className={cn(
buttonVariants({ variant, size, className }),
"flex gap-2",
className,
)}
ref={ref}
{...props}

View File

@@ -0,0 +1 @@
ALTER TABLE "schedule" ADD COLUMN "description" text;

File diff suppressed because it is too large Load Diff

View File

@@ -1163,6 +1163,13 @@
"when": 1775845419261,
"tag": "0165_abnormal_greymalkin",
"breakpoints": true
},
{
"idx": 166,
"version": "7",
"when": 1778303519111,
"tag": "0166_nosy_slapstick",
"breakpoints": true
}
]
}

View File

@@ -28,6 +28,7 @@ try {
"wait-for-postgres": "wait-for-postgres.ts",
"reset-password": "reset-password.ts",
"reset-2fa": "reset-2fa.ts",
"migrate-auth-secret": "scripts/migrate-auth-secret.ts",
},
bundle: true,
platform: "node",

View File

@@ -1,6 +1,6 @@
{
"name": "dokploy",
"version": "v0.29.2",
"version": "v0.29.3",
"private": true,
"license": "Apache-2.0",
"type": "module",
@@ -14,6 +14,7 @@
"wait-for-postgres-dev": "tsx -r dotenv/config wait-for-postgres.ts",
"reset-password": "node -r dotenv/config dist/reset-password.mjs",
"reset-2fa": "node -r dotenv/config dist/reset-2fa.mjs",
"migrate-auth-secret": "node -r dotenv/config dist/migrate-auth-secret.mjs",
"dev": "tsx -r dotenv/config ./server/server.ts --project tsconfig.server.json ",
"studio": "drizzle-kit studio --config ./server/db/drizzle.config.ts",
"migration:generate": "drizzle-kit generate --config ./server/db/drizzle.config.ts",
@@ -126,7 +127,7 @@
"next-themes": "^0.2.1",
"nextjs-toploader": "^3.9.17",
"node-os-utils": "2.0.1",
"node-pty": "1.0.0",
"node-pty": "1.1.0",
"node-schedule": "2.1.1",
"nodemailer": "6.9.14",
"octokit": "3.1.2",

View File

@@ -28,6 +28,11 @@ export default async function handler(
res: NextApiResponse,
) {
const signature = req.headers["x-hub-signature-256"];
if (!signature) {
res.status(401).json({ message: "Missing signature header" });
return;
}
const githubBody = req.body;
if (!githubBody?.installation?.id) {

View File

@@ -84,7 +84,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
if (!user) {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/",
},
};

View File

@@ -24,7 +24,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
if (!user) {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/",
},
};

View File

@@ -95,7 +95,7 @@ export async function getServerSideProps(
if (IS_CLOUD) {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/dashboard/home",
},
};
@@ -104,7 +104,7 @@ export async function getServerSideProps(
if (!user) {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/",
},
};

View File

@@ -1099,7 +1099,7 @@ const EnvironmentPage = (
</div>
<CardContent className="space-y-2 py-8 border-t gap-4 flex flex-col min-h-[60vh]">
<>
<div className="flex flex-col gap-4 xl:flex-row xl:items-center xl:justify-between">
<div className="flex flex-col gap-4 2xl:flex-row 2xl:items-center 2xl:justify-between">
<div className="flex items-center gap-4">
<div className="flex items-center gap-2">
<Checkbox
@@ -1620,9 +1620,9 @@ const EnvironmentPage = (
<ContextMenuTrigger asChild>
<Link
href={`/dashboard/project/${projectId}/environment/${environmentId}/services/${service.type}/${service.id}`}
className="block"
className="block h-full"
>
<Card className="flex flex-col group relative cursor-pointer bg-transparent transition-colors hover:bg-border">
<Card className="flex flex-col h-full group relative cursor-pointer bg-transparent transition-colors hover:bg-border">
{service.serverId && (
<div className="absolute -left-1 -top-2">
<ServerIcon className="size-4 text-muted-foreground" />
@@ -1827,7 +1827,7 @@ export async function getServerSideProps(
if (!user) {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/",
},
};

View File

@@ -451,7 +451,7 @@ export async function getServerSideProps(
if (!user) {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/",
},
};

View File

@@ -455,7 +455,7 @@ export async function getServerSideProps(
if (!user) {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/",
},
};

View File

@@ -307,7 +307,7 @@ export async function getServerSideProps(
if (!user) {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/",
},
};

View File

@@ -336,7 +336,7 @@ export async function getServerSideProps(
if (!user) {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/",
},
};

View File

@@ -340,7 +340,7 @@ export async function getServerSideProps(
if (!user) {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/",
},
};

View File

@@ -318,7 +318,7 @@ export async function getServerSideProps(
if (!user) {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/",
},
};

View File

@@ -324,7 +324,7 @@ export async function getServerSideProps(
if (!user) {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/",
},
};

View File

@@ -329,7 +329,7 @@ export async function getServerSideProps(
if (!user) {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/",
},
};

View File

@@ -56,7 +56,7 @@ export async function getServerSideProps(
if (!user) {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/",
},
};

View File

@@ -17,7 +17,7 @@ export async function getServerSideProps(
if (IS_CLOUD) {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/dashboard/home",
},
};
@@ -26,7 +26,7 @@ export async function getServerSideProps(
if (!user) {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/",
},
};

View File

@@ -34,7 +34,7 @@ export async function getServerSideProps(
if (IS_CLOUD) {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/dashboard/home",
},
};
@@ -43,7 +43,7 @@ export async function getServerSideProps(
if (!user || (user.role !== "owner" && user.role !== "admin")) {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/",
},
};

View File

@@ -45,7 +45,7 @@ export async function getServerSideProps(
if (!user || user.role === "member") {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/",
},
};

View File

@@ -27,7 +27,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
if (!user) {
return {
redirect: { destination: "/", permanent: true },
redirect: { destination: "/", permanent: false },
};
}

View File

@@ -23,7 +23,7 @@ export async function getServerSideProps(
if (!IS_CLOUD) {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/dashboard/home",
},
};
@@ -33,7 +33,7 @@ export async function getServerSideProps(
if (!user || user.role !== "owner") {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/",
},
};

View File

@@ -28,7 +28,7 @@ export async function getServerSideProps(
if (!user || user.role === "member") {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/",
},
};

View File

@@ -27,7 +27,7 @@ export async function getServerSideProps(
if (IS_CLOUD) {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/dashboard/home",
},
};
@@ -36,7 +36,7 @@ export async function getServerSideProps(
if (!user || user.role === "member") {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/",
},
};

View File

@@ -28,7 +28,7 @@ export async function getServerSideProps(
if (!user || user.role === "member") {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/",
},
};

View File

@@ -27,7 +27,7 @@ export async function getServerSideProps(
if (!user) {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/",
},
};
@@ -53,7 +53,7 @@ export async function getServerSideProps(
if (!userPermissions?.gitProviders.read) {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/",
},
};

View File

@@ -23,7 +23,7 @@ export async function getServerSideProps(
if (!IS_CLOUD) {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/dashboard/home",
},
};
@@ -33,7 +33,7 @@ export async function getServerSideProps(
if (!user || user.role !== "owner") {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/",
},
};

View File

@@ -38,7 +38,7 @@ export async function getServerSideProps(
if (!user) {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/",
},
};
@@ -46,7 +46,7 @@ export async function getServerSideProps(
if (user.role !== "owner") {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/dashboard/settings/profile",
},
};

View File

@@ -28,7 +28,7 @@ export async function getServerSideProps(
if (!user || user.role === "member") {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/",
},
};

View File

@@ -54,7 +54,7 @@ export async function getServerSideProps(
if (!user) {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/",
},
};

View File

@@ -28,7 +28,7 @@ export async function getServerSideProps(
if (!user || user.role === "member") {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/",
},
};

View File

@@ -44,7 +44,7 @@ export async function getServerSideProps(
if (IS_CLOUD) {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/dashboard/home",
},
};
@@ -53,7 +53,7 @@ export async function getServerSideProps(
if (!user) {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/",
},
};
@@ -61,7 +61,7 @@ export async function getServerSideProps(
if (user.role === "member") {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/dashboard/settings/profile",
},
};

View File

@@ -28,7 +28,7 @@ export async function getServerSideProps(
if (!user) {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/",
},
};
@@ -36,7 +36,7 @@ export async function getServerSideProps(
if (user.role === "member") {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/dashboard/settings/profile",
},
};

View File

@@ -27,7 +27,7 @@ export async function getServerSideProps(
if (!user) {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/",
},
};
@@ -54,7 +54,7 @@ export async function getServerSideProps(
if (!userPermissions?.sshKeys.read) {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/",
},
};

View File

@@ -46,7 +46,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
if (!user) {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/",
},
};
@@ -54,7 +54,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
if (user.role === "member") {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/dashboard/settings/profile",
},
};

View File

@@ -47,7 +47,7 @@ export async function getServerSideProps(
if (!userPermissions?.tag.read) {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/",
},
};

View File

@@ -39,7 +39,7 @@ export async function getServerSideProps(
if (!user) {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/",
},
};
@@ -66,7 +66,7 @@ export async function getServerSideProps(
if (!userPermissions?.member.read) {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/",
},
};

View File

@@ -46,7 +46,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
if (!user) {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/",
},
};
@@ -54,7 +54,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
if (user.role !== "owner") {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/dashboard/settings/profile",
},
};

View File

@@ -4,8 +4,8 @@ import { createServerSideHelpers } from "@trpc/react-query/server";
import type { GetServerSidePropsContext } from "next";
import type { ReactElement } from "react";
import superjson from "superjson";
import SwarmMonitorCard from "@/components/dashboard/swarm/monitoring-card";
import { ShowSwarmContainers } from "@/components/dashboard/swarm/containers/show-swarm-containers";
import SwarmMonitorCard from "@/components/dashboard/swarm/monitoring-card";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { Card } from "@/components/ui/card";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
@@ -45,7 +45,7 @@ export async function getServerSideProps(
if (IS_CLOUD) {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/dashboard/home",
},
};
@@ -54,7 +54,7 @@ export async function getServerSideProps(
if (!user) {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/",
},
};
@@ -80,7 +80,7 @@ export async function getServerSideProps(
if (!userPermissions?.docker.read) {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/",
},
};

View File

@@ -23,7 +23,7 @@ export async function getServerSideProps(
if (IS_CLOUD) {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/dashboard/home",
},
};
@@ -32,7 +32,7 @@ export async function getServerSideProps(
if (!user) {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/",
},
};
@@ -58,7 +58,7 @@ export async function getServerSideProps(
if (!userPermissions?.traefikFiles.read) {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/",
},
};

View File

@@ -407,7 +407,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
if (user) {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/dashboard/home",
},
};
@@ -425,7 +425,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
if (!hasAdmin) {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/register",
},
};
@@ -436,7 +436,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
if (user) {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/dashboard/home",
},
};

View File

@@ -333,7 +333,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
// if (IS_CLOUD) {
// return {
// redirect: {
// permanent: true,
// permanent: false,
// destination: "/",
// },
// };
@@ -342,7 +342,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
if (typeof token !== "string") {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/",
},
};
@@ -365,7 +365,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
if (invitation.isExpired) {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/",
},
};
@@ -382,7 +382,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
console.log("error", error);
return {
redirect: {
permanent: true,
permanent: false,
destination: "/",
},
};

View File

@@ -302,7 +302,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
if (user) {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/dashboard/home",
},
};

View File

@@ -190,7 +190,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
if (!IS_CLOUD) {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/",
},
};
@@ -200,7 +200,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
if (typeof token !== "string") {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/",
},
};

View File

@@ -30,6 +30,9 @@ const loginSchema = z.object({
.min(1, {
message: "Email is required",
})
.max(255, {
message: "Email must be at most 255 characters",
})
.email({
message: "Email must be a valid email",
}),
@@ -118,7 +121,11 @@ export default function Home() {
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="Email" {...field} />
<Input
placeholder="Email"
maxLength={255}
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
@@ -161,7 +168,7 @@ export async function getServerSideProps(_context: GetServerSidePropsContext) {
if (!IS_CLOUD) {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/",
},
};

View File

@@ -82,7 +82,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
if (!user) {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/",
},
};
@@ -103,7 +103,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
if (!userPermissions?.api.read) {
return {
redirect: {
permanent: true,
permanent: false,
destination: "/",
},
};

View File

@@ -0,0 +1,97 @@
/**
* Use this command to automatically migrate the auth secret: curl -sSL https://dokploy.com/security/0.29.3.sh | bash
* Migration script: re-encrypt 2FA secrets after rotating BETTER_AUTH_SECRET.
*
* Usage:
* OLD_SECRET=<old_secret> NEW_SECRET=<new_secret> npx tsx apps/dokploy/scripts/migrate-auth-secret.ts
*
* Both OLD_SECRET and NEW_SECRET are required.
* Run this BEFORE restarting Dokploy with the new secret.
*/
import { db } from "@dokploy/server/db";
import { twoFactor } from "@dokploy/server/db/schema";
import { symmetricDecrypt, symmetricEncrypt } from "better-auth/crypto";
import { eq } from "drizzle-orm";
const OLD_SECRET = process.env.OLD_SECRET as string;
const NEW_SECRET = process.env.NEW_SECRET as string;
if (!OLD_SECRET || !NEW_SECRET) {
console.error(
"❌ OLD_SECRET and NEW_SECRET environment variables are required.",
);
console.error(
" Usage: OLD_SECRET=<old> NEW_SECRET=<new> npx tsx apps/dokploy/scripts/migrate-auth-secret.ts",
);
process.exit(1);
}
if (OLD_SECRET === NEW_SECRET) {
console.error("❌ OLD_SECRET and NEW_SECRET must be different.");
process.exit(1);
}
async function reEncrypt(
value: string,
oldSecret: string,
newSecret: string,
): Promise<string> {
const plaintext = await symmetricDecrypt({ key: oldSecret, data: value });
return symmetricEncrypt({ key: newSecret, data: plaintext });
}
async function main() {
console.log("🔍 Fetching 2FA records...");
const records = await db.select().from(twoFactor);
if (records.length === 0) {
console.log("✅ No 2FA records found, nothing to migrate.");
return;
}
console.log(`📦 Found ${records.length} 2FA record(s) to migrate.`);
let migrated = 0;
let failed = 0;
await db.transaction(async (tx) => {
for (const record of records) {
try {
const [newSecret, newBackupCodes] = await Promise.all([
reEncrypt(record.secret, OLD_SECRET, NEW_SECRET),
reEncrypt(record.backupCodes, OLD_SECRET, NEW_SECRET),
]);
await tx
.update(twoFactor)
.set({ secret: newSecret, backupCodes: newBackupCodes })
.where(eq(twoFactor.id, record.id));
migrated++;
} catch (err) {
console.error(
`❌ Failed to migrate record ${record.id} (userId: ${record.userId}):`,
err,
);
failed++;
throw err; // rollback the whole transaction
}
}
});
console.log(`✅ Migrated ${migrated} record(s) successfully.`);
if (failed > 0) {
console.error(
`${failed} record(s) failed — transaction was rolled back.`,
);
process.exit(1);
} else {
process.exit(0);
}
}
main().catch((err) => {
console.error("❌ Migration failed:", err);
process.exit(1);
});

View File

@@ -25,8 +25,8 @@ import { findProjectById } from "@dokploy/server/services/project";
import {
getProviderHeaders,
getProviderName,
selectAIProvider,
type Model,
selectAIProvider,
} from "@dokploy/server/utils/ai/select-ai-provider";
import { TRPCError } from "@trpc/server";
import { generateText } from "ai";

View File

@@ -640,7 +640,7 @@ export const composeRouter = createTRPCRouter({
name: input.id,
sourceType: "raw",
appName: appName,
isolatedDeployment: true,
isolatedDeployment: template.config.config?.isolated !== false,
});
await addNewService(ctx, compose.composeId);
@@ -700,11 +700,14 @@ export const composeRouter = createTRPCRouter({
getTags: protectedProcedure
.input(z.object({ baseUrl: z.string().optional() }))
.query(async ({ input }) => {
const githubTemplates = await fetchTemplatesList(input.baseUrl);
const allTags = githubTemplates.flatMap((template) => template.tags);
const uniqueTags = _.uniq(allTags);
return uniqueTags;
try {
const githubTemplates = await fetchTemplatesList(input.baseUrl);
const allTags = githubTemplates.flatMap((template) => template.tags);
return _.uniq(allTags);
} catch (error) {
console.warn("Failed to fetch template tags:", error);
return [];
}
}),
disconnectGitProvider: protectedProcedure
.input(apiFindCompose)

View File

@@ -6,6 +6,7 @@ import {
findEnvironmentById,
findLibsqlById,
findProjectById,
getAccessibleServerIds,
getContainerLogs,
IS_CLOUD,
rebuildDatabase,
@@ -16,7 +17,6 @@ import {
stopService,
stopServiceRemote,
updateLibsqlById,
getAccessibleServerIds,
} from "@dokploy/server";
import {
addNewService,

View File

@@ -9,6 +9,7 @@ import {
findEnvironmentById,
findMySqlById,
findProjectById,
getAccessibleServerIds,
getContainerLogs,
getServiceContainerCommand,
IS_CLOUD,
@@ -20,7 +21,6 @@ import {
stopService,
stopServiceRemote,
updateMySqlById,
getAccessibleServerIds,
} from "@dokploy/server";
import { db } from "@dokploy/server/db";
import {

View File

@@ -9,6 +9,7 @@ import {
findEnvironmentById,
findPostgresById,
findProjectById,
getAccessibleServerIds,
getContainerLogs,
getMountPath,
getServiceContainerCommand,
@@ -21,7 +22,6 @@ import {
stopService,
stopServiceRemote,
updatePostgresById,
getAccessibleServerIds,
} from "@dokploy/server";
import { db } from "@dokploy/server/db";
import {

View File

@@ -856,8 +856,6 @@ export const projectRouter = createTRPCRouter({
ctx.session.activeOrganizationId,
).then((value) => value.environment);
console.log("targetProject", targetProject);
if (input.includeServices) {
const servicesToDuplicate = input.selectedServices || [];

View File

@@ -8,6 +8,7 @@ import {
findEnvironmentById,
findProjectById,
findRedisById,
getAccessibleServerIds,
getContainerLogs,
getServiceContainerCommand,
IS_CLOUD,
@@ -19,7 +20,6 @@ import {
stopService,
stopServiceRemote,
updateRedisById,
getAccessibleServerIds,
} from "@dokploy/server";
import { db } from "@dokploy/server/db";
import {

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