Compare commits

..

38 Commits

Author SHA1 Message Date
Mauricio Siu
fa201a5a96 Update package.json 2026-01-31 04:35:39 -06:00
Mauricio Siu
431ad914f8 Merge pull request #3568 from Dokploy/copilot/fix-swarm-settings-test-commands
Fix swarm health check test commands not persisting
2026-01-31 03:21:20 -06:00
Mauricio Siu
0575fabb0f Merge branch 'canary' into copilot/fix-swarm-settings-test-commands 2026-01-31 03:19:29 -06:00
Mauricio Siu
385a494c83 Merge pull request #3556 from vtomasr5/fix-saving-swarm-settings-placement-preferences
fix: Save Placement button not working for Preferences in Swarm settings
2026-01-31 03:18:41 -06:00
copilot-swe-agent[bot]
d3f0bf654b Fix TypeScript type annotations in health check form
Co-authored-by: Siumauricio <47042324+Siumauricio@users.noreply.github.com>
2026-01-31 09:16:49 +00:00
copilot-swe-agent[bot]
9e8dacfe06 Fix health check form to properly sync test commands with form state
Co-authored-by: Siumauricio <47042324+Siumauricio@users.noreply.github.com>
2026-01-31 09:14:40 +00:00
copilot-swe-agent[bot]
f450b13dc5 Initial plan 2026-01-31 09:10:37 +00:00
Mauricio Siu
9e80bf45d0 Merge pull request #3567 from Dokploy/fix/security-GHSA-wmqj-wr9q-327p
feat(schema): enhance appName validation across database schemas with…
2026-01-31 03:06:56 -06:00
Mauricio Siu
a635908e43 fix(mariadb): correct appName validation to use built appName for uniqueness check 2026-01-31 03:05:08 -06:00
Mauricio Siu
960892fd8d feat(schema): enhance appName validation across database schemas with regex and message 2026-01-31 03:01:49 -06:00
Vicens Juan Tomas Monserrat
8caae549b2 fix(swarm): resolve Save Placement button not working for Preferences
The button was unresponsive because the form's flat data structure
  ({ SpreadDescriptor }) didn't match the Zod schema's nested structure
  ({ Spread: { SpreadDescriptor } }), causing silent validation failure.

  Updated schema to match form state and transform to nested structure
  only when submitting to the API.
2026-01-30 11:48:34 +01:00
Mauricio Siu
20226a300c Merge pull request #3256 from luojiyin1987/fix/dockerfile-cmd-format
Fix/dockerfile cmd format
2026-01-28 09:57:07 -06:00
Mauricio Siu
5f5c4f0e18 Merge branch 'canary' into fix/dockerfile-cmd-format 2026-01-28 09:55:56 -06:00
Mauricio Siu
c579dbeb1c Merge pull request #3540 from Dokploy/3491-ssl-certificate-issuance-broken-with-inwx
chore(traefik): update Traefik version to 3.6.7 in setup scripts
2026-01-28 00:18:17 -06:00
Mauricio Siu
cee1dc97ba chore(traefik): update Traefik version to 3.6.7 in setup scripts 2026-01-28 00:16:06 -06:00
Mauricio Siu
b9419ed5f1 Merge pull request #3539 from Dokploy/3493-when-adding-a-git-repository-as-a-provider-spaces-in-the-repo-name-break-the-repo-selection
feat(bitbucket): add optional slug field for repositories and update …
2026-01-28 00:14:21 -06:00
Mauricio Siu
6bc07d7675 feat(drop): add optional bitbucketRepositorySlug field to baseApp configuration in tests 2026-01-28 00:12:42 -06:00
autofix-ci[bot]
f72dfb3fc7 [autofix.ci] apply automated fixes 2026-01-28 06:10:38 +00:00
Mauricio Siu
27a0490536 feat(bitbucket): add optional slug field for repositories and update related logic 2026-01-28 00:09:56 -06:00
Mauricio Siu
ec6849205a Merge pull request #3537 from Dokploy/3510-commit-message-is-wrong-when-using-remote-builder
fix(application): update commit info extraction to include appName an…
2026-01-27 21:47:19 -06:00
Mauricio Siu
9934346d8c fix(application): update commit info extraction to include appName and serverId 2026-01-27 21:46:54 -06:00
Mauricio Siu
5c89973cc2 Merge pull request #3385 from stripsior/chore/bump-postgres
chore(databases): bump default postgres version while creating to 18
2026-01-27 21:18:50 -06:00
Mauricio Siu
4e8cdfbc80 Merge pull request #3447 from pluisol/feature/pushover-notifications
feat: add Pushover notification provider
2026-01-27 21:16:36 -06:00
Mauricio Siu
d0ea8b5283 Merge pull request #3504 from Bima42/fix/3503-changing-server-domain-fail-with-only-mail
fix: zod object for assign domain
2026-01-27 13:41:05 -06:00
Mauricio Siu
060a053fdb Merge pull request #3527 from p8008d/fix/profile-firstname-update
fix: profile firstName field not updating
2026-01-27 13:39:32 -06:00
Mauricio Siu
304069d3c8 Merge pull request #3530 from Dokploy/fix/prevent-send-malicious-bash
feat(wss): add directory validation for WebSocket server log paths
2026-01-27 09:57:11 -06:00
Mauricio Siu
f3bb56910a Merge pull request #3529 from Dokploy/fix/prevent-send-malicious-bash
fix(wss): add container ID validation to enhance security in WebSocke…
2026-01-27 09:21:06 -06:00
Mauricio Siu
6fdb2e4a21 Merge pull request #3528 from Dokploy/fix/prevent-send-malicious-bash
Fix/prevent send malicious bash
2026-01-27 09:00:11 -06:00
p8008d
74aecf6828 fix: profile firstName field not updating
The profile form was sending `name` field but the database column is
`firstName`. This caused the firstName to be silently ignored during
updates. Changed form field and API schema to use `firstName` to match
the database column.
2026-01-27 15:07:56 +02:00
Bima42
bcbf433607 fix: zod object for assign domain 2026-01-22 08:56:07 +01:00
Plui Sol
7db1f3a69a feat: add Pushover notification provider 2026-01-12 21:35:07 -05:00
Plui Sol
67f0c93298 Merge remote-tracking branch 'origin/canary' into feature/pushover-notifications 2026-01-12 21:31:48 -05:00
Plui Sol
046c52529b feat: add Pushover notification provider 2026-01-12 21:31:12 -05:00
stripsior
27dd20b75d chore(databases): bump default postgres version while creating to 18 2026-01-03 15:16:11 +01:00
luojiyin
3142818cf2 fix(docker): use ENV for HOSTNAME and exec form CMD 2025-12-13 15:33:24 +08:00
luojiyin
d8465ac251 config: set port env 2025-12-13 12:36:15 +08:00
luojiyin
c33b41d082 fix(docker): use ENV for HOSTNAME and exec form CMD 2025-12-13 12:32:01 +08:00
luojiyin
3eea875932 code clear 2025-12-13 12:30:30 +08:00
50 changed files with 14972 additions and 135 deletions

View File

@@ -35,4 +35,5 @@ COPY --from=build /prod/schedules/dist ./dist
COPY --from=build /prod/schedules/package.json ./package.json COPY --from=build /prod/schedules/package.json ./package.json
COPY --from=build /prod/schedules/node_modules ./node_modules COPY --from=build /prod/schedules/node_modules ./node_modules
CMD HOSTNAME=0.0.0.0 && pnpm start ENV HOSTNAME=0.0.0.0
CMD ["pnpm", "start"]

View File

@@ -35,4 +35,5 @@ COPY --from=build /prod/api/dist ./dist
COPY --from=build /prod/api/package.json ./package.json COPY --from=build /prod/api/package.json ./package.json
COPY --from=build /prod/api/node_modules ./node_modules COPY --from=build /prod/api/node_modules ./node_modules
CMD HOSTNAME=0.0.0.0 && pnpm start ENV HOSTNAME=0.0.0.0
CMD ["pnpm", "start"]

View File

@@ -29,6 +29,7 @@ const baseApp: ApplicationNested = {
applicationId: "", applicationId: "",
previewLabels: [], previewLabels: [],
createEnvFile: true, createEnvFile: true,
bitbucketRepositorySlug: "",
herokuVersion: "", herokuVersion: "",
giteaBranch: "", giteaBranch: "",
buildServerId: "", buildServerId: "",

View File

@@ -8,6 +8,7 @@ const baseApp: ApplicationNested = {
applicationId: "", applicationId: "",
previewLabels: [], previewLabels: [],
createEnvFile: true, createEnvFile: true,
bitbucketRepositorySlug: "",
herokuVersion: "", herokuVersion: "",
giteaRepository: "", giteaRepository: "",
giteaOwner: "", giteaOwner: "",

View File

@@ -31,7 +31,6 @@ interface HealthCheckFormProps {
export const HealthCheckForm = ({ id, type }: HealthCheckFormProps) => { export const HealthCheckForm = ({ id, type }: HealthCheckFormProps) => {
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [testCommands, setTestCommands] = useState<string[]>([]);
const queryMap = { const queryMap = {
postgres: () => postgres: () =>
@@ -72,6 +71,8 @@ export const HealthCheckForm = ({ id, type }: HealthCheckFormProps) => {
}, },
}); });
const testCommands = form.watch("Test") || [];
useEffect(() => { useEffect(() => {
if (data?.healthCheckSwarm) { if (data?.healthCheckSwarm) {
const hc = data.healthCheckSwarm; const hc = data.healthCheckSwarm;
@@ -82,7 +83,6 @@ export const HealthCheckForm = ({ id, type }: HealthCheckFormProps) => {
StartPeriod: hc.StartPeriod, StartPeriod: hc.StartPeriod,
Retries: hc.Retries, Retries: hc.Retries,
}); });
setTestCommands(hc.Test || []);
} }
}, [data, form]); }, [data, form]);
@@ -117,17 +117,20 @@ export const HealthCheckForm = ({ id, type }: HealthCheckFormProps) => {
}; };
const addTestCommand = () => { const addTestCommand = () => {
setTestCommands([...testCommands, ""]); form.setValue("Test", [...testCommands, ""]);
}; };
const updateTestCommand = (index: number, value: string) => { const updateTestCommand = (index: number, value: string) => {
const newCommands = [...testCommands]; const newCommands = [...testCommands];
newCommands[index] = value; newCommands[index] = value;
setTestCommands(newCommands); form.setValue("Test", newCommands);
}; };
const removeTestCommand = (index: number) => { const removeTestCommand = (index: number) => {
setTestCommands(testCommands.filter((_, i) => i !== index)); form.setValue(
"Test",
testCommands.filter((_: string, i: number) => i !== index),
);
}; };
return ( return (
@@ -140,7 +143,7 @@ export const HealthCheckForm = ({ id, type }: HealthCheckFormProps) => {
http://localhost:3000/health"]) http://localhost:3000/health"])
</FormDescription> </FormDescription>
<div className="space-y-2 mt-2"> <div className="space-y-2 mt-2">
{testCommands.map((cmd, index) => ( {testCommands.map((cmd: string, index: number) => (
<div key={index} className="flex gap-2"> <div key={index} className="flex gap-2">
<Input <Input
value={cmd} value={cmd}

View File

@@ -1,10 +1,10 @@
export { HealthCheckForm } from "./health-check-form";
export { RestartPolicyForm } from "./restart-policy-form";
export { PlacementForm } from "./placement-form";
export { UpdateConfigForm } from "./update-config-form";
export { RollbackConfigForm } from "./rollback-config-form";
export { ModeForm } from "./mode-form";
export { LabelsForm } from "./labels-form";
export { StopGracePeriodForm } from "./stop-grace-period-form";
export { EndpointSpecForm } from "./endpoint-spec-form"; export { EndpointSpecForm } from "./endpoint-spec-form";
export { HealthCheckForm } from "./health-check-form";
export { LabelsForm } from "./labels-form";
export { ModeForm } from "./mode-form";
export { PlacementForm } from "./placement-form";
export { RestartPolicyForm } from "./restart-policy-form";
export { RollbackConfigForm } from "./rollback-config-form";
export { StopGracePeriodForm } from "./stop-grace-period-form";
export { UpdateConfigForm } from "./update-config-form";
export { filterEmptyValues, hasValues } from "./utils"; export { filterEmptyValues, hasValues } from "./utils";

View File

@@ -17,9 +17,7 @@ import { Input } from "@/components/ui/input";
import { api } from "@/utils/api"; import { api } from "@/utils/api";
const PreferenceSchema = z.object({ const PreferenceSchema = z.object({
Spread: z.object({ SpreadDescriptor: z.string(),
SpreadDescriptor: z.string(),
}),
}); });
const PlatformSchema = z.object({ const PlatformSchema = z.object({
@@ -116,7 +114,14 @@ export const PlacementForm = ({ id, type }: PlacementFormProps) => {
mysqlId: id || "", mysqlId: id || "",
mariadbId: id || "", mariadbId: id || "",
mongoId: id || "", mongoId: id || "",
placementSwarm: hasAnyValue ? formData : null, placementSwarm: hasAnyValue
? {
...formData,
Preferences: formData.Preferences?.map((p) => ({
Spread: { SpreadDescriptor: p.SpreadDescriptor },
})),
}
: null,
}); });
toast.success("Placement updated successfully"); toast.success("Placement updated successfully");

View File

@@ -54,6 +54,7 @@ const BitbucketProviderSchema = z.object({
.object({ .object({
repo: z.string().min(1, "Repo is required"), repo: z.string().min(1, "Repo is required"),
owner: z.string().min(1, "Owner is required"), owner: z.string().min(1, "Owner is required"),
slug: z.string().optional(),
}) })
.required(), .required(),
branch: z.string().min(1, "Branch is required"), branch: z.string().min(1, "Branch is required"),
@@ -82,6 +83,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
repository: { repository: {
owner: "", owner: "",
repo: "", repo: "",
slug: "",
}, },
bitbucketId: "", bitbucketId: "",
branch: "", branch: "",
@@ -114,11 +116,14 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
} = api.bitbucket.getBitbucketBranches.useQuery( } = api.bitbucket.getBitbucketBranches.useQuery(
{ {
owner: repository?.owner, owner: repository?.owner,
repo: repository?.repo, repo: repository?.slug || repository?.repo || "",
bitbucketId, bitbucketId,
}, },
{ {
enabled: !!repository?.owner && !!repository?.repo && !!bitbucketId, enabled:
!!repository?.owner &&
!!(repository?.slug || repository?.repo) &&
!!bitbucketId,
}, },
); );
@@ -129,6 +134,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
repository: { repository: {
repo: data.bitbucketRepository || "", repo: data.bitbucketRepository || "",
owner: data.bitbucketOwner || "", owner: data.bitbucketOwner || "",
slug: data.bitbucketRepositorySlug || "",
}, },
buildPath: data.bitbucketBuildPath || "/", buildPath: data.bitbucketBuildPath || "/",
bitbucketId: data.bitbucketId || "", bitbucketId: data.bitbucketId || "",
@@ -142,6 +148,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
await mutateAsync({ await mutateAsync({
bitbucketBranch: data.branch, bitbucketBranch: data.branch,
bitbucketRepository: data.repository.repo, bitbucketRepository: data.repository.repo,
bitbucketRepositorySlug: data.repository.slug || data.repository.repo,
bitbucketOwner: data.repository.owner, bitbucketOwner: data.repository.owner,
bitbucketBuildPath: data.buildPath, bitbucketBuildPath: data.buildPath,
bitbucketId: data.bitbucketId, bitbucketId: data.bitbucketId,
@@ -181,6 +188,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
form.setValue("repository", { form.setValue("repository", {
owner: "", owner: "",
repo: "", repo: "",
slug: "",
}); });
form.setValue("branch", ""); form.setValue("branch", "");
}} }}
@@ -217,7 +225,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
<FormLabel>Repository</FormLabel> <FormLabel>Repository</FormLabel>
{field.value.owner && field.value.repo && ( {field.value.owner && field.value.repo && (
<Link <Link
href={`https://bitbucket.org/${field.value.owner}/${field.value.repo}`} href={`https://bitbucket.org/${field.value.owner}/${field.value.slug || field.value.repo}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary" className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary"
@@ -271,6 +279,7 @@ export const SaveBitbucketProvider = ({ applicationId }: Props) => {
form.setValue("repository", { form.setValue("repository", {
owner: repo.owner.username as string, owner: repo.owner.username as string,
repo: repo.name, repo: repo.name,
slug: repo.slug,
}); });
form.setValue("branch", ""); form.setValue("branch", "");
}} }}

View File

@@ -54,6 +54,7 @@ const BitbucketProviderSchema = z.object({
.object({ .object({
repo: z.string().min(1, "Repo is required"), repo: z.string().min(1, "Repo is required"),
owner: z.string().min(1, "Owner is required"), owner: z.string().min(1, "Owner is required"),
slug: z.string().optional(),
}) })
.required(), .required(),
branch: z.string().min(1, "Branch is required"), branch: z.string().min(1, "Branch is required"),
@@ -82,6 +83,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
repository: { repository: {
owner: "", owner: "",
repo: "", repo: "",
slug: "",
}, },
bitbucketId: "", bitbucketId: "",
branch: "", branch: "",
@@ -114,11 +116,14 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
} = api.bitbucket.getBitbucketBranches.useQuery( } = api.bitbucket.getBitbucketBranches.useQuery(
{ {
owner: repository?.owner, owner: repository?.owner,
repo: repository?.repo, repo: repository?.slug || repository?.repo || "",
bitbucketId, bitbucketId,
}, },
{ {
enabled: !!repository?.owner && !!repository?.repo && !!bitbucketId, enabled:
!!repository?.owner &&
!!(repository?.slug || repository?.repo) &&
!!bitbucketId,
}, },
); );
@@ -129,6 +134,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
repository: { repository: {
repo: data.bitbucketRepository || "", repo: data.bitbucketRepository || "",
owner: data.bitbucketOwner || "", owner: data.bitbucketOwner || "",
slug: data.bitbucketRepositorySlug || "",
}, },
composePath: data.composePath, composePath: data.composePath,
bitbucketId: data.bitbucketId || "", bitbucketId: data.bitbucketId || "",
@@ -142,6 +148,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
await mutateAsync({ await mutateAsync({
bitbucketBranch: data.branch, bitbucketBranch: data.branch,
bitbucketRepository: data.repository.repo, bitbucketRepository: data.repository.repo,
bitbucketRepositorySlug: data.repository.slug || data.repository.repo,
bitbucketOwner: data.repository.owner, bitbucketOwner: data.repository.owner,
bitbucketId: data.bitbucketId, bitbucketId: data.bitbucketId,
composePath: data.composePath, composePath: data.composePath,
@@ -183,6 +190,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
form.setValue("repository", { form.setValue("repository", {
owner: "", owner: "",
repo: "", repo: "",
slug: "",
}); });
form.setValue("branch", ""); form.setValue("branch", "");
}} }}
@@ -219,7 +227,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
<FormLabel>Repository</FormLabel> <FormLabel>Repository</FormLabel>
{field.value.owner && field.value.repo && ( {field.value.owner && field.value.repo && (
<Link <Link
href={`https://bitbucket.org/${field.value.owner}/${field.value.repo}`} href={`https://bitbucket.org/${field.value.owner}/${field.value.slug || field.value.repo}`}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary" className="flex items-center gap-1 text-sm text-muted-foreground hover:text-primary"
@@ -273,6 +281,7 @@ export const SaveBitbucketProviderCompose = ({ composeId }: Props) => {
form.setValue("repository", { form.setValue("repository", {
owner: repo.owner.username as string, owner: repo.owner.username as string,
repo: repo.name, repo: repo.name,
slug: repo.slug,
}); });
form.setValue("branch", ""); form.setValue("branch", "");
}} }}

View File

@@ -129,7 +129,7 @@ export const ShowCustomCommand = ({ id, type }: Props) => {
<FormItem> <FormItem>
<FormLabel>Docker Image</FormLabel> <FormLabel>Docker Image</FormLabel>
<FormControl> <FormControl>
<Input placeholder="postgres:15" {...field} /> <Input placeholder="postgres:18" {...field} />
</FormControl> </FormControl>
<FormMessage /> <FormMessage />

View File

@@ -58,7 +58,7 @@ const dockerImageDefaultPlaceholder: Record<DbType, string> = {
mongo: "mongo:7", mongo: "mongo:7",
mariadb: "mariadb:11", mariadb: "mariadb:11",
mysql: "mysql:8", mysql: "mysql:8",
postgres: "postgres:15", postgres: "postgres:18",
redis: "redis:7", redis: "redis:7",
}; };

View File

@@ -15,6 +15,7 @@ import {
GotifyIcon, GotifyIcon,
LarkIcon, LarkIcon,
NtfyIcon, NtfyIcon,
PushoverIcon,
SlackIcon, SlackIcon,
TelegramIcon, TelegramIcon,
} from "@/components/icons/notification-icons"; } from "@/components/icons/notification-icons";
@@ -114,6 +115,16 @@ export const notificationSchema = z.discriminatedUnion("type", [
priority: z.number().min(1).max(5).default(3), priority: z.number().min(1).max(5).default(3),
}) })
.merge(notificationBaseSchema), .merge(notificationBaseSchema),
z
.object({
type: z.literal("pushover"),
userKey: z.string().min(1, { message: "User Key is required" }),
apiToken: z.string().min(1, { message: "API Token is required" }),
priority: z.number().min(-2).max(2).default(0),
retry: z.number().min(30).nullish(),
expire: z.number().min(1).max(10800).nullish(),
})
.merge(notificationBaseSchema),
z z
.object({ .object({
type: z.literal("custom"), type: z.literal("custom"),
@@ -166,6 +177,10 @@ export const notificationsMap = {
icon: <NtfyIcon />, icon: <NtfyIcon />,
label: "ntfy", label: "ntfy",
}, },
pushover: {
icon: <PushoverIcon />,
label: "Pushover",
},
custom: { custom: {
icon: <PenBoxIcon size={29} className="text-muted-foreground" />, icon: <PenBoxIcon size={29} className="text-muted-foreground" />,
label: "Custom", label: "Custom",
@@ -209,6 +224,9 @@ export const HandleNotifications = ({ notificationId }: Props) => {
const { mutateAsync: testCustomConnection, isLoading: isLoadingCustom } = const { mutateAsync: testCustomConnection, isLoading: isLoadingCustom } =
api.notification.testCustomConnection.useMutation(); api.notification.testCustomConnection.useMutation();
const { mutateAsync: testPushoverConnection, isLoading: isLoadingPushover } =
api.notification.testPushoverConnection.useMutation();
const customMutation = notificationId const customMutation = notificationId
? api.notification.updateCustom.useMutation() ? api.notification.updateCustom.useMutation()
: api.notification.createCustom.useMutation(); : api.notification.createCustom.useMutation();
@@ -233,6 +251,9 @@ export const HandleNotifications = ({ notificationId }: Props) => {
const larkMutation = notificationId const larkMutation = notificationId
? api.notification.updateLark.useMutation() ? api.notification.updateLark.useMutation()
: api.notification.createLark.useMutation(); : api.notification.createLark.useMutation();
const pushoverMutation = notificationId
? api.notification.updatePushover.useMutation()
: api.notification.createPushover.useMutation();
const form = useForm<NotificationSchema>({ const form = useForm<NotificationSchema>({
defaultValues: { defaultValues: {
@@ -393,6 +414,23 @@ export const HandleNotifications = ({ notificationId }: Props) => {
dockerCleanup: notification.dockerCleanup, dockerCleanup: notification.dockerCleanup,
serverThreshold: notification.serverThreshold, serverThreshold: notification.serverThreshold,
}); });
} else if (notification.notificationType === "pushover") {
form.reset({
appBuildError: notification.appBuildError,
appDeploy: notification.appDeploy,
dokployRestart: notification.dokployRestart,
databaseBackup: notification.databaseBackup,
volumeBackup: notification.volumeBackup,
type: notification.notificationType,
userKey: notification.pushover?.userKey,
apiToken: notification.pushover?.apiToken,
priority: notification.pushover?.priority,
retry: notification.pushover?.retry ?? undefined,
expire: notification.pushover?.expire ?? undefined,
name: notification.name,
dockerCleanup: notification.dockerCleanup,
serverThreshold: notification.serverThreshold,
});
} }
} else { } else {
form.reset(); form.reset();
@@ -408,6 +446,7 @@ export const HandleNotifications = ({ notificationId }: Props) => {
ntfy: ntfyMutation, ntfy: ntfyMutation,
lark: larkMutation, lark: larkMutation,
custom: customMutation, custom: customMutation,
pushover: pushoverMutation,
}; };
const onSubmit = async (data: NotificationSchema) => { const onSubmit = async (data: NotificationSchema) => {
@@ -559,6 +598,28 @@ export const HandleNotifications = ({ notificationId }: Props) => {
notificationId: notificationId || "", notificationId: notificationId || "",
customId: notification?.customId || "", customId: notification?.customId || "",
}); });
} else if (data.type === "pushover") {
if (data.priority === 2 && (data.retry == null || data.expire == null)) {
toast.error("Retry and expire are required for emergency priority (2)");
return;
}
promise = pushoverMutation.mutateAsync({
appBuildError: appBuildError,
appDeploy: appDeploy,
dokployRestart: dokployRestart,
databaseBackup: databaseBackup,
volumeBackup: volumeBackup,
userKey: data.userKey,
apiToken: data.apiToken,
priority: data.priority,
retry: data.priority === 2 ? data.retry : undefined,
expire: data.priority === 2 ? data.expire : undefined,
name: data.name,
dockerCleanup: dockerCleanup,
serverThreshold: serverThreshold,
notificationId: notificationId || "",
pushoverId: notification?.pushoverId || "",
});
} }
if (promise) { if (promise) {
@@ -1255,6 +1316,147 @@ export const HandleNotifications = ({ notificationId }: Props) => {
/> />
</> </>
)} )}
{type === "pushover" && (
<>
<FormField
control={form.control}
name="userKey"
render={({ field }) => (
<FormItem>
<FormLabel>User Key</FormLabel>
<FormControl>
<Input placeholder="ub3de9kl2q..." {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="apiToken"
render={({ field }) => (
<FormItem>
<FormLabel>API Token</FormLabel>
<FormControl>
<Input placeholder="a3d9k2q7m4..." {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="priority"
defaultValue={0}
render={({ field }) => (
<FormItem className="w-full">
<FormLabel>Priority</FormLabel>
<FormControl>
<Input
placeholder="0"
value={field.value ?? 0}
onChange={(e) => {
const value = e.target.value;
if (value === "" || value === "-") {
field.onChange(0);
} else {
const priority = Number.parseInt(value);
if (
!Number.isNaN(priority) &&
priority >= -2 &&
priority <= 2
) {
field.onChange(priority);
}
}
}}
type="number"
min={-2}
max={2}
/>
</FormControl>
<FormDescription>
Message priority (-2 to 2, default: 0, emergency: 2)
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
{form.watch("priority") === 2 && (
<>
<FormField
control={form.control}
name="retry"
render={({ field }) => (
<FormItem className="w-full">
<FormLabel>Retry (seconds)</FormLabel>
<FormControl>
<Input
placeholder="30"
{...field}
value={field.value ?? ""}
onChange={(e) => {
const value = e.target.value;
if (value === "") {
field.onChange(undefined);
} else {
const retry = Number.parseInt(value);
if (!Number.isNaN(retry)) {
field.onChange(retry);
}
}
}}
type="number"
min={30}
/>
</FormControl>
<FormDescription>
How often (in seconds) to retry. Minimum 30
seconds.
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="expire"
render={({ field }) => (
<FormItem className="w-full">
<FormLabel>Expire (seconds)</FormLabel>
<FormControl>
<Input
placeholder="3600"
{...field}
value={field.value ?? ""}
onChange={(e) => {
const value = e.target.value;
if (value === "") {
field.onChange(undefined);
} else {
const expire = Number.parseInt(value);
if (!Number.isNaN(expire)) {
field.onChange(expire);
}
}
}}
type="number"
min={1}
max={10800}
/>
</FormControl>
<FormDescription>
How long to keep retrying (max 10800 seconds / 3
hours).
</FormDescription>
<FormMessage />
</FormItem>
)}
/>
</>
)}
</>
)}
</div> </div>
</div> </div>
<div className="flex flex-col gap-4"> <div className="flex flex-col gap-4">
@@ -1428,7 +1630,8 @@ export const HandleNotifications = ({ notificationId }: Props) => {
isLoadingGotify || isLoadingGotify ||
isLoadingNtfy || isLoadingNtfy ||
isLoadingLark || isLoadingLark ||
isLoadingCustom isLoadingCustom ||
isLoadingPushover
} }
variant="secondary" variant="secondary"
type="button" type="button"
@@ -1497,6 +1700,22 @@ export const HandleNotifications = ({ notificationId }: Props) => {
endpoint: data.endpoint, endpoint: data.endpoint,
headers: headersRecord, headers: headersRecord,
}); });
} else if (data.type === "pushover") {
if (
data.priority === 2 &&
(data.retry == null || data.expire == null)
) {
throw new Error(
"Retry and expire are required for emergency priority (2)",
);
}
await testPushoverConnection({
userKey: data.userKey,
apiToken: data.apiToken,
priority: data.priority,
retry: data.priority === 2 ? data.retry : undefined,
expire: data.priority === 2 ? data.expire : undefined,
});
} }
toast.success("Connection Success"); toast.success("Connection Success");
} catch (error) { } catch (error) {

View File

@@ -41,7 +41,7 @@ const profileSchema = z.object({
password: z.string().nullable(), password: z.string().nullable(),
currentPassword: z.string().nullable(), currentPassword: z.string().nullable(),
image: z.string().optional(), image: z.string().optional(),
name: z.string().optional(), firstName: z.string().optional(),
lastName: z.string().optional(), lastName: z.string().optional(),
allowImpersonation: z.boolean().optional().default(false), allowImpersonation: z.boolean().optional().default(false),
}); });
@@ -91,7 +91,7 @@ export const ProfileForm = () => {
image: data?.user?.image || "", image: data?.user?.image || "",
currentPassword: "", currentPassword: "",
allowImpersonation: data?.user?.allowImpersonation || false, allowImpersonation: data?.user?.allowImpersonation || false,
name: data?.user?.firstName || "", firstName: data?.user?.firstName || "",
lastName: data?.user?.lastName || "", lastName: data?.user?.lastName || "",
}, },
resolver: zodResolver(profileSchema), resolver: zodResolver(profileSchema),
@@ -106,7 +106,7 @@ export const ProfileForm = () => {
image: data?.user?.image || "", image: data?.user?.image || "",
currentPassword: form.getValues("currentPassword") || "", currentPassword: form.getValues("currentPassword") || "",
allowImpersonation: data?.user?.allowImpersonation, allowImpersonation: data?.user?.allowImpersonation,
name: data?.user?.firstName || "", firstName: data?.user?.firstName || "",
lastName: data?.user?.lastName || "", lastName: data?.user?.lastName || "",
}, },
{ {
@@ -131,7 +131,7 @@ export const ProfileForm = () => {
image: values.image, image: values.image,
currentPassword: values.currentPassword || undefined, currentPassword: values.currentPassword || undefined,
allowImpersonation: values.allowImpersonation, allowImpersonation: values.allowImpersonation,
name: values.name || undefined, firstName: values.firstName || undefined,
lastName: values.lastName || undefined, lastName: values.lastName || undefined,
}); });
await refetch(); await refetch();
@@ -141,7 +141,7 @@ export const ProfileForm = () => {
password: "", password: "",
image: values.image, image: values.image,
currentPassword: "", currentPassword: "",
name: values.name || "", firstName: values.firstName || "",
lastName: values.lastName || "", lastName: values.lastName || "",
}); });
} catch (error) { } catch (error) {
@@ -184,7 +184,7 @@ export const ProfileForm = () => {
<div className="space-y-4"> <div className="space-y-4">
<FormField <FormField
control={form.control} control={form.control}
name="name" name="firstName"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>First Name</FormLabel> <FormLabel>First Name</FormLabel>

View File

@@ -231,3 +231,29 @@ export const NtfyIcon = ({ className }: Props) => {
</svg> </svg>
); );
}; };
export const PushoverIcon = ({ className }: Props) => {
return (
<svg
viewBox="0 0 600 600"
className={cn("size-8", className)}
xmlns="http://www.w3.org/2000/svg"
>
<g stroke="none" strokeWidth="1">
<ellipse
style={{ fillRule: "evenodd" }}
fill="#249DF1"
transform="matrix(-0.674571, 0.73821, -0.73821, -0.674571, 556.833239, 241.613465)"
cx="216.308"
cy="152.076"
rx="296.855"
ry="296.855"
/>
<path
fill="#FFFFFF"
d="M 280.949 172.514 L 355.429 162.714 L 282.909 326.374 L 282.909 326.374 C 295.649 325.394 308.142 321.067 320.389 313.394 L 320.389 313.394 L 320.389 313.394 C 332.642 305.714 343.916 296.077 354.209 284.484 L 354.209 284.484 L 354.209 284.484 C 364.496 272.884 373.396 259.981 380.909 245.774 L 380.909 245.774 L 380.909 245.774 C 388.422 231.561 393.812 217.594 397.079 203.874 L 397.079 203.874 L 397.079 203.874 C 399.039 195.381 399.939 187.214 399.779 179.374 L 399.779 179.374 L 399.779 179.374 C 399.612 171.534 397.569 164.674 393.649 158.794 L 393.649 158.794 L 393.649 158.794 C 389.729 152.914 383.766 148.177 375.759 144.584 L 375.759 144.584 L 375.759 144.584 C 367.759 140.991 356.899 139.194 343.179 139.194 L 343.179 139.194 L 343.179 139.194 C 327.172 139.194 311.409 141.807 295.889 147.034 L 295.889 147.034 L 295.889 147.034 C 280.376 152.261 266.002 159.857 252.769 169.824 L 252.769 169.824 L 252.769 169.824 C 239.542 179.784 228.029 192.197 218.229 207.064 L 218.229 207.064 L 218.229 207.064 C 208.429 221.924 201.406 238.827 197.159 257.774 L 197.159 257.774 L 197.159 257.774 C 195.526 263.981 194.546 268.961 194.219 272.714 L 194.219 272.714 L 194.219 272.714 C 193.892 276.474 193.812 279.577 193.979 282.024 L 193.979 282.024 L 193.979 282.024 C 194.139 284.477 194.462 286.357 194.949 287.664 L 194.949 287.664 L 194.949 287.664 C 195.442 288.971 195.852 290.277 196.179 291.584 L 196.179 291.584 L 196.179 291.584 C 179.519 291.584 167.349 288.234 159.669 281.534 L 159.669 281.534 L 159.669 281.534 C 151.996 274.841 150.119 263.164 154.039 246.504 L 154.039 246.504 L 154.039 246.504 C 157.959 229.191 166.862 212.694 180.749 197.014 L 180.749 197.014 L 180.749 197.014 C 194.629 181.334 211.122 167.531 230.229 155.604 L 230.229 155.604 L 230.229 155.604 C 249.342 143.684 270.249 134.214 292.949 127.194 L 292.949 127.194 L 292.949 127.194 C 315.656 120.167 337.789 116.654 359.349 116.654 L 359.349 116.654 L 359.349 116.654 C 378.296 116.654 394.219 119.347 407.119 124.734 L 407.119 124.734 L 407.119 124.734 C 420.026 130.127 430.072 137.234 437.259 146.054 L 437.259 146.054 L 437.259 146.054 C 444.446 154.874 448.936 165.164 450.729 176.924 L 450.729 176.924 L 450.729 176.924 C 452.529 188.684 451.959 200.934 449.019 213.674 L 449.019 213.674 L 449.019 213.674 C 445.426 229.027 438.646 244.464 428.679 259.984 L 428.679 259.984 L 428.679 259.984 C 418.719 275.497 406.226 289.544 391.199 302.124 L 391.199 302.124 L 391.199 302.124 C 376.172 314.697 358.939 324.904 339.499 332.744 L 339.499 332.744 L 339.499 332.744 C 320.066 340.584 299.406 344.504 277.519 344.504 L 277.519 344.504 L 275.069 344.504 L 212.839 484.154 L 142.279 484.154 L 280.949 172.514 Z"
/>
</g>
</svg>
);
};

View File

@@ -0,0 +1,12 @@
ALTER TYPE "public"."notificationType" ADD VALUE 'pushover' BEFORE 'custom';--> statement-breakpoint
CREATE TABLE "pushover" (
"pushoverId" text PRIMARY KEY NOT NULL,
"userKey" text NOT NULL,
"apiToken" text NOT NULL,
"priority" integer DEFAULT 0 NOT NULL,
"retry" integer,
"expire" integer
);
--> statement-breakpoint
ALTER TABLE "notification" ADD COLUMN "pushoverId" text;--> statement-breakpoint
ALTER TABLE "notification" ADD CONSTRAINT "notification_pushoverId_pushover_pushoverId_fk" FOREIGN KEY ("pushoverId") REFERENCES "public"."pushover"("pushoverId") ON DELETE cascade ON UPDATE no action;

View File

@@ -0,0 +1,2 @@
ALTER TABLE "application" ADD COLUMN "bitbucketRepositorySlug" text;--> statement-breakpoint
ALTER TABLE "compose" ADD COLUMN "bitbucketRepositorySlug" text;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -946,6 +946,20 @@
"when": 1767871040249, "when": 1767871040249,
"tag": "0134_strong_hercules", "tag": "0134_strong_hercules",
"breakpoints": true "breakpoints": true
},
{
"idx": 135,
"version": "7",
"when": 1768271617042,
"tag": "0135_illegal_magik",
"breakpoints": true
},
{
"idx": 136,
"version": "7",
"when": 1769580434296,
"tag": "0136_tidy_puff_adder",
"breakpoints": true
} }
] ]
} }

View File

@@ -1,6 +1,6 @@
{ {
"name": "dokploy", "name": "dokploy",
"version": "v0.26.6", "version": "v0.26.7",
"private": true, "private": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"type": "module", "type": "module",

View File

@@ -195,7 +195,9 @@ export default async function handler(
const commitedPaths = await extractCommitedPaths( const commitedPaths = await extractCommitedPaths(
req.body, req.body,
application.bitbucket, application.bitbucket,
application.bitbucketRepository || "", application.bitbucketRepositorySlug ||
application.bitbucketRepository ||
"",
); );
const shouldDeployPaths = shouldDeploy( const shouldDeployPaths = shouldDeploy(

View File

@@ -100,7 +100,9 @@ export default async function handler(
const commitedPaths = await extractCommitedPaths( const commitedPaths = await extractCommitedPaths(
req.body, req.body,
composeResult.bitbucket, composeResult.bitbucket,
composeResult.bitbucketRepository || "", composeResult.bitbucketRepositorySlug ||
composeResult.bitbucketRepository ||
"",
); );
const shouldDeployPaths = shouldDeploy( const shouldDeployPaths = shouldDeploy(

View File

@@ -469,6 +469,7 @@ export const applicationRouter = createTRPCRouter({
} }
await updateApplication(input.applicationId, { await updateApplication(input.applicationId, {
bitbucketRepository: input.bitbucketRepository, bitbucketRepository: input.bitbucketRepository,
bitbucketRepositorySlug: input.bitbucketRepositorySlug,
bitbucketOwner: input.bitbucketOwner, bitbucketOwner: input.bitbucketOwner,
bitbucketBranch: input.bitbucketBranch, bitbucketBranch: input.bitbucketBranch,
bitbucketBuildPath: input.bitbucketBuildPath, bitbucketBuildPath: input.bitbucketBuildPath,

View File

@@ -5,6 +5,7 @@ import {
createGotifyNotification, createGotifyNotification,
createLarkNotification, createLarkNotification,
createNtfyNotification, createNtfyNotification,
createPushoverNotification,
createSlackNotification, createSlackNotification,
createTelegramNotification, createTelegramNotification,
findNotificationById, findNotificationById,
@@ -17,6 +18,7 @@ import {
sendGotifyNotification, sendGotifyNotification,
sendLarkNotification, sendLarkNotification,
sendNtfyNotification, sendNtfyNotification,
sendPushoverNotification,
sendServerThresholdNotifications, sendServerThresholdNotifications,
sendSlackNotification, sendSlackNotification,
sendTelegramNotification, sendTelegramNotification,
@@ -26,6 +28,7 @@ import {
updateGotifyNotification, updateGotifyNotification,
updateLarkNotification, updateLarkNotification,
updateNtfyNotification, updateNtfyNotification,
updatePushoverNotification,
updateSlackNotification, updateSlackNotification,
updateTelegramNotification, updateTelegramNotification,
} from "@dokploy/server"; } from "@dokploy/server";
@@ -46,6 +49,7 @@ import {
apiCreateGotify, apiCreateGotify,
apiCreateLark, apiCreateLark,
apiCreateNtfy, apiCreateNtfy,
apiCreatePushover,
apiCreateSlack, apiCreateSlack,
apiCreateTelegram, apiCreateTelegram,
apiFindOneNotification, apiFindOneNotification,
@@ -55,6 +59,7 @@ import {
apiTestGotifyConnection, apiTestGotifyConnection,
apiTestLarkConnection, apiTestLarkConnection,
apiTestNtfyConnection, apiTestNtfyConnection,
apiTestPushoverConnection,
apiTestSlackConnection, apiTestSlackConnection,
apiTestTelegramConnection, apiTestTelegramConnection,
apiUpdateCustom, apiUpdateCustom,
@@ -63,6 +68,7 @@ import {
apiUpdateGotify, apiUpdateGotify,
apiUpdateLark, apiUpdateLark,
apiUpdateNtfy, apiUpdateNtfy,
apiUpdatePushover,
apiUpdateSlack, apiUpdateSlack,
apiUpdateTelegram, apiUpdateTelegram,
notifications, notifications,
@@ -342,6 +348,7 @@ export const notificationRouter = createTRPCRouter({
ntfy: true, ntfy: true,
custom: true, custom: true,
lark: true, lark: true,
pushover: true,
}, },
orderBy: desc(notifications.createdAt), orderBy: desc(notifications.createdAt),
where: eq(notifications.organizationId, ctx.session.activeOrganizationId), where: eq(notifications.organizationId, ctx.session.activeOrganizationId),
@@ -634,6 +641,62 @@ export const notificationRouter = createTRPCRouter({
}); });
} }
}), }),
createPushover: adminProcedure
.input(apiCreatePushover)
.mutation(async ({ input, ctx }) => {
try {
return await createPushoverNotification(
input,
ctx.session.activeOrganizationId,
);
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error creating the notification",
cause: error,
});
}
}),
updatePushover: adminProcedure
.input(apiUpdatePushover)
.mutation(async ({ input, ctx }) => {
try {
const notification = await findNotificationById(input.notificationId);
if (
IS_CLOUD &&
notification.organizationId !== ctx.session.activeOrganizationId
) {
throw new TRPCError({
code: "UNAUTHORIZED",
message: "You are not authorized to update this notification",
});
}
return await updatePushoverNotification({
...input,
organizationId: ctx.session.activeOrganizationId,
});
} catch (error) {
throw error;
}
}),
testPushoverConnection: adminProcedure
.input(apiTestPushoverConnection)
.mutation(async ({ input }) => {
try {
await sendPushoverNotification(
input,
"Test Notification",
"Hi, From Dokploy 👋",
);
return true;
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error testing the notification",
cause: error,
});
}
}),
getEmailProviders: adminProcedure.query(async ({ ctx }) => { getEmailProviders: adminProcedure.query(async ({ ctx }) => {
return await db.query.notifications.findMany({ return await db.query.notifications.findMany({
where: eq(notifications.organizationId, ctx.session.activeOrganizationId), where: eq(notifications.organizationId, ctx.session.activeOrganizationId),

View File

@@ -22,7 +22,7 @@ import {
await initializeNetwork(); await initializeNetwork();
createDefaultTraefikConfig(); createDefaultTraefikConfig();
createDefaultServerTraefikConfig(); createDefaultServerTraefikConfig();
await execAsync("docker pull traefik:v3.6.1"); await execAsync("docker pull traefik:v3.6.7");
await initializeStandaloneTraefik(); await initializeStandaloneTraefik();
await initializeRedis(); await initializeRedis();
await initializePostgres(); await initializePostgres();

View File

@@ -1,6 +1,7 @@
{ {
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json", "$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
"files": { "files": {
"ignoreUnknown": true,
"includes": [ "includes": [
"**", "**",
"!**/.docker", "!**/.docker",

View File

@@ -47,7 +47,7 @@ import {
UpdateConfigSwarmSchema, UpdateConfigSwarmSchema,
} from "./shared"; } from "./shared";
import { sshKeys } from "./ssh-key"; import { sshKeys } from "./ssh-key";
import { generateAppName } from "./utils"; import { APP_NAME_MESSAGE, APP_NAME_REGEX, generateAppName } from "./utils";
export const sourceType = pgEnum("sourceType", [ export const sourceType = pgEnum("sourceType", [
"docker", "docker",
"git", "git",
@@ -136,6 +136,7 @@ export const applications = pgTable("application", {
giteaBuildPath: text("giteaBuildPath").default("/"), giteaBuildPath: text("giteaBuildPath").default("/"),
// Bitbucket // Bitbucket
bitbucketRepository: text("bitbucketRepository"), bitbucketRepository: text("bitbucketRepository"),
bitbucketRepositorySlug: text("bitbucketRepositorySlug"),
bitbucketOwner: text("bitbucketOwner"), bitbucketOwner: text("bitbucketOwner"),
bitbucketBranch: text("bitbucketBranch"), bitbucketBranch: text("bitbucketBranch"),
bitbucketBuildPath: text("bitbucketBuildPath").default("/"), bitbucketBuildPath: text("bitbucketBuildPath").default("/"),
@@ -286,7 +287,12 @@ export const applicationsRelations = relations(
); );
const createSchema = createInsertSchema(applications, { const createSchema = createInsertSchema(applications, {
appName: z.string(), appName: z
.string()
.min(1)
.max(63)
.regex(APP_NAME_REGEX, APP_NAME_MESSAGE)
.optional(),
createdAt: z.string(), createdAt: z.string(),
applicationId: z.string(), applicationId: z.string(),
autoDeploy: z.boolean(), autoDeploy: z.boolean(),
@@ -451,6 +457,7 @@ export const apiSaveBitbucketProvider = createSchema
bitbucketBuildPath: true, bitbucketBuildPath: true,
bitbucketOwner: true, bitbucketOwner: true,
bitbucketRepository: true, bitbucketRepository: true,
bitbucketRepositorySlug: true,
bitbucketId: true, bitbucketId: true,
applicationId: true, applicationId: true,
watchPaths: true, watchPaths: true,

View File

@@ -16,7 +16,7 @@ import { schedules } from "./schedule";
import { server } from "./server"; import { server } from "./server";
import { applicationStatus, triggerType } from "./shared"; import { applicationStatus, triggerType } from "./shared";
import { sshKeys } from "./ssh-key"; import { sshKeys } from "./ssh-key";
import { generateAppName } from "./utils"; import { APP_NAME_MESSAGE, APP_NAME_REGEX, generateAppName } from "./utils";
export const sourceTypeCompose = pgEnum("sourceTypeCompose", [ export const sourceTypeCompose = pgEnum("sourceTypeCompose", [
"git", "git",
"github", "github",
@@ -56,6 +56,7 @@ export const compose = pgTable("compose", {
gitlabPathNamespace: text("gitlabPathNamespace"), gitlabPathNamespace: text("gitlabPathNamespace"),
// Bitbucket // Bitbucket
bitbucketRepository: text("bitbucketRepository"), bitbucketRepository: text("bitbucketRepository"),
bitbucketRepositorySlug: text("bitbucketRepositorySlug"),
bitbucketOwner: text("bitbucketOwner"), bitbucketOwner: text("bitbucketOwner"),
bitbucketBranch: text("bitbucketBranch"), bitbucketBranch: text("bitbucketBranch"),
// Gitea // Gitea
@@ -146,6 +147,12 @@ export const composeRelations = relations(compose, ({ one, many }) => ({
const createSchema = createInsertSchema(compose, { const createSchema = createInsertSchema(compose, {
name: z.string().min(1), name: z.string().min(1),
appName: z
.string()
.min(1)
.max(63)
.regex(APP_NAME_REGEX, APP_NAME_MESSAGE)
.optional(),
description: z.string(), description: z.string(),
env: z.string().optional(), env: z.string().optional(),
composeFile: z.string().optional(), composeFile: z.string().optional(),

View File

@@ -26,7 +26,7 @@ import {
type UpdateConfigSwarm, type UpdateConfigSwarm,
UpdateConfigSwarmSchema, UpdateConfigSwarmSchema,
} from "./shared"; } from "./shared";
import { generateAppName } from "./utils"; import { APP_NAME_MESSAGE, APP_NAME_REGEX, generateAppName } from "./utils";
export const mariadb = pgTable("mariadb", { export const mariadb = pgTable("mariadb", {
mariadbId: text("mariadbId") mariadbId: text("mariadbId")
@@ -96,7 +96,12 @@ export const mariadbRelations = relations(mariadb, ({ one, many }) => ({
const createSchema = createInsertSchema(mariadb, { const createSchema = createInsertSchema(mariadb, {
mariadbId: z.string(), mariadbId: z.string(),
name: z.string().min(1), name: z.string().min(1),
appName: z.string().min(1), appName: z
.string()
.min(1)
.max(63)
.regex(APP_NAME_REGEX, APP_NAME_MESSAGE)
.optional(),
createdAt: z.string(), createdAt: z.string(),
databaseName: z.string().min(1), databaseName: z.string().min(1),
databaseUser: z.string().min(1), databaseUser: z.string().min(1),
@@ -138,20 +143,18 @@ const createSchema = createInsertSchema(mariadb, {
endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(), endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
}); });
export const apiCreateMariaDB = createSchema export const apiCreateMariaDB = createSchema.pick({
.pick({ name: true,
name: true, appName: true,
appName: true, dockerImage: true,
dockerImage: true, databaseRootPassword: true,
databaseRootPassword: true, environmentId: true,
environmentId: true, description: true,
description: true, databaseName: true,
databaseName: true, databaseUser: true,
databaseUser: true, databasePassword: true,
databasePassword: true, serverId: true,
serverId: true, });
})
.required();
export const apiFindOneMariaDB = createSchema export const apiFindOneMariaDB = createSchema
.pick({ .pick({

View File

@@ -33,7 +33,7 @@ import {
type UpdateConfigSwarm, type UpdateConfigSwarm,
UpdateConfigSwarmSchema, UpdateConfigSwarmSchema,
} from "./shared"; } from "./shared";
import { generateAppName } from "./utils"; import { APP_NAME_MESSAGE, APP_NAME_REGEX, generateAppName } from "./utils";
export const mongo = pgTable("mongo", { export const mongo = pgTable("mongo", {
mongoId: text("mongoId") mongoId: text("mongoId")
@@ -98,7 +98,12 @@ export const mongoRelations = relations(mongo, ({ one, many }) => ({
})); }));
const createSchema = createInsertSchema(mongo, { const createSchema = createInsertSchema(mongo, {
appName: z.string().min(1), appName: z
.string()
.min(1)
.max(63)
.regex(APP_NAME_REGEX, APP_NAME_MESSAGE)
.optional(),
createdAt: z.string(), createdAt: z.string(),
mongoId: z.string(), mongoId: z.string(),
name: z.string().min(1), name: z.string().min(1),
@@ -135,19 +140,17 @@ const createSchema = createInsertSchema(mongo, {
endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(), endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
}); });
export const apiCreateMongo = createSchema export const apiCreateMongo = createSchema.pick({
.pick({ name: true,
name: true, appName: true,
appName: true, dockerImage: true,
dockerImage: true, environmentId: true,
environmentId: true, description: true,
description: true, databaseUser: true,
databaseUser: true, databasePassword: true,
databasePassword: true, serverId: true,
serverId: true, replicaSets: true,
replicaSets: true, });
})
.required();
export const apiFindOneMongo = createSchema export const apiFindOneMongo = createSchema
.pick({ .pick({

View File

@@ -26,7 +26,7 @@ import {
type UpdateConfigSwarm, type UpdateConfigSwarm,
UpdateConfigSwarmSchema, UpdateConfigSwarmSchema,
} from "./shared"; } from "./shared";
import { generateAppName } from "./utils"; import { APP_NAME_MESSAGE, APP_NAME_REGEX, generateAppName } from "./utils";
export const mysql = pgTable("mysql", { export const mysql = pgTable("mysql", {
mysqlId: text("mysqlId") mysqlId: text("mysqlId")
@@ -93,7 +93,12 @@ export const mysqlRelations = relations(mysql, ({ one, many }) => ({
const createSchema = createInsertSchema(mysql, { const createSchema = createInsertSchema(mysql, {
mysqlId: z.string(), mysqlId: z.string(),
appName: z.string().min(1), appName: z
.string()
.min(1)
.max(63)
.regex(APP_NAME_REGEX, APP_NAME_MESSAGE)
.optional(),
createdAt: z.string(), createdAt: z.string(),
name: z.string().min(1), name: z.string().min(1),
databaseName: z.string().min(1), databaseName: z.string().min(1),
@@ -135,20 +140,18 @@ const createSchema = createInsertSchema(mysql, {
endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(), endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
}); });
export const apiCreateMySql = createSchema export const apiCreateMySql = createSchema.pick({
.pick({ name: true,
name: true, appName: true,
appName: true, dockerImage: true,
dockerImage: true, environmentId: true,
environmentId: true, description: true,
description: true, databaseName: true,
databaseName: true, databaseUser: true,
databaseUser: true, databasePassword: true,
databasePassword: true, databaseRootPassword: true,
databaseRootPassword: true, serverId: true,
serverId: true, });
})
.required();
export const apiFindOneMySql = createSchema export const apiFindOneMySql = createSchema
.pick({ .pick({

View File

@@ -19,6 +19,7 @@ export const notificationType = pgEnum("notificationType", [
"email", "email",
"gotify", "gotify",
"ntfy", "ntfy",
"pushover",
"custom", "custom",
"lark", "lark",
]); ]);
@@ -64,6 +65,9 @@ export const notifications = pgTable("notification", {
larkId: text("larkId").references(() => lark.larkId, { larkId: text("larkId").references(() => lark.larkId, {
onDelete: "cascade", onDelete: "cascade",
}), }),
pushoverId: text("pushoverId").references(() => pushover.pushoverId, {
onDelete: "cascade",
}),
organizationId: text("organizationId") organizationId: text("organizationId")
.notNull() .notNull()
.references(() => organization.id, { onDelete: "cascade" }), .references(() => organization.id, { onDelete: "cascade" }),
@@ -149,6 +153,18 @@ export const lark = pgTable("lark", {
webhookUrl: text("webhookUrl").notNull(), webhookUrl: text("webhookUrl").notNull(),
}); });
export const pushover = pgTable("pushover", {
pushoverId: text("pushoverId")
.notNull()
.primaryKey()
.$defaultFn(() => nanoid()),
userKey: text("userKey").notNull(),
apiToken: text("apiToken").notNull(),
priority: integer("priority").notNull().default(0),
retry: integer("retry"),
expire: integer("expire"),
});
export const notificationsRelations = relations(notifications, ({ one }) => ({ export const notificationsRelations = relations(notifications, ({ one }) => ({
slack: one(slack, { slack: one(slack, {
fields: [notifications.slackId], fields: [notifications.slackId],
@@ -182,6 +198,10 @@ export const notificationsRelations = relations(notifications, ({ one }) => ({
fields: [notifications.larkId], fields: [notifications.larkId],
references: [lark.larkId], references: [lark.larkId],
}), }),
pushover: one(pushover, {
fields: [notifications.pushoverId],
references: [pushover.pushoverId],
}),
organization: one(organization, { organization: one(organization, {
fields: [notifications.organizationId], fields: [notifications.organizationId],
references: [organization.id], references: [organization.id],
@@ -439,6 +459,69 @@ export const apiTestLarkConnection = apiCreateLark.pick({
webhookUrl: true, webhookUrl: true,
}); });
export const apiCreatePushover = notificationsSchema
.pick({
appBuildError: true,
databaseBackup: true,
volumeBackup: true,
dokployRestart: true,
name: true,
appDeploy: true,
dockerCleanup: true,
serverThreshold: true,
})
.extend({
userKey: z.string().min(1),
apiToken: z.string().min(1),
priority: z.number().min(-2).max(2).default(0),
retry: z.number().min(30).nullish(),
expire: z.number().min(1).max(10800).nullish(),
})
.refine(
(data) =>
data.priority !== 2 || (data.retry != null && data.expire != null),
{
message: "Retry and expire are required for emergency priority (2)",
path: ["retry"],
},
);
export const apiUpdatePushover = z.object({
notificationId: z.string().min(1),
pushoverId: z.string().min(1),
organizationId: z.string().optional(),
userKey: z.string().min(1).optional(),
apiToken: z.string().min(1).optional(),
priority: z.number().min(-2).max(2).optional(),
retry: z.number().min(30).nullish(),
expire: z.number().min(1).max(10800).nullish(),
appBuildError: z.boolean().optional(),
databaseBackup: z.boolean().optional(),
volumeBackup: z.boolean().optional(),
dokployRestart: z.boolean().optional(),
name: z.string().optional(),
appDeploy: z.boolean().optional(),
dockerCleanup: z.boolean().optional(),
serverThreshold: z.boolean().optional(),
});
export const apiTestPushoverConnection = z
.object({
userKey: z.string().min(1),
apiToken: z.string().min(1),
priority: z.number().min(-2).max(2),
retry: z.number().min(30).nullish(),
expire: z.number().min(1).max(10800).nullish(),
})
.refine(
(data) =>
data.priority !== 2 || (data.retry != null && data.expire != null),
{
message: "Retry and expire are required for emergency priority (2)",
path: ["retry"],
},
);
export const apiSendTest = notificationsSchema export const apiSendTest = notificationsSchema
.extend({ .extend({
botToken: z.string(), botToken: z.string(),

View File

@@ -26,7 +26,7 @@ import {
type UpdateConfigSwarm, type UpdateConfigSwarm,
UpdateConfigSwarmSchema, UpdateConfigSwarmSchema,
} from "./shared"; } from "./shared";
import { generateAppName } from "./utils"; import { APP_NAME_MESSAGE, APP_NAME_REGEX, generateAppName } from "./utils";
export const postgres = pgTable("postgres", { export const postgres = pgTable("postgres", {
postgresId: text("postgresId") postgresId: text("postgresId")
@@ -94,6 +94,12 @@ export const postgresRelations = relations(postgres, ({ one, many }) => ({
const createSchema = createInsertSchema(postgres, { const createSchema = createInsertSchema(postgres, {
postgresId: z.string(), postgresId: z.string(),
name: z.string().min(1), name: z.string().min(1),
appName: z
.string()
.min(1)
.max(63)
.regex(APP_NAME_REGEX, APP_NAME_MESSAGE)
.optional(),
databasePassword: z databasePassword: z
.string() .string()
.regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, { .regex(/^[a-zA-Z0-9@#%^&*()_+\-=[\]{}|;:,.<>?~`]*$/, {
@@ -102,7 +108,7 @@ const createSchema = createInsertSchema(postgres, {
}), }),
databaseName: z.string().min(1), databaseName: z.string().min(1),
databaseUser: z.string().min(1), databaseUser: z.string().min(1),
dockerImage: z.string().default("postgres:15"), dockerImage: z.string().default("postgres:18"),
command: z.string().optional(), command: z.string().optional(),
args: z.array(z.string()).optional(), args: z.array(z.string()).optional(),
env: z.string().optional(), env: z.string().optional(),
@@ -128,19 +134,17 @@ const createSchema = createInsertSchema(postgres, {
endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(), endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
}); });
export const apiCreatePostgres = createSchema export const apiCreatePostgres = createSchema.pick({
.pick({ name: true,
name: true, appName: true,
appName: true, databaseName: true,
databaseName: true, databaseUser: true,
databaseUser: true, databasePassword: true,
databasePassword: true, dockerImage: true,
dockerImage: true, environmentId: true,
environmentId: true, description: true,
description: true, serverId: true,
serverId: true, });
})
.required();
export const apiFindOnePostgres = createSchema export const apiFindOnePostgres = createSchema
.pick({ .pick({

View File

@@ -25,7 +25,7 @@ import {
type UpdateConfigSwarm, type UpdateConfigSwarm,
UpdateConfigSwarmSchema, UpdateConfigSwarmSchema,
} from "./shared"; } from "./shared";
import { generateAppName } from "./utils"; import { APP_NAME_MESSAGE, APP_NAME_REGEX, generateAppName } from "./utils";
export const redis = pgTable("redis", { export const redis = pgTable("redis", {
redisId: text("redisId") redisId: text("redisId")
@@ -88,7 +88,12 @@ export const redisRelations = relations(redis, ({ one, many }) => ({
const createSchema = createInsertSchema(redis, { const createSchema = createInsertSchema(redis, {
redisId: z.string(), redisId: z.string(),
appName: z.string().min(1), appName: z
.string()
.min(1)
.max(63)
.regex(APP_NAME_REGEX, APP_NAME_MESSAGE)
.optional(),
createdAt: z.string(), createdAt: z.string(),
name: z.string().min(1), name: z.string().min(1),
databasePassword: z.string(), databasePassword: z.string(),
@@ -117,17 +122,15 @@ const createSchema = createInsertSchema(redis, {
endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(), endpointSpecSwarm: EndpointSpecSwarmSchema.nullable(),
}); });
export const apiCreateRedis = createSchema export const apiCreateRedis = createSchema.pick({
.pick({ name: true,
name: true, appName: true,
appName: true, databasePassword: true,
databasePassword: true, dockerImage: true,
dockerImage: true, environmentId: true,
environmentId: true, description: true,
description: true, serverId: true,
serverId: true, });
})
.required();
export const apiFindOneRedis = createSchema export const apiFindOneRedis = createSchema
.pick({ .pick({

View File

@@ -214,6 +214,6 @@ export const apiUpdateUser = createSchema.partial().extend({
.optional(), .optional(),
password: z.string().optional(), password: z.string().optional(),
currentPassword: z.string().optional(), currentPassword: z.string().optional(),
name: z.string().optional(), firstName: z.string().optional(),
lastName: z.string().optional(), lastName: z.string().optional(),
}); });

View File

@@ -6,6 +6,12 @@ const alphabet = "abcdefghijklmnopqrstuvwxyz123456789";
const customNanoid = customAlphabet(alphabet, 6); const customNanoid = customAlphabet(alphabet, 6);
/** App name: letters, numbers, dots, underscores, hyphens only (no spaces). Safe for shell/Docker. */
export const APP_NAME_REGEX = /^[a-zA-Z0-9._-]+$/;
export const APP_NAME_MESSAGE =
"App name can only contain letters, numbers, dots, underscores and hyphens";
export const generateAppName = (type: string) => { export const generateAppName = (type: string) => {
const verb = faker.hacker.verb().replace(/ /g, "-"); const verb = faker.hacker.verb().replace(/ /g, "-");
const adjective = faker.hacker.adjective().replace(/ /g, "-"); const adjective = faker.hacker.adjective().replace(/ /g, "-");

View File

@@ -131,7 +131,10 @@ export const apiAssignDomain = z
.object({ .object({
host: z.string(), host: z.string(),
certificateType: z.enum(["letsencrypt", "none", "custom"]), certificateType: z.enum(["letsencrypt", "none", "custom"]),
letsEncryptEmail: z.string().email().optional().nullable(), letsEncryptEmail: z
.union([z.string().email(), z.literal("")])
.optional()
.nullable(),
https: z.boolean().optional(), https: z.boolean().optional(),
}) })
.required() .required()

View File

@@ -253,7 +253,11 @@ export const deployApplication = async ({
} finally { } finally {
// Only extract commit info for non-docker sources // Only extract commit info for non-docker sources
if (application.sourceType !== "docker") { if (application.sourceType !== "docker") {
const commitInfo = await getGitCommitInfo(application); const commitInfo = await getGitCommitInfo({
appName: application.appName,
type: "application",
serverId: serverId,
});
if (commitInfo) { if (commitInfo) {
await updateDeployment(deployment.deploymentId, { await updateDeployment(deployment.deploymentId, {

View File

@@ -18,7 +18,7 @@ export type Mariadb = typeof mariadb.$inferSelect;
export const createMariadb = async (input: typeof apiCreateMariaDB._type) => { export const createMariadb = async (input: typeof apiCreateMariaDB._type) => {
const appName = buildAppName("mariadb", input.appName); const appName = buildAppName("mariadb", input.appName);
const valid = await validUniqueServerAppName(input.appName); const valid = await validUniqueServerAppName(appName);
if (!valid) { if (!valid) {
throw new TRPCError({ throw new TRPCError({
code: "CONFLICT", code: "CONFLICT",

View File

@@ -6,6 +6,7 @@ import {
type apiCreateGotify, type apiCreateGotify,
type apiCreateLark, type apiCreateLark,
type apiCreateNtfy, type apiCreateNtfy,
type apiCreatePushover,
type apiCreateSlack, type apiCreateSlack,
type apiCreateTelegram, type apiCreateTelegram,
type apiUpdateCustom, type apiUpdateCustom,
@@ -14,6 +15,7 @@ import {
type apiUpdateGotify, type apiUpdateGotify,
type apiUpdateLark, type apiUpdateLark,
type apiUpdateNtfy, type apiUpdateNtfy,
type apiUpdatePushover,
type apiUpdateSlack, type apiUpdateSlack,
type apiUpdateTelegram, type apiUpdateTelegram,
custom, custom,
@@ -23,6 +25,7 @@ import {
lark, lark,
notifications, notifications,
ntfy, ntfy,
pushover,
slack, slack,
telegram, telegram,
} from "@dokploy/server/db/schema"; } from "@dokploy/server/db/schema";
@@ -694,6 +697,7 @@ export const findNotificationById = async (notificationId: string) => {
ntfy: true, ntfy: true,
custom: true, custom: true,
lark: true, lark: true,
pushover: true,
}, },
}); });
if (!notification) { if (!notification) {
@@ -817,3 +821,99 @@ export const updateNotificationById = async (
return result[0]; return result[0];
}; };
export const createPushoverNotification = async (
input: typeof apiCreatePushover._type,
organizationId: string,
) => {
await db.transaction(async (tx) => {
const newPushover = await tx
.insert(pushover)
.values({
userKey: input.userKey,
apiToken: input.apiToken,
priority: input.priority,
retry: input.retry,
expire: input.expire,
})
.returning()
.then((value) => value[0]);
if (!newPushover) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error input: Inserting pushover",
});
}
const newDestination = await tx
.insert(notifications)
.values({
pushoverId: newPushover.pushoverId,
name: input.name,
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
volumeBackup: input.volumeBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
serverThreshold: input.serverThreshold,
notificationType: "pushover",
organizationId: organizationId,
})
.returning()
.then((value) => value[0]);
if (!newDestination) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error input: Inserting notification",
});
}
return newDestination;
});
};
export const updatePushoverNotification = async (
input: typeof apiUpdatePushover._type,
) => {
await db.transaction(async (tx) => {
const newDestination = await tx
.update(notifications)
.set({
name: input.name,
appDeploy: input.appDeploy,
appBuildError: input.appBuildError,
databaseBackup: input.databaseBackup,
volumeBackup: input.volumeBackup,
dokployRestart: input.dokployRestart,
dockerCleanup: input.dockerCleanup,
organizationId: input.organizationId,
serverThreshold: input.serverThreshold,
})
.where(eq(notifications.notificationId, input.notificationId))
.returning()
.then((value) => value[0]);
if (!newDestination) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error Updating notification",
});
}
await tx
.update(pushover)
.set({
userKey: input.userKey,
apiToken: input.apiToken,
priority: input.priority,
retry: input.retry,
expire: input.expire,
})
.where(eq(pushover.pushoverId, input.pushoverId));
return newDestination;
});
};

View File

@@ -20,7 +20,7 @@ export const TRAEFIK_PORT =
Number.parseInt(process.env.TRAEFIK_PORT!, 10) || 80; Number.parseInt(process.env.TRAEFIK_PORT!, 10) || 80;
export const TRAEFIK_HTTP3_PORT = export const TRAEFIK_HTTP3_PORT =
Number.parseInt(process.env.TRAEFIK_HTTP3_PORT!, 10) || 443; Number.parseInt(process.env.TRAEFIK_HTTP3_PORT!, 10) || 443;
export const TRAEFIK_VERSION = process.env.TRAEFIK_VERSION || "3.6.1"; export const TRAEFIK_VERSION = process.env.TRAEFIK_VERSION || "3.6.4";
export interface TraefikOptions { export interface TraefikOptions {
env?: string[]; env?: string[];

View File

@@ -11,6 +11,7 @@ import {
sendGotifyNotification, sendGotifyNotification,
sendLarkNotification, sendLarkNotification,
sendNtfyNotification, sendNtfyNotification,
sendPushoverNotification,
sendSlackNotification, sendSlackNotification,
sendTelegramNotification, sendTelegramNotification,
} from "./utils"; } from "./utils";
@@ -48,12 +49,22 @@ export const sendBuildErrorNotifications = async ({
ntfy: true, ntfy: true,
custom: true, custom: true,
lark: true, lark: true,
pushover: true,
}, },
}); });
for (const notification of notificationList) { for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify, ntfy, custom, lark } = const {
notification; email,
discord,
telegram,
slack,
gotify,
ntfy,
custom,
lark,
pushover,
} = notification;
try { try {
if (email) { if (email) {
const template = await renderAsync( const template = await renderAsync(
@@ -349,6 +360,14 @@ export const sendBuildErrorNotifications = async ({
}, },
}); });
} }
if (pushover) {
await sendPushoverNotification(
pushover,
"Build Failed",
`Project: ${projectName}\nApplication: ${applicationName}\nType: ${applicationType}\nDate: ${date.toLocaleString()}\nError: ${errorMessage}`,
);
}
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} }

View File

@@ -12,6 +12,7 @@ import {
sendGotifyNotification, sendGotifyNotification,
sendLarkNotification, sendLarkNotification,
sendNtfyNotification, sendNtfyNotification,
sendPushoverNotification,
sendSlackNotification, sendSlackNotification,
sendTelegramNotification, sendTelegramNotification,
} from "./utils"; } from "./utils";
@@ -51,12 +52,22 @@ export const sendBuildSuccessNotifications = async ({
ntfy: true, ntfy: true,
custom: true, custom: true,
lark: true, lark: true,
pushover: true,
}, },
}); });
for (const notification of notificationList) { for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify, ntfy, custom, lark } = const {
notification; email,
discord,
telegram,
slack,
gotify,
ntfy,
custom,
lark,
pushover,
} = notification;
try { try {
if (email) { if (email) {
const template = await renderAsync( const template = await renderAsync(
@@ -363,6 +374,14 @@ export const sendBuildSuccessNotifications = async ({
}, },
}); });
} }
if (pushover) {
await sendPushoverNotification(
pushover,
"Build Success",
`Project: ${projectName}\nApplication: ${applicationName}\nEnvironment: ${environmentName}\nType: ${applicationType}\nDate: ${date.toLocaleString()}`,
);
}
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} }

View File

@@ -11,6 +11,7 @@ import {
sendGotifyNotification, sendGotifyNotification,
sendLarkNotification, sendLarkNotification,
sendNtfyNotification, sendNtfyNotification,
sendPushoverNotification,
sendSlackNotification, sendSlackNotification,
sendTelegramNotification, sendTelegramNotification,
} from "./utils"; } from "./utils";
@@ -48,12 +49,22 @@ export const sendDatabaseBackupNotifications = async ({
ntfy: true, ntfy: true,
custom: true, custom: true,
lark: true, lark: true,
pushover: true,
}, },
}); });
for (const notification of notificationList) { for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify, ntfy, custom, lark } = const {
notification; email,
discord,
telegram,
slack,
gotify,
ntfy,
custom,
lark,
pushover,
} = notification;
try { try {
if (email) { if (email) {
const template = await renderAsync( const template = await renderAsync(
@@ -377,6 +388,14 @@ export const sendDatabaseBackupNotifications = async ({
}, },
}); });
} }
if (pushover) {
await sendPushoverNotification(
pushover,
`Database Backup ${type === "success" ? "Successful" : "Failed"}`,
`Project: ${projectName}\nApplication: ${applicationName}\nDatabase: ${databaseType}\nDatabase Name: ${databaseName}\nDate: ${date.toLocaleString()}${type === "error" && errorMessage ? `\nError: ${errorMessage}` : ""}`,
);
}
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} }

View File

@@ -11,6 +11,7 @@ import {
sendGotifyNotification, sendGotifyNotification,
sendLarkNotification, sendLarkNotification,
sendNtfyNotification, sendNtfyNotification,
sendPushoverNotification,
sendSlackNotification, sendSlackNotification,
sendTelegramNotification, sendTelegramNotification,
} from "./utils"; } from "./utils";
@@ -35,12 +36,22 @@ export const sendDockerCleanupNotifications = async (
ntfy: true, ntfy: true,
custom: true, custom: true,
lark: true, lark: true,
pushover: true,
}, },
}); });
for (const notification of notificationList) { for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify, ntfy, custom, lark } = const {
notification; email,
discord,
telegram,
slack,
gotify,
ntfy,
custom,
lark,
pushover,
} = notification;
try { try {
if (email) { if (email) {
const template = await renderAsync( const template = await renderAsync(
@@ -230,6 +241,14 @@ export const sendDockerCleanupNotifications = async (
}, },
}); });
} }
if (pushover) {
await sendPushoverNotification(
pushover,
"Docker Cleanup",
`Date: ${date.toLocaleString()}\nMessage: ${message}`,
);
}
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} }

View File

@@ -11,6 +11,7 @@ import {
sendGotifyNotification, sendGotifyNotification,
sendLarkNotification, sendLarkNotification,
sendNtfyNotification, sendNtfyNotification,
sendPushoverNotification,
sendSlackNotification, sendSlackNotification,
sendTelegramNotification, sendTelegramNotification,
} from "./utils"; } from "./utils";
@@ -29,12 +30,22 @@ export const sendDokployRestartNotifications = async () => {
ntfy: true, ntfy: true,
custom: true, custom: true,
lark: true, lark: true,
pushover: true,
}, },
}); });
for (const notification of notificationList) { for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify, ntfy, custom, lark } = const {
notification; email,
discord,
telegram,
slack,
gotify,
ntfy,
custom,
lark,
pushover,
} = notification;
try { try {
if (email) { if (email) {
@@ -219,6 +230,14 @@ export const sendDokployRestartNotifications = async () => {
}, },
}); });
} }
if (pushover) {
await sendPushoverNotification(
pushover,
"Dokploy Server Restarted",
`Date: ${date.toLocaleString()}`,
);
}
} catch (error) { } catch (error) {
console.log(error); console.log(error);
} }

View File

@@ -5,6 +5,7 @@ import {
sendCustomNotification, sendCustomNotification,
sendDiscordNotification, sendDiscordNotification,
sendLarkNotification, sendLarkNotification,
sendPushoverNotification,
sendSlackNotification, sendSlackNotification,
sendTelegramNotification, sendTelegramNotification,
} from "./utils"; } from "./utils";
@@ -38,6 +39,7 @@ export const sendServerThresholdNotifications = async (
slack: true, slack: true,
custom: true, custom: true,
lark: true, lark: true,
pushover: true,
}, },
}); });
@@ -45,7 +47,7 @@ export const sendServerThresholdNotifications = async (
const typeColor = 0xff0000; // Rojo para indicar alerta const typeColor = 0xff0000; // Rojo para indicar alerta
for (const notification of notificationList) { for (const notification of notificationList) {
const { discord, telegram, slack, custom, lark } = notification; const { discord, telegram, slack, custom, lark, pushover } = notification;
if (discord) { if (discord) {
const decorate = (decoration: string, text: string) => const decorate = (decoration: string, text: string) =>
@@ -266,5 +268,13 @@ export const sendServerThresholdNotifications = async (
}, },
}); });
} }
if (pushover) {
await sendPushoverNotification(
pushover,
`Server ${payload.Type} Alert`,
`Server: ${payload.ServerName}\nType: ${payload.Type}\nCurrent: ${payload.Value.toFixed(2)}%\nThreshold: ${payload.Threshold.toFixed(2)}%\nMessage: ${payload.Message}\nTime: ${date.toLocaleString()}`,
);
}
} }
}; };

View File

@@ -5,6 +5,7 @@ import type {
gotify, gotify,
lark, lark,
ntfy, ntfy,
pushover,
slack, slack,
telegram, telegram,
} from "@dokploy/server/db/schema"; } from "@dokploy/server/db/schema";
@@ -223,3 +224,33 @@ export const sendLarkNotification = async (
console.log(err); console.log(err);
} }
}; };
export const sendPushoverNotification = async (
connection: typeof pushover.$inferInsert,
title: string,
message: string,
) => {
const formData = new URLSearchParams();
formData.append("token", connection.apiToken);
formData.append("user", connection.userKey);
formData.append("title", title);
formData.append("message", message);
formData.append("priority", connection.priority?.toString() || "0");
// For emergency priority (2), retry and expire are required
if (connection.priority === 2) {
formData.append("retry", connection.retry?.toString() || "30");
formData.append("expire", connection.expire?.toString() || "3600");
}
const response = await fetch("https://api.pushover.net/1/messages.json", {
method: "POST",
body: formData,
});
if (!response.ok) {
throw new Error(
`Failed to send Pushover notification: ${response.statusText}`,
);
}
};

View File

@@ -9,6 +9,7 @@ import {
sendEmailNotification, sendEmailNotification,
sendGotifyNotification, sendGotifyNotification,
sendNtfyNotification, sendNtfyNotification,
sendPushoverNotification,
sendSlackNotification, sendSlackNotification,
sendTelegramNotification, sendTelegramNotification,
} from "./utils"; } from "./utils";
@@ -53,11 +54,13 @@ export const sendVolumeBackupNotifications = async ({
slack: true, slack: true,
gotify: true, gotify: true,
ntfy: true, ntfy: true,
pushover: true,
}, },
}); });
for (const notification of notificationList) { for (const notification of notificationList) {
const { email, discord, telegram, slack, gotify, ntfy } = notification; const { email, discord, telegram, slack, gotify, ntfy, pushover } =
notification;
if (email) { if (email) {
const subject = `Volume Backup ${type === "success" ? "Successful" : "Failed"} - ${applicationName}`; const subject = `Volume Backup ${type === "success" ? "Successful" : "Failed"} - ${applicationName}`;
@@ -270,5 +273,13 @@ export const sendVolumeBackupNotifications = async ({
], ],
}); });
} }
if (pushover) {
await sendPushoverNotification(
pushover,
`Volume Backup ${type === "success" ? "Successful" : "Failed"}`,
`Project: ${projectName}\nApplication: ${applicationName}\nVolume: ${volumeName}\nService Type: ${serviceType}${backupSize ? `\nBackup Size: ${backupSize}` : ""}\nDate: ${date.toLocaleString()}${type === "error" && errorMessage ? `\nError: ${errorMessage}` : ""}`,
);
}
} }
}; };

View File

@@ -79,6 +79,7 @@ export const getBitbucketHeaders = (bitbucketProvider: Bitbucket) => {
interface CloneBitbucketRepository { interface CloneBitbucketRepository {
appName: string; appName: string;
bitbucketRepository: string | null; bitbucketRepository: string | null;
bitbucketRepositorySlug?: string | null;
bitbucketOwner: string | null; bitbucketOwner: string | null;
bitbucketBranch: string | null; bitbucketBranch: string | null;
bitbucketId: string | null; bitbucketId: string | null;
@@ -117,7 +118,8 @@ export const cloneBitbucketRepository = async ({
const outputPath = join(basePath, appName, "code"); const outputPath = join(basePath, appName, "code");
command += `rm -rf ${outputPath};`; command += `rm -rf ${outputPath};`;
command += `mkdir -p ${outputPath};`; command += `mkdir -p ${outputPath};`;
const repoclone = `bitbucket.org/${bitbucketOwner}/${bitbucketRepository}.git`; const repoToUse = entity.bitbucketRepositorySlug || bitbucketRepository;
const repoclone = `bitbucket.org/${bitbucketOwner}/${repoToUse}.git`;
const cloneUrl = getBitbucketCloneUrl(bitbucket, repoclone); const cloneUrl = getBitbucketCloneUrl(bitbucket, repoclone);
command += `echo "Cloning Repo ${repoclone} to ${outputPath}: ✅";`; command += `echo "Cloning Repo ${repoclone} to ${outputPath}: ✅";`;
command += `git clone --branch ${bitbucketBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} --progress;`; command += `git clone --branch ${bitbucketBranch} --depth 1 ${enableSubmodules ? "--recurse-submodules" : ""} ${cloneUrl} ${outputPath} --progress;`;
@@ -137,6 +139,7 @@ export const getBitbucketRepositories = async (bitbucketId?: string) => {
let repositories: { let repositories: {
name: string; name: string;
url: string; url: string;
slug: string;
owner: { username: string }; owner: { username: string };
}[] = []; }[] = [];
@@ -159,6 +162,7 @@ export const getBitbucketRepositories = async (bitbucketId?: string) => {
const mappedData = data.values.map((repo: any) => ({ const mappedData = data.values.map((repo: any) => ({
name: repo.name, name: repo.name,
url: repo.links.html.href, url: repo.links.html.href,
slug: repo.slug,
owner: { owner: {
username: repo.workspace.slug, username: repo.workspace.slug,
}, },