mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-15 20:25:23 +02:00
Merge branch 'canary' into feat/quick-service-switcher
This commit is contained in:
@@ -14,13 +14,18 @@ vi.mock("@dokploy/server/db", () => {
|
||||
set: vi.fn(() => chain),
|
||||
where: vi.fn(() => chain),
|
||||
returning: vi.fn().mockResolvedValue([{}] as any),
|
||||
from: vi.fn(() => chain),
|
||||
innerJoin: vi.fn(() => chain),
|
||||
then: (resolve: (v: any) => void) => {
|
||||
resolve([]);
|
||||
},
|
||||
} as any;
|
||||
return chain;
|
||||
};
|
||||
|
||||
return {
|
||||
db: {
|
||||
select: vi.fn(),
|
||||
select: vi.fn(() => createChainableMock()),
|
||||
insert: vi.fn(),
|
||||
update: vi.fn(() => createChainableMock()),
|
||||
delete: vi.fn(),
|
||||
@@ -31,6 +36,9 @@ vi.mock("@dokploy/server/db", () => {
|
||||
patch: {
|
||||
findMany: vi.fn().mockResolvedValue([]),
|
||||
},
|
||||
member: {
|
||||
findMany: vi.fn().mockResolvedValue([]),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -15,13 +15,18 @@ vi.mock("@dokploy/server/db", () => {
|
||||
set: vi.fn(() => chain),
|
||||
where: vi.fn(() => chain),
|
||||
returning: vi.fn().mockResolvedValue([{}]),
|
||||
from: vi.fn(() => chain),
|
||||
innerJoin: vi.fn(() => chain),
|
||||
then: (resolve: (v: any) => void) => {
|
||||
resolve([]);
|
||||
},
|
||||
};
|
||||
return chain;
|
||||
};
|
||||
|
||||
return {
|
||||
db: {
|
||||
select: vi.fn(),
|
||||
select: vi.fn(() => createChainableMock()),
|
||||
insert: vi.fn(),
|
||||
update: vi.fn(() => createChainableMock()),
|
||||
delete: vi.fn(),
|
||||
@@ -32,6 +37,9 @@ vi.mock("@dokploy/server/db", () => {
|
||||
patch: {
|
||||
findMany: vi.fn().mockResolvedValue([]),
|
||||
},
|
||||
member: {
|
||||
findMany: vi.fn().mockResolvedValue([]),
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -12,8 +12,11 @@ vi.mock("@dokploy/server/db", () => {
|
||||
chain.where = () => chain;
|
||||
chain.values = () => chain;
|
||||
chain.returning = () => Promise.resolve([{}]);
|
||||
// biome-ignore lint/suspicious/noThenProperty: this is for testing
|
||||
chain.then = undefined;
|
||||
chain.from = () => chain;
|
||||
chain.innerJoin = () => chain;
|
||||
chain.then = (resolve: (value: unknown) => void) => {
|
||||
resolve([]);
|
||||
};
|
||||
|
||||
const tableMock = {
|
||||
findFirst: vi.fn(() => Promise.resolve(undefined)),
|
||||
@@ -22,7 +25,6 @@ vi.mock("@dokploy/server/db", () => {
|
||||
update: vi.fn(() => chain),
|
||||
delete: vi.fn(() => chain),
|
||||
};
|
||||
const createQueryMock = () => tableMock;
|
||||
|
||||
return {
|
||||
db: {
|
||||
|
||||
@@ -194,13 +194,21 @@ export const ShowDeployment = ({
|
||||
{" "}
|
||||
{filteredLogs.length > 0 ? (
|
||||
filteredLogs.map((log: LogLine, index: number) => (
|
||||
<TerminalLine key={index} log={log} noTimestamp />
|
||||
<TerminalLine
|
||||
key={`${log.rawTimestamp ?? ""}-${index}`}
|
||||
log={log}
|
||||
noTimestamp
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<>
|
||||
{optionalErrors.length > 0 ? (
|
||||
optionalErrors.map((log: LogLine, index: number) => (
|
||||
<TerminalLine key={`extra-${index}`} log={log} noTimestamp />
|
||||
<TerminalLine
|
||||
key={`extra-${log.rawTimestamp ?? ""}-${index}`}
|
||||
log={log}
|
||||
noTimestamp
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className="flex justify-center items-center h-full text-muted-foreground">
|
||||
|
||||
@@ -459,7 +459,7 @@ export const SaveGithubProvider = ({ applicationId }: Props) => {
|
||||
<div className="flex flex-wrap gap-2 mb-2">
|
||||
{field.value?.map((path, index) => (
|
||||
<Badge
|
||||
key={index}
|
||||
key={`${path}-${index}`}
|
||||
variant="secondary"
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
|
||||
@@ -448,7 +448,7 @@ export const SaveGitlabProvider = ({ applicationId }: Props) => {
|
||||
<div className="flex flex-wrap gap-2 mb-2">
|
||||
{field.value?.map((path, index) => (
|
||||
<Badge
|
||||
key={index}
|
||||
key={`${path}-${index}`}
|
||||
variant="secondary"
|
||||
className="flex items-center gap-1"
|
||||
>
|
||||
|
||||
@@ -402,7 +402,7 @@ export const DockerLogsId: React.FC<Props> = ({
|
||||
{filteredLogs.length > 0 ? (
|
||||
filteredLogs.map((filteredLog: LogLine, index: number) => (
|
||||
<TerminalLine
|
||||
key={index}
|
||||
key={`${filteredLog.rawTimestamp ?? ""}-${index}`}
|
||||
log={filteredLog}
|
||||
searchTerm={search}
|
||||
noTimestamp={!showTimestamp}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
||||
import { PenBoxIcon } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
@@ -41,6 +41,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export const UpdateMariadb = ({ mariadbId }: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const utils = api.useUtils();
|
||||
const { mutateAsync, error, isError, isPending } =
|
||||
api.mariadb.update.useMutation();
|
||||
@@ -79,6 +80,7 @@ export const UpdateMariadb = ({ mariadbId }: Props) => {
|
||||
utils.mariadb.one.invalidate({
|
||||
mariadbId: mariadbId,
|
||||
});
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error updating the Mariadb");
|
||||
@@ -87,7 +89,7 @@ export const UpdateMariadb = ({ mariadbId }: Props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
||||
import { PenBoxIcon } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
@@ -41,6 +41,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export const UpdateMysql = ({ mysqlId }: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const utils = api.useUtils();
|
||||
const { mutateAsync, error, isError, isPending } =
|
||||
api.mysql.update.useMutation();
|
||||
@@ -79,6 +80,7 @@ export const UpdateMysql = ({ mysqlId }: Props) => {
|
||||
utils.mysql.one.invalidate({
|
||||
mysqlId: mysqlId,
|
||||
});
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error updating MySQL");
|
||||
@@ -87,7 +89,7 @@ export const UpdateMysql = ({ mysqlId }: Props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
||||
@@ -25,7 +25,6 @@ import {
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
export type Services = {
|
||||
appName: string;
|
||||
serverId?: string | null;
|
||||
name: string;
|
||||
type:
|
||||
|
||||
@@ -2,7 +2,6 @@ import {
|
||||
AlertTriangle,
|
||||
ArrowUpDown,
|
||||
BookIcon,
|
||||
ExternalLinkIcon,
|
||||
FolderInput,
|
||||
Loader2,
|
||||
MoreHorizontalIcon,
|
||||
@@ -16,7 +15,6 @@ import { toast } from "sonner";
|
||||
import { AdvanceBreadcrumb } from "@/components/shared/advance-breadcrumb";
|
||||
import { DateTooltip } from "@/components/shared/date-tooltip";
|
||||
import { FocusShortcutInput } from "@/components/shared/focus-shortcut-input";
|
||||
import { StatusTooltip } from "@/components/shared/status-tooltip";
|
||||
import {
|
||||
AlertDialog,
|
||||
AlertDialogAction,
|
||||
@@ -40,10 +38,8 @@ import {
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
DropdownMenuGroup,
|
||||
DropdownMenuItem,
|
||||
DropdownMenuLabel,
|
||||
DropdownMenuSeparator,
|
||||
DropdownMenuTrigger,
|
||||
} from "@/components/ui/dropdown-menu";
|
||||
import {
|
||||
@@ -278,14 +274,6 @@ export const ShowProjects = () => {
|
||||
)
|
||||
.reduce((acc, curr) => acc + curr, 0);
|
||||
|
||||
const haveServicesWithDomains = project?.environments
|
||||
.map(
|
||||
(env) =>
|
||||
env.applications.length > 0 ||
|
||||
env.compose.length > 0,
|
||||
)
|
||||
.some(Boolean);
|
||||
|
||||
// Find default environment from accessible environments, or fall back to first accessible environment
|
||||
const accessibleEnvironment =
|
||||
project?.environments.find((env) => env.isDefault) ||
|
||||
@@ -311,122 +299,6 @@ export const ShowProjects = () => {
|
||||
}}
|
||||
>
|
||||
<Card className="group relative w-full h-full bg-transparent transition-colors hover:bg-border">
|
||||
{haveServicesWithDomains ? (
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
className="absolute -right-3 -top-3 size-9 translate-y-1 rounded-full p-0 opacity-0 transition-all duration-200 group-hover:translate-y-0 group-hover:opacity-100"
|
||||
size="sm"
|
||||
variant="default"
|
||||
>
|
||||
<ExternalLinkIcon className="size-3.5" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent
|
||||
className="w-[200px] space-y-2 overflow-y-auto max-h-[400px]"
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
>
|
||||
{project.environments.some(
|
||||
(env) => env.applications.length > 0,
|
||||
) && (
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel>
|
||||
Applications
|
||||
</DropdownMenuLabel>
|
||||
{project.environments.map((env) =>
|
||||
env.applications.map((app) => (
|
||||
<div key={app.applicationId}>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel className="font-normal capitalize text-xs flex items-center justify-between">
|
||||
{app.name}
|
||||
<StatusTooltip
|
||||
status={
|
||||
app.applicationStatus
|
||||
}
|
||||
/>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{app.domains.map((domain) => (
|
||||
<DropdownMenuItem
|
||||
key={domain.domainId}
|
||||
asChild
|
||||
>
|
||||
<Link
|
||||
className="space-x-4 text-xs cursor-pointer justify-between"
|
||||
target="_blank"
|
||||
href={`${
|
||||
domain.https
|
||||
? "https"
|
||||
: "http"
|
||||
}://${domain.host}${
|
||||
domain.path
|
||||
}`}
|
||||
>
|
||||
<span className="truncate">
|
||||
{domain.host}
|
||||
</span>
|
||||
<ExternalLinkIcon className="size-4 shrink-0" />
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuGroup>
|
||||
</div>
|
||||
)),
|
||||
)}
|
||||
</DropdownMenuGroup>
|
||||
)}
|
||||
{project.environments.some(
|
||||
(env) => env.compose.length > 0,
|
||||
) && (
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel>
|
||||
Compose
|
||||
</DropdownMenuLabel>
|
||||
{project.environments.map((env) =>
|
||||
env.compose.map((comp) => (
|
||||
<div key={comp.composeId}>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuGroup>
|
||||
<DropdownMenuLabel className="font-normal capitalize text-xs flex items-center justify-between">
|
||||
{comp.name}
|
||||
<StatusTooltip
|
||||
status={comp.composeStatus}
|
||||
/>
|
||||
</DropdownMenuLabel>
|
||||
<DropdownMenuSeparator />
|
||||
{comp.domains.map((domain) => (
|
||||
<DropdownMenuItem
|
||||
key={domain.domainId}
|
||||
asChild
|
||||
>
|
||||
<Link
|
||||
className="space-x-4 text-xs cursor-pointer justify-between"
|
||||
target="_blank"
|
||||
href={`${
|
||||
domain.https
|
||||
? "https"
|
||||
: "http"
|
||||
}://${domain.host}${
|
||||
domain.path
|
||||
}`}
|
||||
>
|
||||
<span className="truncate">
|
||||
{domain.host}
|
||||
</span>
|
||||
<ExternalLinkIcon className="size-4 shrink-0" />
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
))}
|
||||
</DropdownMenuGroup>
|
||||
</div>
|
||||
)),
|
||||
)}
|
||||
</DropdownMenuGroup>
|
||||
)}
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
) : null}
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center justify-between gap-2 overflow-clip">
|
||||
<span className="flex flex-col gap-1.5 ">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
||||
import { PenBoxIcon } from "lucide-react";
|
||||
import { useEffect } from "react";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
@@ -41,6 +41,7 @@ interface Props {
|
||||
}
|
||||
|
||||
export const UpdateRedis = ({ redisId }: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const utils = api.useUtils();
|
||||
const { mutateAsync, error, isError, isPending } =
|
||||
api.redis.update.useMutation();
|
||||
@@ -79,6 +80,7 @@ export const UpdateRedis = ({ redisId }: Props) => {
|
||||
utils.redis.one.invalidate({
|
||||
redisId: redisId,
|
||||
});
|
||||
setIsOpen(false);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Error updating Redis");
|
||||
@@ -87,7 +89,7 @@ export const UpdateRedis = ({ redisId }: Props) => {
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog>
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
variant="ghost"
|
||||
|
||||
@@ -30,7 +30,7 @@ export const AddGithubProvider = () => {
|
||||
const url = document.location.origin;
|
||||
const manifest = JSON.stringify(
|
||||
{
|
||||
redirect_url: `${origin}/api/providers/github/setup?organizationId=${activeOrganization?.id}&userId=${session?.user?.id}`,
|
||||
redirect_url: `${origin}/api/providers/github/setup?organizationId=${activeOrganization?.id ?? ""}&userId=${session?.user?.id ?? ""}`,
|
||||
name: `Dokploy-${format(new Date(), "yyyy-MM-dd")}-${randomString()}`,
|
||||
url: origin,
|
||||
hook_attributes: {
|
||||
@@ -52,7 +52,7 @@ export const AddGithubProvider = () => {
|
||||
);
|
||||
|
||||
setManifest(manifest);
|
||||
}, [data?.id]);
|
||||
}, [data?.id, activeOrganization?.id, session?.user?.id]);
|
||||
|
||||
return (
|
||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
||||
@@ -131,7 +131,11 @@ export const AddGithubProvider = () => {
|
||||
Unsure if you already have an app?
|
||||
</a>
|
||||
<Button
|
||||
disabled={isOrganization && organizationName.length < 1}
|
||||
disabled={
|
||||
(isOrganization && organizationName.length < 1) ||
|
||||
!activeOrganization?.id ||
|
||||
!session?.user?.id
|
||||
}
|
||||
type="submit"
|
||||
className="self-end"
|
||||
>
|
||||
|
||||
@@ -356,7 +356,7 @@ export const Configure2FA = () => {
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{backupCodes.map((code, index) => (
|
||||
<code
|
||||
key={index}
|
||||
key={`${code}-${index}`}
|
||||
className="bg-background p-2 rounded text-sm font-mono text-center"
|
||||
>
|
||||
{code}
|
||||
|
||||
@@ -401,7 +401,7 @@ export const Enable2FA = () => {
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{backupCodes.map((code, index) => (
|
||||
<code
|
||||
key={index}
|
||||
key={`${code}-${index}`}
|
||||
className="bg-muted p-2 rounded text-sm font-mono"
|
||||
>
|
||||
{code}
|
||||
|
||||
@@ -100,7 +100,7 @@ export const SetupMonitoring = ({ serverId }: Props) => {
|
||||
|
||||
const url = useUrl();
|
||||
|
||||
const { data: projects } = api.project.all.useQuery();
|
||||
const { data: projects } = api.project.allForPermissions.useQuery();
|
||||
|
||||
const extractServicesFromProjects = () => {
|
||||
if (!projects) return [];
|
||||
|
||||
@@ -190,7 +190,7 @@ export const SetupServer = ({ serverId, asButton = false }: Props) => {
|
||||
Automatic process
|
||||
</span>
|
||||
<Link
|
||||
href="https://docs.dokploy.com/docs/core/multi-server/instructions#requirements"
|
||||
href="https://docs.dokploy.com/docs/core/remote-servers/instructions#requirements"
|
||||
target="_blank"
|
||||
className="text-primary flex flex-row gap-2"
|
||||
>
|
||||
|
||||
@@ -172,7 +172,7 @@ export const CreateSSHKey = () => {
|
||||
etc.)
|
||||
</p>
|
||||
<Link
|
||||
href="https://docs.dokploy.com/docs/core/multi-server/instructions#requirements"
|
||||
href="https://docs.dokploy.com/docs/core/remote-servers/instructions#requirements"
|
||||
target="_blank"
|
||||
className="text-primary flex flex-row gap-2 mt-2"
|
||||
>
|
||||
|
||||
@@ -307,9 +307,9 @@ export const WelcomeSuscription = () => {
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 gap-6">
|
||||
{features.map((feature, index) => (
|
||||
{features.map((feature) => (
|
||||
<div
|
||||
key={index}
|
||||
key={feature.title}
|
||||
className="flex flex-col items-start p-4 bg-card rounded-lg shadow-md hover:shadow-lg transition-shadow"
|
||||
>
|
||||
<div className="text-3xl mb-2">{feature.icon}</div>
|
||||
|
||||
@@ -28,8 +28,12 @@ import {
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { api, type RouterOutputs } from "@/utils/api";
|
||||
|
||||
type Project = RouterOutputs["project"]["all"][number];
|
||||
type Environment = Project["environments"][number];
|
||||
/** Shape returned by project.allForPermissions (admin only). Used for the permissions UI. */
|
||||
type ProjectForPermissions =
|
||||
RouterOutputs["project"]["allForPermissions"][number];
|
||||
type EnvironmentForPermissions = ProjectForPermissions["environments"][number];
|
||||
|
||||
type Environment = EnvironmentForPermissions;
|
||||
|
||||
export type Services = {
|
||||
appName: string;
|
||||
@@ -173,7 +177,9 @@ interface Props {
|
||||
|
||||
export const AddUserPermissions = ({ userId }: Props) => {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
const { data: projects } = api.project.all.useQuery();
|
||||
const { data: projects } = api.project.allForPermissions.useQuery(undefined, {
|
||||
enabled: isOpen,
|
||||
});
|
||||
|
||||
const { data, refetch } = api.user.one.useQuery(
|
||||
{
|
||||
|
||||
@@ -11,20 +11,19 @@ interface Props {
|
||||
export const DashboardLayout = ({ children }: Props) => {
|
||||
const { data: haveRootAccess } = api.user.haveRootAccess.useQuery();
|
||||
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||
const { data: isUserSubscribed } = api.settings.isUserSubscribed.useQuery(
|
||||
undefined,
|
||||
{
|
||||
enabled: isCloud === true,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
refetchOnReconnect: false,
|
||||
},
|
||||
);
|
||||
const { data: currentPlan } = api.stripe.getCurrentPlan.useQuery(undefined, {
|
||||
enabled: isCloud === true,
|
||||
refetchOnWindowFocus: false,
|
||||
refetchOnMount: false,
|
||||
refetchOnReconnect: false,
|
||||
});
|
||||
|
||||
const isChatEnabled = isCloud === true && currentPlan === "startup";
|
||||
|
||||
return (
|
||||
<>
|
||||
<Page>{children}</Page>
|
||||
{isCloud === true && isUserSubscribed === true && (
|
||||
{isChatEnabled && (
|
||||
<>
|
||||
<HubSpotWidget />
|
||||
</>
|
||||
|
||||
@@ -32,7 +32,6 @@ interface Props {
|
||||
}
|
||||
|
||||
export const BreadcrumbSidebar = ({ list }: Props) => {
|
||||
console.log(list);
|
||||
return (
|
||||
<header className="flex h-16 shrink-0 items-center gap-2 transition-[width,height] ease-linear group-has-[[data-collapsible=icon]]/sidebar-wrapper:h-12">
|
||||
<div className="flex items-center justify-between w-full">
|
||||
|
||||
@@ -60,7 +60,11 @@ export const DrawerLogs = ({ isOpen, onClose, filteredLogs }: Props) => {
|
||||
{" "}
|
||||
{filteredLogs.length > 0 ? (
|
||||
filteredLogs.map((log: LogLine, index: number) => (
|
||||
<TerminalLine key={index} log={log} noTimestamp />
|
||||
<TerminalLine
|
||||
key={`${log.rawTimestamp ?? ""}-${index}`}
|
||||
log={log}
|
||||
noTimestamp
|
||||
/>
|
||||
))
|
||||
) : (
|
||||
<div className="flex justify-center items-center h-full text-muted-foreground">
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dokploy",
|
||||
"version": "v0.28.0",
|
||||
"version": "v0.28.2",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
@@ -55,7 +55,7 @@
|
||||
"@codemirror/legacy-modes": "6.4.0",
|
||||
"@codemirror/view": "6.29.0",
|
||||
"@dokploy/server": "workspace:*",
|
||||
"@dokploy/trpc-openapi": "0.0.16",
|
||||
"@dokploy/trpc-openapi": "0.0.17",
|
||||
"@faker-js/faker": "^8.4.1",
|
||||
"@hookform/resolvers": "^5.2.2",
|
||||
"@octokit/auth-app": "^6.1.3",
|
||||
|
||||
@@ -100,7 +100,6 @@ import { appRouter } from "@/server/api/root";
|
||||
import { api } from "@/utils/api";
|
||||
|
||||
export type Services = {
|
||||
appName: string;
|
||||
serverId?: string | null;
|
||||
serverName?: string | null;
|
||||
name: string;
|
||||
@@ -146,7 +145,6 @@ export const extractServicesFromEnvironment = (
|
||||
}
|
||||
}
|
||||
return {
|
||||
appName: item.appName,
|
||||
name: item.name,
|
||||
type: "application",
|
||||
id: item.applicationId,
|
||||
@@ -161,7 +159,6 @@ export const extractServicesFromEnvironment = (
|
||||
|
||||
const mariadb: Services[] =
|
||||
environment.mariadb?.map((item) => ({
|
||||
appName: item.appName,
|
||||
name: item.name,
|
||||
type: "mariadb",
|
||||
id: item.mariadbId,
|
||||
@@ -174,7 +171,6 @@ export const extractServicesFromEnvironment = (
|
||||
|
||||
const postgres: Services[] =
|
||||
environment.postgres?.map((item) => ({
|
||||
appName: item.appName,
|
||||
name: item.name,
|
||||
type: "postgres",
|
||||
id: item.postgresId,
|
||||
@@ -187,7 +183,6 @@ export const extractServicesFromEnvironment = (
|
||||
|
||||
const mongo: Services[] =
|
||||
environment.mongo?.map((item) => ({
|
||||
appName: item.appName,
|
||||
name: item.name,
|
||||
type: "mongo",
|
||||
id: item.mongoId,
|
||||
@@ -200,7 +195,6 @@ export const extractServicesFromEnvironment = (
|
||||
|
||||
const redis: Services[] =
|
||||
environment.redis?.map((item) => ({
|
||||
appName: item.appName,
|
||||
name: item.name,
|
||||
type: "redis",
|
||||
id: item.redisId,
|
||||
@@ -213,7 +207,6 @@ export const extractServicesFromEnvironment = (
|
||||
|
||||
const mysql: Services[] =
|
||||
environment.mysql?.map((item) => ({
|
||||
appName: item.appName,
|
||||
name: item.name,
|
||||
type: "mysql",
|
||||
id: item.mysqlId,
|
||||
@@ -242,7 +235,6 @@ export const extractServicesFromEnvironment = (
|
||||
}
|
||||
}
|
||||
return {
|
||||
appName: item.appName,
|
||||
name: item.name,
|
||||
type: "compose",
|
||||
id: item.composeId,
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
# Proprietary Features
|
||||
|
||||
This directory contains all proprietary functionality of Dokploy.
|
||||
|
||||
## Purpose
|
||||
|
||||
This folder will house all **paid features** and premium functionality that are not part of the open source code.
|
||||
|
||||
## License
|
||||
|
||||
The code in this directory is under Dokploy's proprietary license. See [LICENSE_PROPRIETARY.md](../../../LICENSE_PROPRIETARY.md) for more details.
|
||||
|
||||
## Contact
|
||||
|
||||
If you want to learn more about our paid features or have any questions, please contact us at:
|
||||
|
||||
- Email: [sales@dokploy.com](mailto:sales@dokploy.com)
|
||||
- Contact Form: [https://dokploy.com/contact](https://dokploy.com/contact)
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
findApplicationById,
|
||||
findEnvironmentById,
|
||||
findGitProviderById,
|
||||
findMemberById,
|
||||
findProjectById,
|
||||
getApplicationStats,
|
||||
IS_CLOUD,
|
||||
@@ -32,7 +33,7 @@ import {
|
||||
} from "@dokploy/server";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
|
||||
import { nanoid } from "nanoid";
|
||||
import { z } from "zod";
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
@@ -53,6 +54,8 @@ import {
|
||||
apiSaveGitProvider,
|
||||
apiUpdateApplication,
|
||||
applications,
|
||||
environments,
|
||||
projects,
|
||||
} from "@/server/db/schema";
|
||||
import { deploymentWorker } from "@/server/queues/deployments-queue";
|
||||
import type { DeploymentJob } from "@/server/queues/queue-types";
|
||||
@@ -1002,4 +1005,138 @@ export const applicationRouter = createTRPCRouter({
|
||||
message: "Deployment cancellation only available in cloud version",
|
||||
});
|
||||
}),
|
||||
|
||||
search: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
q: z.string().optional(),
|
||||
name: z.string().optional(),
|
||||
appName: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
repository: z.string().optional(),
|
||||
owner: z.string().optional(),
|
||||
dockerImage: z.string().optional(),
|
||||
projectId: z.string().optional(),
|
||||
environmentId: z.string().optional(),
|
||||
limit: z.number().min(1).max(100).default(20),
|
||||
offset: z.number().min(0).default(0),
|
||||
}),
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const baseConditions = [
|
||||
eq(projects.organizationId, ctx.session.activeOrganizationId),
|
||||
];
|
||||
|
||||
if (input.projectId) {
|
||||
baseConditions.push(eq(environments.projectId, input.projectId));
|
||||
}
|
||||
if (input.environmentId) {
|
||||
baseConditions.push(
|
||||
eq(applications.environmentId, input.environmentId),
|
||||
);
|
||||
}
|
||||
|
||||
if (input.q?.trim()) {
|
||||
const term = `%${input.q.trim()}%`;
|
||||
baseConditions.push(
|
||||
or(
|
||||
ilike(applications.name, term),
|
||||
ilike(applications.appName, term),
|
||||
ilike(applications.description ?? "", term),
|
||||
ilike(applications.repository ?? "", term),
|
||||
ilike(applications.owner ?? "", term),
|
||||
ilike(applications.dockerImage ?? "", term),
|
||||
)!,
|
||||
);
|
||||
}
|
||||
|
||||
if (input.name?.trim()) {
|
||||
baseConditions.push(ilike(applications.name, `%${input.name.trim()}%`));
|
||||
}
|
||||
if (input.appName?.trim()) {
|
||||
baseConditions.push(
|
||||
ilike(applications.appName, `%${input.appName.trim()}%`),
|
||||
);
|
||||
}
|
||||
if (input.description?.trim()) {
|
||||
baseConditions.push(
|
||||
ilike(
|
||||
applications.description ?? "",
|
||||
`%${input.description.trim()}%`,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (input.repository?.trim()) {
|
||||
baseConditions.push(
|
||||
ilike(applications.repository ?? "", `%${input.repository.trim()}%`),
|
||||
);
|
||||
}
|
||||
if (input.owner?.trim()) {
|
||||
baseConditions.push(
|
||||
ilike(applications.owner ?? "", `%${input.owner.trim()}%`),
|
||||
);
|
||||
}
|
||||
if (input.dockerImage?.trim()) {
|
||||
baseConditions.push(
|
||||
ilike(
|
||||
applications.dockerImage ?? "",
|
||||
`%${input.dockerImage.trim()}%`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (ctx.user.role === "member") {
|
||||
const { accessedServices } = await findMemberById(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
if (accessedServices.length === 0) return { items: [], total: 0 };
|
||||
baseConditions.push(
|
||||
sql`${applications.applicationId} IN (${sql.join(
|
||||
accessedServices.map((id) => sql`${id}`),
|
||||
sql`, `,
|
||||
)})`,
|
||||
);
|
||||
}
|
||||
|
||||
const where = and(...baseConditions);
|
||||
|
||||
const [items, countResult] = await Promise.all([
|
||||
db
|
||||
.select({
|
||||
applicationId: applications.applicationId,
|
||||
name: applications.name,
|
||||
appName: applications.appName,
|
||||
description: applications.description,
|
||||
environmentId: applications.environmentId,
|
||||
applicationStatus: applications.applicationStatus,
|
||||
sourceType: applications.sourceType,
|
||||
createdAt: applications.createdAt,
|
||||
})
|
||||
.from(applications)
|
||||
.innerJoin(
|
||||
environments,
|
||||
eq(applications.environmentId, environments.environmentId),
|
||||
)
|
||||
.innerJoin(projects, eq(environments.projectId, projects.projectId))
|
||||
.where(where)
|
||||
.orderBy(desc(applications.createdAt))
|
||||
.limit(input.limit)
|
||||
.offset(input.offset),
|
||||
db
|
||||
.select({ count: sql<number>`count(*)::int` })
|
||||
.from(applications)
|
||||
.innerJoin(
|
||||
environments,
|
||||
eq(applications.environmentId, environments.environmentId),
|
||||
)
|
||||
.innerJoin(projects, eq(environments.projectId, projects.projectId))
|
||||
.where(where),
|
||||
]);
|
||||
|
||||
return {
|
||||
items,
|
||||
total: countResult[0]?.count ?? 0,
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
findDomainsByComposeId,
|
||||
findEnvironmentById,
|
||||
findGitProviderById,
|
||||
findMemberById,
|
||||
findProjectById,
|
||||
findServerById,
|
||||
getComposeContainer,
|
||||
@@ -41,7 +42,7 @@ import {
|
||||
} from "@dokploy/server/templates/github";
|
||||
import { processTemplate } from "@dokploy/server/templates/processors";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
|
||||
import _ from "lodash";
|
||||
import { nanoid } from "nanoid";
|
||||
import { parse } from "toml";
|
||||
@@ -58,6 +59,8 @@ import {
|
||||
apiRedeployCompose,
|
||||
apiUpdateCompose,
|
||||
compose as composeTable,
|
||||
environments,
|
||||
projects,
|
||||
} from "@/server/db/schema";
|
||||
import { deploymentWorker } from "@/server/queues/deployments-queue";
|
||||
import type { DeploymentJob } from "@/server/queues/queue-types";
|
||||
@@ -1054,4 +1057,114 @@ export const composeRouter = createTRPCRouter({
|
||||
message: "Deployment cancellation only available in cloud version",
|
||||
});
|
||||
}),
|
||||
|
||||
search: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
q: z.string().optional(),
|
||||
name: z.string().optional(),
|
||||
appName: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
projectId: z.string().optional(),
|
||||
environmentId: z.string().optional(),
|
||||
limit: z.number().min(1).max(100).default(20),
|
||||
offset: z.number().min(0).default(0),
|
||||
}),
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const baseConditions = [
|
||||
eq(projects.organizationId, ctx.session.activeOrganizationId),
|
||||
];
|
||||
|
||||
if (input.projectId) {
|
||||
baseConditions.push(eq(environments.projectId, input.projectId));
|
||||
}
|
||||
if (input.environmentId) {
|
||||
baseConditions.push(
|
||||
eq(composeTable.environmentId, input.environmentId),
|
||||
);
|
||||
}
|
||||
|
||||
if (input.q?.trim()) {
|
||||
const term = `%${input.q.trim()}%`;
|
||||
baseConditions.push(
|
||||
or(
|
||||
ilike(composeTable.name, term),
|
||||
ilike(composeTable.appName, term),
|
||||
ilike(composeTable.description ?? "", term),
|
||||
)!,
|
||||
);
|
||||
}
|
||||
|
||||
if (input.name?.trim()) {
|
||||
baseConditions.push(ilike(composeTable.name, `%${input.name.trim()}%`));
|
||||
}
|
||||
if (input.appName?.trim()) {
|
||||
baseConditions.push(
|
||||
ilike(composeTable.appName, `%${input.appName.trim()}%`),
|
||||
);
|
||||
}
|
||||
if (input.description?.trim()) {
|
||||
baseConditions.push(
|
||||
ilike(
|
||||
composeTable.description ?? "",
|
||||
`%${input.description.trim()}%`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (ctx.user.role === "member") {
|
||||
const { accessedServices } = await findMemberById(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
if (accessedServices.length === 0) return { items: [], total: 0 };
|
||||
baseConditions.push(
|
||||
sql`${composeTable.composeId} IN (${sql.join(
|
||||
accessedServices.map((id) => sql`${id}`),
|
||||
sql`, `,
|
||||
)})`,
|
||||
);
|
||||
}
|
||||
|
||||
const where = and(...baseConditions);
|
||||
|
||||
const [items, countResult] = await Promise.all([
|
||||
db
|
||||
.select({
|
||||
composeId: composeTable.composeId,
|
||||
name: composeTable.name,
|
||||
appName: composeTable.appName,
|
||||
description: composeTable.description,
|
||||
environmentId: composeTable.environmentId,
|
||||
composeStatus: composeTable.composeStatus,
|
||||
sourceType: composeTable.sourceType,
|
||||
createdAt: composeTable.createdAt,
|
||||
})
|
||||
.from(composeTable)
|
||||
.innerJoin(
|
||||
environments,
|
||||
eq(composeTable.environmentId, environments.environmentId),
|
||||
)
|
||||
.innerJoin(projects, eq(environments.projectId, projects.projectId))
|
||||
.where(where)
|
||||
.orderBy(desc(composeTable.createdAt))
|
||||
.limit(input.limit)
|
||||
.offset(input.offset),
|
||||
db
|
||||
.select({ count: sql<number>`count(*)::int` })
|
||||
.from(composeTable)
|
||||
.innerJoin(
|
||||
environments,
|
||||
eq(composeTable.environmentId, environments.environmentId),
|
||||
)
|
||||
.innerJoin(projects, eq(environments.projectId, projects.projectId))
|
||||
.where(where),
|
||||
]);
|
||||
|
||||
return {
|
||||
items,
|
||||
total: countResult[0]?.count ?? 0,
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -11,7 +11,9 @@ import {
|
||||
findMemberById,
|
||||
updateEnvironmentById,
|
||||
} from "@dokploy/server";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import {
|
||||
@@ -21,6 +23,7 @@ import {
|
||||
apiRemoveEnvironment,
|
||||
apiUpdateEnvironment,
|
||||
} from "@/server/db/schema";
|
||||
import { environments, projects } from "@/server/db/schema";
|
||||
|
||||
// Helper function to filter services within an environment based on user permissions
|
||||
const filterEnvironmentServices = (
|
||||
@@ -358,4 +361,92 @@ export const environmentRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
}),
|
||||
|
||||
search: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
q: z.string().optional(),
|
||||
name: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
projectId: z.string().optional(),
|
||||
limit: z.number().min(1).max(100).default(20),
|
||||
offset: z.number().min(0).default(0),
|
||||
}),
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const baseConditions = [
|
||||
eq(projects.organizationId, ctx.session.activeOrganizationId),
|
||||
];
|
||||
|
||||
if (input.projectId) {
|
||||
baseConditions.push(eq(environments.projectId, input.projectId));
|
||||
}
|
||||
|
||||
if (input.q?.trim()) {
|
||||
const term = `%${input.q.trim()}%`;
|
||||
baseConditions.push(
|
||||
or(
|
||||
ilike(environments.name, term),
|
||||
ilike(environments.description ?? "", term),
|
||||
)!,
|
||||
);
|
||||
}
|
||||
|
||||
if (input.name?.trim()) {
|
||||
baseConditions.push(ilike(environments.name, `%${input.name.trim()}%`));
|
||||
}
|
||||
if (input.description?.trim()) {
|
||||
baseConditions.push(
|
||||
ilike(
|
||||
environments.description ?? "",
|
||||
`%${input.description.trim()}%`,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
if (ctx.user.role === "member") {
|
||||
const { accessedEnvironments } = await findMemberById(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
if (accessedEnvironments.length === 0) return { items: [], total: 0 };
|
||||
baseConditions.push(
|
||||
sql`${environments.environmentId} IN (${sql.join(
|
||||
accessedEnvironments.map((id) => sql`${id}`),
|
||||
sql`, `,
|
||||
)})`,
|
||||
);
|
||||
}
|
||||
|
||||
const where = and(...baseConditions);
|
||||
|
||||
const [items, countResult] = await Promise.all([
|
||||
db
|
||||
.select({
|
||||
environmentId: environments.environmentId,
|
||||
name: environments.name,
|
||||
description: environments.description,
|
||||
createdAt: environments.createdAt,
|
||||
env: environments.env,
|
||||
projectId: environments.projectId,
|
||||
isDefault: environments.isDefault,
|
||||
})
|
||||
.from(environments)
|
||||
.innerJoin(projects, eq(environments.projectId, projects.projectId))
|
||||
.where(where)
|
||||
.orderBy(desc(environments.createdAt))
|
||||
.limit(input.limit)
|
||||
.offset(input.offset),
|
||||
db
|
||||
.select({ count: sql<number>`count(*)::int` })
|
||||
.from(environments)
|
||||
.innerJoin(projects, eq(environments.projectId, projects.projectId))
|
||||
.where(where),
|
||||
]);
|
||||
|
||||
return {
|
||||
items,
|
||||
total: countResult[0]?.count ?? 0,
|
||||
};
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -8,6 +8,7 @@ import {
|
||||
findBackupsByDbId,
|
||||
findEnvironmentById,
|
||||
findMariadbById,
|
||||
findMemberById,
|
||||
findProjectById,
|
||||
IS_CLOUD,
|
||||
rebuildDatabase,
|
||||
@@ -22,7 +23,7 @@ import {
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { observable } from "@trpc/server/observable";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import {
|
||||
@@ -37,6 +38,7 @@ import {
|
||||
apiUpdateMariaDB,
|
||||
mariadb as mariadbTable,
|
||||
} from "@/server/db/schema";
|
||||
import { environments, projects } from "@/server/db/schema";
|
||||
import { cancelJobs } from "@/server/utils/backup";
|
||||
export const mariadbRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
@@ -446,4 +448,102 @@ export const mariadbRouter = createTRPCRouter({
|
||||
await rebuildDatabase(mariadb.mariadbId, "mariadb");
|
||||
return true;
|
||||
}),
|
||||
search: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
q: z.string().optional(),
|
||||
name: z.string().optional(),
|
||||
appName: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
projectId: z.string().optional(),
|
||||
environmentId: z.string().optional(),
|
||||
limit: z.number().min(1).max(100).default(20),
|
||||
offset: z.number().min(0).default(0),
|
||||
}),
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const baseConditions = [
|
||||
eq(projects.organizationId, ctx.session.activeOrganizationId),
|
||||
];
|
||||
if (input.projectId) {
|
||||
baseConditions.push(eq(environments.projectId, input.projectId));
|
||||
}
|
||||
if (input.environmentId) {
|
||||
baseConditions.push(
|
||||
eq(mariadbTable.environmentId, input.environmentId),
|
||||
);
|
||||
}
|
||||
if (input.q?.trim()) {
|
||||
const term = `%${input.q.trim()}%`;
|
||||
baseConditions.push(
|
||||
or(
|
||||
ilike(mariadbTable.name, term),
|
||||
ilike(mariadbTable.appName, term),
|
||||
ilike(mariadbTable.description ?? "", term),
|
||||
)!,
|
||||
);
|
||||
}
|
||||
if (input.name?.trim()) {
|
||||
baseConditions.push(ilike(mariadbTable.name, `%${input.name.trim()}%`));
|
||||
}
|
||||
if (input.appName?.trim()) {
|
||||
baseConditions.push(
|
||||
ilike(mariadbTable.appName, `%${input.appName.trim()}%`),
|
||||
);
|
||||
}
|
||||
if (input.description?.trim()) {
|
||||
baseConditions.push(
|
||||
ilike(
|
||||
mariadbTable.description ?? "",
|
||||
`%${input.description.trim()}%`,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (ctx.user.role === "member") {
|
||||
const { accessedServices } = await findMemberById(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
if (accessedServices.length === 0) return { items: [], total: 0 };
|
||||
baseConditions.push(
|
||||
sql`${mariadbTable.mariadbId} IN (${sql.join(
|
||||
accessedServices.map((id) => sql`${id}`),
|
||||
sql`, `,
|
||||
)})`,
|
||||
);
|
||||
}
|
||||
const where = and(...baseConditions);
|
||||
const [items, countResult] = await Promise.all([
|
||||
db
|
||||
.select({
|
||||
mariadbId: mariadbTable.mariadbId,
|
||||
name: mariadbTable.name,
|
||||
appName: mariadbTable.appName,
|
||||
description: mariadbTable.description,
|
||||
environmentId: mariadbTable.environmentId,
|
||||
applicationStatus: mariadbTable.applicationStatus,
|
||||
createdAt: mariadbTable.createdAt,
|
||||
})
|
||||
.from(mariadbTable)
|
||||
.innerJoin(
|
||||
environments,
|
||||
eq(mariadbTable.environmentId, environments.environmentId),
|
||||
)
|
||||
.innerJoin(projects, eq(environments.projectId, projects.projectId))
|
||||
.where(where)
|
||||
.orderBy(desc(mariadbTable.createdAt))
|
||||
.limit(input.limit)
|
||||
.offset(input.offset),
|
||||
db
|
||||
.select({ count: sql<number>`count(*)::int` })
|
||||
.from(mariadbTable)
|
||||
.innerJoin(
|
||||
environments,
|
||||
eq(mariadbTable.environmentId, environments.environmentId),
|
||||
)
|
||||
.innerJoin(projects, eq(environments.projectId, projects.projectId))
|
||||
.where(where),
|
||||
]);
|
||||
return { items, total: countResult[0]?.count ?? 0 };
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
deployMongo,
|
||||
findBackupsByDbId,
|
||||
findEnvironmentById,
|
||||
findMemberById,
|
||||
findMongoById,
|
||||
findProjectById,
|
||||
IS_CLOUD,
|
||||
@@ -21,7 +22,7 @@ import {
|
||||
} from "@dokploy/server";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import {
|
||||
@@ -36,6 +37,7 @@ import {
|
||||
apiUpdateMongo,
|
||||
mongo as mongoTable,
|
||||
} from "@/server/db/schema";
|
||||
import { environments, projects } from "@/server/db/schema";
|
||||
import { cancelJobs } from "@/server/utils/backup";
|
||||
export const mongoRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
@@ -476,4 +478,97 @@ export const mongoRouter = createTRPCRouter({
|
||||
|
||||
return true;
|
||||
}),
|
||||
search: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
q: z.string().optional(),
|
||||
name: z.string().optional(),
|
||||
appName: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
projectId: z.string().optional(),
|
||||
environmentId: z.string().optional(),
|
||||
limit: z.number().min(1).max(100).default(20),
|
||||
offset: z.number().min(0).default(0),
|
||||
}),
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const baseConditions = [
|
||||
eq(projects.organizationId, ctx.session.activeOrganizationId),
|
||||
];
|
||||
if (input.projectId) {
|
||||
baseConditions.push(eq(environments.projectId, input.projectId));
|
||||
}
|
||||
if (input.environmentId) {
|
||||
baseConditions.push(eq(mongoTable.environmentId, input.environmentId));
|
||||
}
|
||||
if (input.q?.trim()) {
|
||||
const term = `%${input.q.trim()}%`;
|
||||
baseConditions.push(
|
||||
or(
|
||||
ilike(mongoTable.name, term),
|
||||
ilike(mongoTable.appName, term),
|
||||
ilike(mongoTable.description ?? "", term),
|
||||
)!,
|
||||
);
|
||||
}
|
||||
if (input.name?.trim()) {
|
||||
baseConditions.push(ilike(mongoTable.name, `%${input.name.trim()}%`));
|
||||
}
|
||||
if (input.appName?.trim()) {
|
||||
baseConditions.push(
|
||||
ilike(mongoTable.appName, `%${input.appName.trim()}%`),
|
||||
);
|
||||
}
|
||||
if (input.description?.trim()) {
|
||||
baseConditions.push(
|
||||
ilike(mongoTable.description ?? "", `%${input.description.trim()}%`),
|
||||
);
|
||||
}
|
||||
if (ctx.user.role === "member") {
|
||||
const { accessedServices } = await findMemberById(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
if (accessedServices.length === 0) return { items: [], total: 0 };
|
||||
baseConditions.push(
|
||||
sql`${mongoTable.mongoId} IN (${sql.join(
|
||||
accessedServices.map((id) => sql`${id}`),
|
||||
sql`, `,
|
||||
)})`,
|
||||
);
|
||||
}
|
||||
const where = and(...baseConditions);
|
||||
const [items, countResult] = await Promise.all([
|
||||
db
|
||||
.select({
|
||||
mongoId: mongoTable.mongoId,
|
||||
name: mongoTable.name,
|
||||
appName: mongoTable.appName,
|
||||
description: mongoTable.description,
|
||||
environmentId: mongoTable.environmentId,
|
||||
applicationStatus: mongoTable.applicationStatus,
|
||||
createdAt: mongoTable.createdAt,
|
||||
})
|
||||
.from(mongoTable)
|
||||
.innerJoin(
|
||||
environments,
|
||||
eq(mongoTable.environmentId, environments.environmentId),
|
||||
)
|
||||
.innerJoin(projects, eq(environments.projectId, projects.projectId))
|
||||
.where(where)
|
||||
.orderBy(desc(mongoTable.createdAt))
|
||||
.limit(input.limit)
|
||||
.offset(input.offset),
|
||||
db
|
||||
.select({ count: sql<number>`count(*)::int` })
|
||||
.from(mongoTable)
|
||||
.innerJoin(
|
||||
environments,
|
||||
eq(mongoTable.environmentId, environments.environmentId),
|
||||
)
|
||||
.innerJoin(projects, eq(environments.projectId, projects.projectId))
|
||||
.where(where),
|
||||
]);
|
||||
return { items, total: countResult[0]?.count ?? 0 };
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
deployMySql,
|
||||
findBackupsByDbId,
|
||||
findEnvironmentById,
|
||||
findMemberById,
|
||||
findMySqlById,
|
||||
findProjectById,
|
||||
IS_CLOUD,
|
||||
@@ -21,7 +22,7 @@ import {
|
||||
} from "@dokploy/server";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import {
|
||||
@@ -34,7 +35,9 @@ import {
|
||||
apiSaveEnvironmentVariablesMySql,
|
||||
apiSaveExternalPortMySql,
|
||||
apiUpdateMySql,
|
||||
environments,
|
||||
mysql as mysqlTable,
|
||||
projects,
|
||||
} from "@/server/db/schema";
|
||||
import { cancelJobs } from "@/server/utils/backup";
|
||||
|
||||
@@ -471,4 +474,97 @@ export const mysqlRouter = createTRPCRouter({
|
||||
|
||||
return true;
|
||||
}),
|
||||
search: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
q: z.string().optional(),
|
||||
name: z.string().optional(),
|
||||
appName: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
projectId: z.string().optional(),
|
||||
environmentId: z.string().optional(),
|
||||
limit: z.number().min(1).max(100).default(20),
|
||||
offset: z.number().min(0).default(0),
|
||||
}),
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const baseConditions = [
|
||||
eq(projects.organizationId, ctx.session.activeOrganizationId),
|
||||
];
|
||||
if (input.projectId) {
|
||||
baseConditions.push(eq(environments.projectId, input.projectId));
|
||||
}
|
||||
if (input.environmentId) {
|
||||
baseConditions.push(eq(mysqlTable.environmentId, input.environmentId));
|
||||
}
|
||||
if (input.q?.trim()) {
|
||||
const term = `%${input.q.trim()}%`;
|
||||
baseConditions.push(
|
||||
or(
|
||||
ilike(mysqlTable.name, term),
|
||||
ilike(mysqlTable.appName, term),
|
||||
ilike(mysqlTable.description ?? "", term),
|
||||
)!,
|
||||
);
|
||||
}
|
||||
if (input.name?.trim()) {
|
||||
baseConditions.push(ilike(mysqlTable.name, `%${input.name.trim()}%`));
|
||||
}
|
||||
if (input.appName?.trim()) {
|
||||
baseConditions.push(
|
||||
ilike(mysqlTable.appName, `%${input.appName.trim()}%`),
|
||||
);
|
||||
}
|
||||
if (input.description?.trim()) {
|
||||
baseConditions.push(
|
||||
ilike(mysqlTable.description ?? "", `%${input.description.trim()}%`),
|
||||
);
|
||||
}
|
||||
if (ctx.user.role === "member") {
|
||||
const { accessedServices } = await findMemberById(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
if (accessedServices.length === 0) return { items: [], total: 0 };
|
||||
baseConditions.push(
|
||||
sql`${mysqlTable.mysqlId} IN (${sql.join(
|
||||
accessedServices.map((id) => sql`${id}`),
|
||||
sql`, `,
|
||||
)})`,
|
||||
);
|
||||
}
|
||||
const where = and(...baseConditions);
|
||||
const [items, countResult] = await Promise.all([
|
||||
db
|
||||
.select({
|
||||
mysqlId: mysqlTable.mysqlId,
|
||||
name: mysqlTable.name,
|
||||
appName: mysqlTable.appName,
|
||||
description: mysqlTable.description,
|
||||
environmentId: mysqlTable.environmentId,
|
||||
applicationStatus: mysqlTable.applicationStatus,
|
||||
createdAt: mysqlTable.createdAt,
|
||||
})
|
||||
.from(mysqlTable)
|
||||
.innerJoin(
|
||||
environments,
|
||||
eq(mysqlTable.environmentId, environments.environmentId),
|
||||
)
|
||||
.innerJoin(projects, eq(environments.projectId, projects.projectId))
|
||||
.where(where)
|
||||
.orderBy(desc(mysqlTable.createdAt))
|
||||
.limit(input.limit)
|
||||
.offset(input.offset),
|
||||
db
|
||||
.select({ count: sql<number>`count(*)::int` })
|
||||
.from(mysqlTable)
|
||||
.innerJoin(
|
||||
environments,
|
||||
eq(mysqlTable.environmentId, environments.environmentId),
|
||||
)
|
||||
.innerJoin(projects, eq(environments.projectId, projects.projectId))
|
||||
.where(where),
|
||||
]);
|
||||
return { items, total: countResult[0]?.count ?? 0 };
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -7,6 +7,7 @@ import {
|
||||
deployPostgres,
|
||||
findBackupsByDbId,
|
||||
findEnvironmentById,
|
||||
findMemberById,
|
||||
findPostgresById,
|
||||
findProjectById,
|
||||
getMountPath,
|
||||
@@ -22,7 +23,7 @@ import {
|
||||
} from "@dokploy/server";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import {
|
||||
@@ -37,6 +38,7 @@ import {
|
||||
apiUpdatePostgres,
|
||||
postgres as postgresTable,
|
||||
} from "@/server/db/schema";
|
||||
import { environments, projects } from "@/server/db/schema";
|
||||
import { cancelJobs } from "@/server/utils/backup";
|
||||
|
||||
export const postgresRouter = createTRPCRouter({
|
||||
@@ -483,4 +485,104 @@ export const postgresRouter = createTRPCRouter({
|
||||
|
||||
return true;
|
||||
}),
|
||||
search: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
q: z.string().optional(),
|
||||
name: z.string().optional(),
|
||||
appName: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
projectId: z.string().optional(),
|
||||
environmentId: z.string().optional(),
|
||||
limit: z.number().min(1).max(100).default(20),
|
||||
offset: z.number().min(0).default(0),
|
||||
}),
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const baseConditions = [
|
||||
eq(projects.organizationId, ctx.session.activeOrganizationId),
|
||||
];
|
||||
if (input.projectId) {
|
||||
baseConditions.push(eq(environments.projectId, input.projectId));
|
||||
}
|
||||
if (input.environmentId) {
|
||||
baseConditions.push(
|
||||
eq(postgresTable.environmentId, input.environmentId),
|
||||
);
|
||||
}
|
||||
if (input.q?.trim()) {
|
||||
const term = `%${input.q.trim()}%`;
|
||||
baseConditions.push(
|
||||
or(
|
||||
ilike(postgresTable.name, term),
|
||||
ilike(postgresTable.appName, term),
|
||||
ilike(postgresTable.description ?? "", term),
|
||||
)!,
|
||||
);
|
||||
}
|
||||
if (input.name?.trim()) {
|
||||
baseConditions.push(
|
||||
ilike(postgresTable.name, `%${input.name.trim()}%`),
|
||||
);
|
||||
}
|
||||
if (input.appName?.trim()) {
|
||||
baseConditions.push(
|
||||
ilike(postgresTable.appName, `%${input.appName.trim()}%`),
|
||||
);
|
||||
}
|
||||
if (input.description?.trim()) {
|
||||
baseConditions.push(
|
||||
ilike(
|
||||
postgresTable.description ?? "",
|
||||
`%${input.description.trim()}%`,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (ctx.user.role === "member") {
|
||||
const { accessedServices } = await findMemberById(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
if (accessedServices.length === 0) return { items: [], total: 0 };
|
||||
baseConditions.push(
|
||||
sql`${postgresTable.postgresId} IN (${sql.join(
|
||||
accessedServices.map((id) => sql`${id}`),
|
||||
sql`, `,
|
||||
)})`,
|
||||
);
|
||||
}
|
||||
const where = and(...baseConditions);
|
||||
const [items, countResult] = await Promise.all([
|
||||
db
|
||||
.select({
|
||||
postgresId: postgresTable.postgresId,
|
||||
name: postgresTable.name,
|
||||
appName: postgresTable.appName,
|
||||
description: postgresTable.description,
|
||||
environmentId: postgresTable.environmentId,
|
||||
applicationStatus: postgresTable.applicationStatus,
|
||||
createdAt: postgresTable.createdAt,
|
||||
})
|
||||
.from(postgresTable)
|
||||
.innerJoin(
|
||||
environments,
|
||||
eq(postgresTable.environmentId, environments.environmentId),
|
||||
)
|
||||
.innerJoin(projects, eq(environments.projectId, projects.projectId))
|
||||
.where(where)
|
||||
.orderBy(desc(postgresTable.createdAt))
|
||||
.limit(input.limit)
|
||||
.offset(input.offset),
|
||||
db
|
||||
.select({ count: sql<number>`count(*)::int` })
|
||||
.from(postgresTable)
|
||||
.innerJoin(
|
||||
environments,
|
||||
eq(postgresTable.environmentId, environments.environmentId),
|
||||
)
|
||||
.innerJoin(projects, eq(environments.projectId, projects.projectId))
|
||||
.where(where),
|
||||
]);
|
||||
return { items, total: countResult[0]?.count ?? 0 };
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -34,10 +34,14 @@ import {
|
||||
} from "@dokploy/server";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { and, desc, eq, sql } from "drizzle-orm";
|
||||
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
|
||||
import type { AnyPgColumn } from "drizzle-orm/pg-core";
|
||||
import { z } from "zod";
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import {
|
||||
adminProcedure,
|
||||
createTRPCRouter,
|
||||
protectedProcedure,
|
||||
} from "@/server/api/trpc";
|
||||
import {
|
||||
apiCreateProject,
|
||||
apiFindOneProject,
|
||||
@@ -219,31 +223,69 @@ export const projectRouter = createTRPCRouter({
|
||||
applications.applicationId,
|
||||
accessedServices,
|
||||
),
|
||||
with: { domains: true },
|
||||
columns: {
|
||||
applicationId: true,
|
||||
name: true,
|
||||
applicationStatus: true,
|
||||
},
|
||||
},
|
||||
mariadb: {
|
||||
where: buildServiceFilter(mariadb.mariadbId, accessedServices),
|
||||
columns: {
|
||||
mariadbId: true,
|
||||
name: true,
|
||||
applicationStatus: true,
|
||||
},
|
||||
},
|
||||
mongo: {
|
||||
where: buildServiceFilter(mongo.mongoId, accessedServices),
|
||||
columns: {
|
||||
mongoId: true,
|
||||
name: true,
|
||||
applicationStatus: true,
|
||||
},
|
||||
},
|
||||
mysql: {
|
||||
where: buildServiceFilter(mysql.mysqlId, accessedServices),
|
||||
columns: {
|
||||
mysqlId: true,
|
||||
name: true,
|
||||
applicationStatus: true,
|
||||
},
|
||||
},
|
||||
postgres: {
|
||||
where: buildServiceFilter(
|
||||
postgres.postgresId,
|
||||
accessedServices,
|
||||
),
|
||||
columns: {
|
||||
postgresId: true,
|
||||
name: true,
|
||||
applicationStatus: true,
|
||||
},
|
||||
},
|
||||
redis: {
|
||||
where: buildServiceFilter(redis.redisId, accessedServices),
|
||||
columns: {
|
||||
redisId: true,
|
||||
name: true,
|
||||
applicationStatus: true,
|
||||
},
|
||||
},
|
||||
compose: {
|
||||
where: buildServiceFilter(compose.composeId, accessedServices),
|
||||
with: { domains: true },
|
||||
columns: {
|
||||
composeId: true,
|
||||
name: true,
|
||||
composeStatus: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
columns: {
|
||||
environmentId: true,
|
||||
isDefault: true,
|
||||
name: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
orderBy: desc(projects.createdAt),
|
||||
@@ -255,21 +297,50 @@ export const projectRouter = createTRPCRouter({
|
||||
environments: {
|
||||
with: {
|
||||
applications: {
|
||||
with: {
|
||||
domains: true,
|
||||
columns: {
|
||||
applicationId: true,
|
||||
name: true,
|
||||
applicationStatus: true,
|
||||
},
|
||||
},
|
||||
mariadb: {
|
||||
columns: {
|
||||
mariadbId: true,
|
||||
},
|
||||
},
|
||||
mongo: {
|
||||
columns: {
|
||||
mongoId: true,
|
||||
},
|
||||
},
|
||||
mysql: {
|
||||
columns: {
|
||||
mysqlId: true,
|
||||
},
|
||||
},
|
||||
postgres: {
|
||||
columns: {
|
||||
postgresId: true,
|
||||
},
|
||||
},
|
||||
redis: {
|
||||
columns: {
|
||||
redisId: true,
|
||||
},
|
||||
},
|
||||
mariadb: true,
|
||||
mongo: true,
|
||||
mysql: true,
|
||||
postgres: true,
|
||||
redis: true,
|
||||
compose: {
|
||||
with: {
|
||||
domains: true,
|
||||
columns: {
|
||||
composeId: true,
|
||||
name: true,
|
||||
composeStatus: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
columns: {
|
||||
name: true,
|
||||
environmentId: true,
|
||||
isDefault: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
where: eq(projects.organizationId, ctx.session.activeOrganizationId),
|
||||
@@ -277,6 +348,183 @@ export const projectRouter = createTRPCRouter({
|
||||
});
|
||||
}),
|
||||
|
||||
/** All projects with full environments and services for the admin permissions UI. Admin only. */
|
||||
allForPermissions: adminProcedure.query(async ({ ctx }) => {
|
||||
return await db.query.projects.findMany({
|
||||
where: eq(projects.organizationId, ctx.session.activeOrganizationId),
|
||||
orderBy: desc(projects.createdAt),
|
||||
columns: {
|
||||
projectId: true,
|
||||
name: true,
|
||||
},
|
||||
with: {
|
||||
environments: {
|
||||
columns: {
|
||||
environmentId: true,
|
||||
name: true,
|
||||
isDefault: true,
|
||||
},
|
||||
with: {
|
||||
applications: {
|
||||
columns: {
|
||||
applicationId: true,
|
||||
appName: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
applicationStatus: true,
|
||||
description: true,
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
mariadb: {
|
||||
columns: {
|
||||
mariadbId: true,
|
||||
appName: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
applicationStatus: true,
|
||||
description: true,
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
postgres: {
|
||||
columns: {
|
||||
postgresId: true,
|
||||
appName: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
applicationStatus: true,
|
||||
description: true,
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
mysql: {
|
||||
columns: {
|
||||
mysqlId: true,
|
||||
appName: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
applicationStatus: true,
|
||||
description: true,
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
mongo: {
|
||||
columns: {
|
||||
mongoId: true,
|
||||
appName: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
applicationStatus: true,
|
||||
description: true,
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
redis: {
|
||||
columns: {
|
||||
redisId: true,
|
||||
appName: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
applicationStatus: true,
|
||||
description: true,
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
compose: {
|
||||
columns: {
|
||||
composeId: true,
|
||||
appName: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
composeStatus: true,
|
||||
description: true,
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
}),
|
||||
|
||||
search: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
q: z.string().optional(),
|
||||
name: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
limit: z.number().min(1).max(100).default(20),
|
||||
offset: z.number().min(0).default(0),
|
||||
}),
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const baseConditions = [
|
||||
eq(projects.organizationId, ctx.session.activeOrganizationId),
|
||||
];
|
||||
|
||||
if (input.q?.trim()) {
|
||||
const term = `%${input.q.trim()}%`;
|
||||
baseConditions.push(
|
||||
or(
|
||||
ilike(projects.name, term),
|
||||
ilike(projects.description ?? "", term),
|
||||
)!,
|
||||
);
|
||||
}
|
||||
|
||||
if (input.name?.trim()) {
|
||||
baseConditions.push(ilike(projects.name, `%${input.name.trim()}%`));
|
||||
}
|
||||
if (input.description?.trim()) {
|
||||
baseConditions.push(
|
||||
ilike(projects.description ?? "", `%${input.description.trim()}%`),
|
||||
);
|
||||
}
|
||||
|
||||
if (ctx.user.role === "member") {
|
||||
const { accessedProjects } = await findMemberById(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
if (accessedProjects.length === 0) return { items: [], total: 0 };
|
||||
baseConditions.push(
|
||||
sql`${projects.projectId} IN (${sql.join(
|
||||
accessedProjects.map((id) => sql`${id}`),
|
||||
sql`, `,
|
||||
)})`,
|
||||
);
|
||||
}
|
||||
|
||||
const where = and(...baseConditions);
|
||||
|
||||
const [items, countResult] = await Promise.all([
|
||||
db.query.projects.findMany({
|
||||
where,
|
||||
limit: input.limit,
|
||||
offset: input.offset,
|
||||
orderBy: desc(projects.createdAt),
|
||||
columns: {
|
||||
projectId: true,
|
||||
name: true,
|
||||
description: true,
|
||||
createdAt: true,
|
||||
organizationId: true,
|
||||
env: true,
|
||||
},
|
||||
}),
|
||||
db
|
||||
.select({ count: sql<number>`count(*)::int` })
|
||||
.from(projects)
|
||||
.where(where),
|
||||
]);
|
||||
|
||||
return {
|
||||
items,
|
||||
total: countResult[0]?.count ?? 0,
|
||||
};
|
||||
}),
|
||||
|
||||
remove: protectedProcedure
|
||||
.input(apiRemoveProject)
|
||||
.mutation(async ({ input, ctx }) => {
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
createRedis,
|
||||
deployRedis,
|
||||
findEnvironmentById,
|
||||
findMemberById,
|
||||
findProjectById,
|
||||
findRedisById,
|
||||
IS_CLOUD,
|
||||
@@ -20,7 +21,7 @@ import {
|
||||
} from "@dokploy/server";
|
||||
import { db } from "@dokploy/server/db";
|
||||
import { TRPCError } from "@trpc/server";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { and, desc, eq, ilike, or, sql } from "drizzle-orm";
|
||||
import { z } from "zod";
|
||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||
import {
|
||||
@@ -35,6 +36,7 @@ import {
|
||||
apiUpdateRedis,
|
||||
redis as redisTable,
|
||||
} from "@/server/db/schema";
|
||||
import { environments, projects } from "@/server/db/schema";
|
||||
export const redisRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.input(apiCreateRedis)
|
||||
@@ -450,4 +452,97 @@ export const redisRouter = createTRPCRouter({
|
||||
await rebuildDatabase(redis.redisId, "redis");
|
||||
return true;
|
||||
}),
|
||||
search: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
q: z.string().optional(),
|
||||
name: z.string().optional(),
|
||||
appName: z.string().optional(),
|
||||
description: z.string().optional(),
|
||||
projectId: z.string().optional(),
|
||||
environmentId: z.string().optional(),
|
||||
limit: z.number().min(1).max(100).default(20),
|
||||
offset: z.number().min(0).default(0),
|
||||
}),
|
||||
)
|
||||
.query(async ({ ctx, input }) => {
|
||||
const baseConditions = [
|
||||
eq(projects.organizationId, ctx.session.activeOrganizationId),
|
||||
];
|
||||
if (input.projectId) {
|
||||
baseConditions.push(eq(environments.projectId, input.projectId));
|
||||
}
|
||||
if (input.environmentId) {
|
||||
baseConditions.push(eq(redisTable.environmentId, input.environmentId));
|
||||
}
|
||||
if (input.q?.trim()) {
|
||||
const term = `%${input.q.trim()}%`;
|
||||
baseConditions.push(
|
||||
or(
|
||||
ilike(redisTable.name, term),
|
||||
ilike(redisTable.appName, term),
|
||||
ilike(redisTable.description ?? "", term),
|
||||
)!,
|
||||
);
|
||||
}
|
||||
if (input.name?.trim()) {
|
||||
baseConditions.push(ilike(redisTable.name, `%${input.name.trim()}%`));
|
||||
}
|
||||
if (input.appName?.trim()) {
|
||||
baseConditions.push(
|
||||
ilike(redisTable.appName, `%${input.appName.trim()}%`),
|
||||
);
|
||||
}
|
||||
if (input.description?.trim()) {
|
||||
baseConditions.push(
|
||||
ilike(redisTable.description ?? "", `%${input.description.trim()}%`),
|
||||
);
|
||||
}
|
||||
if (ctx.user.role === "member") {
|
||||
const { accessedServices } = await findMemberById(
|
||||
ctx.user.id,
|
||||
ctx.session.activeOrganizationId,
|
||||
);
|
||||
if (accessedServices.length === 0) return { items: [], total: 0 };
|
||||
baseConditions.push(
|
||||
sql`${redisTable.redisId} IN (${sql.join(
|
||||
accessedServices.map((id) => sql`${id}`),
|
||||
sql`, `,
|
||||
)})`,
|
||||
);
|
||||
}
|
||||
const where = and(...baseConditions);
|
||||
const [items, countResult] = await Promise.all([
|
||||
db
|
||||
.select({
|
||||
redisId: redisTable.redisId,
|
||||
name: redisTable.name,
|
||||
appName: redisTable.appName,
|
||||
description: redisTable.description,
|
||||
environmentId: redisTable.environmentId,
|
||||
applicationStatus: redisTable.applicationStatus,
|
||||
createdAt: redisTable.createdAt,
|
||||
})
|
||||
.from(redisTable)
|
||||
.innerJoin(
|
||||
environments,
|
||||
eq(redisTable.environmentId, environments.environmentId),
|
||||
)
|
||||
.innerJoin(projects, eq(environments.projectId, projects.projectId))
|
||||
.where(where)
|
||||
.orderBy(desc(redisTable.createdAt))
|
||||
.limit(input.limit)
|
||||
.offset(input.offset),
|
||||
db
|
||||
.select({ count: sql<number>`count(*)::int` })
|
||||
.from(redisTable)
|
||||
.innerJoin(
|
||||
environments,
|
||||
eq(redisTable.environmentId, environments.environmentId),
|
||||
)
|
||||
.innerJoin(projects, eq(environments.projectId, projects.projectId))
|
||||
.where(where),
|
||||
]);
|
||||
return { items, total: countResult[0]?.count ?? 0 };
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -530,12 +530,12 @@ export const settingsRouter = createTRPCRouter({
|
||||
getOpenApiDocument: protectedProcedure.query(
|
||||
async ({ ctx }): Promise<unknown> => {
|
||||
const protocol = ctx.req.headers["x-forwarded-proto"];
|
||||
const url = `${protocol}://${ctx.req.headers.host}/api/trpc`;
|
||||
const url = `${protocol}://${ctx.req.headers.host}/api`;
|
||||
const openApiDocument = generateOpenApiDocument(appRouter, {
|
||||
title: "tRPC OpenAPI",
|
||||
version: packageInfo.version,
|
||||
baseUrl: url,
|
||||
docsUrl: `${url}/trpc/settings.getOpenApiDocument`,
|
||||
docsUrl: `${url}/settings.getOpenApiDocument`,
|
||||
tags: [
|
||||
"admin",
|
||||
"docker",
|
||||
|
||||
@@ -21,9 +21,51 @@ import {
|
||||
STARTUP_PRODUCT_ID,
|
||||
WEBSITE_URL,
|
||||
} from "@/server/utils/stripe";
|
||||
import { adminProcedure, createTRPCRouter } from "../trpc";
|
||||
import { adminProcedure, createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
|
||||
export const stripeRouter = createTRPCRouter({
|
||||
/** Returns the current billing plan for the user's organization. Used to gate features like chat (Startup only). */
|
||||
getCurrentPlan: protectedProcedure.query(async ({ ctx }) => {
|
||||
if (!IS_CLOUD) return null;
|
||||
const owner = await findUserById(ctx.user.ownerId);
|
||||
if (!owner?.stripeCustomerId) return null;
|
||||
|
||||
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!, {
|
||||
apiVersion: "2024-09-30.acacia",
|
||||
});
|
||||
const subscriptions = await stripe.subscriptions.list({
|
||||
customer: owner.stripeCustomerId,
|
||||
status: "active",
|
||||
expand: ["data.items.data.price"],
|
||||
});
|
||||
const activeSub = subscriptions.data[0];
|
||||
if (!activeSub) return null;
|
||||
|
||||
const priceIds = activeSub.items.data.map(
|
||||
(item) => (item.price as Stripe.Price).id,
|
||||
);
|
||||
if (
|
||||
priceIds.some(
|
||||
(id) =>
|
||||
id === STARTUP_BASE_PRICE_MONTHLY_ID ||
|
||||
id === STARTUP_BASE_PRICE_ANNUAL_ID,
|
||||
)
|
||||
) {
|
||||
return "startup" as const;
|
||||
}
|
||||
if (
|
||||
priceIds.some(
|
||||
(id) => id === HOBBY_PRICE_MONTHLY_ID || id === HOBBY_PRICE_ANNUAL_ID,
|
||||
)
|
||||
) {
|
||||
return "hobby" as const;
|
||||
}
|
||||
if (priceIds.some((id) => LEGACY_PRICE_IDS.includes(id))) {
|
||||
return "legacy" as const;
|
||||
}
|
||||
return null;
|
||||
}),
|
||||
|
||||
getProducts: adminProcedure.query(async ({ ctx }) => {
|
||||
const user = await findUserById(ctx.user.ownerId);
|
||||
const stripeCustomerId = user.stripeCustomerId;
|
||||
|
||||
38
apps/dokploy/server/db/index.ts
Normal file
38
apps/dokploy/server/db/index.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import { dbUrl } from "@dokploy/server/db/constants";
|
||||
import * as schema from "@dokploy/server/db/schema";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { drizzle, type PostgresJsDatabase } from "drizzle-orm/postgres-js";
|
||||
import postgres from "postgres";
|
||||
|
||||
export { and, eq };
|
||||
|
||||
type Database = PostgresJsDatabase<typeof schema>;
|
||||
/**
|
||||
* Evita problemas de redeclaración global en monorepos.
|
||||
* No usamos `declare global`.
|
||||
*/
|
||||
const globalForDb = globalThis as unknown as {
|
||||
db?: Database;
|
||||
};
|
||||
|
||||
let dbConnection: Database;
|
||||
|
||||
if (process.env.NODE_ENV === "production") {
|
||||
// En producción no usamos global cache
|
||||
dbConnection = drizzle(postgres(dbUrl), {
|
||||
schema,
|
||||
});
|
||||
} else {
|
||||
// En desarrollo reutilizamos conexión para evitar múltiples conexiones
|
||||
if (!globalForDb.db) {
|
||||
globalForDb.db = drizzle(postgres(dbUrl), {
|
||||
schema,
|
||||
});
|
||||
}
|
||||
|
||||
dbConnection = globalForDb.db;
|
||||
}
|
||||
|
||||
export const db: Database = dbConnection;
|
||||
|
||||
export { dbUrl };
|
||||
@@ -73,23 +73,25 @@ const { handler, api } = betterAuth({
|
||||
disabled: process.env.NODE_ENV === "production",
|
||||
},
|
||||
async trustedOrigins() {
|
||||
const trustedOrigins = await getTrustedOrigins();
|
||||
if (IS_CLOUD) {
|
||||
return trustedOrigins;
|
||||
return getTrustedOrigins();
|
||||
}
|
||||
const settings = await getWebServerSettings();
|
||||
if (!settings) {
|
||||
return [];
|
||||
}
|
||||
return [
|
||||
...(settings?.serverIp ? [`http://${settings?.serverIp}:3000`] : []),
|
||||
...(settings?.host ? [`https://${settings?.host}`] : []),
|
||||
...(process.env.NODE_ENV === "development"
|
||||
const [trustedOrigins, settings] = await Promise.all([
|
||||
getTrustedOrigins(),
|
||||
getWebServerSettings(),
|
||||
]);
|
||||
if (!settings) return [];
|
||||
const devOrigins =
|
||||
process.env.NODE_ENV === "development"
|
||||
? [
|
||||
"http://localhost:3000",
|
||||
"https://absolutely-handy-falcon.ngrok-free.app",
|
||||
]
|
||||
: []),
|
||||
: [];
|
||||
return [
|
||||
...(settings?.serverIp ? [`http://${settings?.serverIp}:3000`] : []),
|
||||
...(settings?.host ? [`https://${settings?.host}`] : []),
|
||||
...devOrigins,
|
||||
...trustedOrigins,
|
||||
];
|
||||
},
|
||||
|
||||
@@ -117,23 +117,33 @@ export const getDokployUrl = async () => {
|
||||
return `http://${settings?.serverIp}:${process.env.PORT}`;
|
||||
};
|
||||
|
||||
export const getTrustedOrigins = async () => {
|
||||
const members = await db.query.member.findMany({
|
||||
where: eq(member.role, "owner"),
|
||||
with: {
|
||||
user: true,
|
||||
},
|
||||
});
|
||||
const TRUSTED_ORIGINS_CACHE_TTL_MS = 30 * 60_000;
|
||||
let trustedOriginsCache: { data: string[]; expiresAt: number } | null = null;
|
||||
|
||||
if (members.length === 0) {
|
||||
return [];
|
||||
export const getTrustedOrigins = async () => {
|
||||
const runQuery = async () => {
|
||||
const rows = await db
|
||||
.select({ trustedOrigins: user.trustedOrigins })
|
||||
.from(member)
|
||||
.innerJoin(user, eq(member.userId, user.id))
|
||||
.where(eq(member.role, "owner"));
|
||||
return Array.from(new Set(rows.flatMap((r) => r.trustedOrigins ?? [])));
|
||||
};
|
||||
|
||||
if (IS_CLOUD) {
|
||||
const now = Date.now();
|
||||
if (trustedOriginsCache && now < trustedOriginsCache.expiresAt) {
|
||||
return trustedOriginsCache.data;
|
||||
}
|
||||
const trustedOrigins = await runQuery();
|
||||
trustedOriginsCache = {
|
||||
data: trustedOrigins,
|
||||
expiresAt: now + TRUSTED_ORIGINS_CACHE_TTL_MS,
|
||||
};
|
||||
return trustedOrigins;
|
||||
}
|
||||
|
||||
const trustedOrigins = members.flatMap(
|
||||
(member) => member.user.trustedOrigins || [],
|
||||
);
|
||||
|
||||
return Array.from(new Set(trustedOrigins));
|
||||
return runQuery();
|
||||
};
|
||||
|
||||
export const getTrustedProviders = async () => {
|
||||
|
||||
@@ -34,42 +34,139 @@ export const createEnvironment = async (
|
||||
export const findEnvironmentById = async (environmentId: string) => {
|
||||
const environment = await db.query.environments.findFirst({
|
||||
where: eq(environments.environmentId, environmentId),
|
||||
columns: {
|
||||
name: true,
|
||||
description: true,
|
||||
environmentId: true,
|
||||
isDefault: true,
|
||||
projectId: true,
|
||||
env: true,
|
||||
},
|
||||
with: {
|
||||
applications: {
|
||||
with: {
|
||||
deployments: true,
|
||||
server: true,
|
||||
server: {
|
||||
columns: {
|
||||
name: true,
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
columns: {
|
||||
name: true,
|
||||
applicationId: true,
|
||||
createdAt: true,
|
||||
applicationStatus: true,
|
||||
description: true,
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
mariadb: {
|
||||
with: {
|
||||
server: true,
|
||||
server: {
|
||||
columns: {
|
||||
name: true,
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
columns: {
|
||||
mariadbId: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
applicationStatus: true,
|
||||
description: true,
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
mongo: {
|
||||
with: {
|
||||
server: true,
|
||||
server: {
|
||||
columns: {
|
||||
name: true,
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
columns: {
|
||||
mongoId: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
applicationStatus: true,
|
||||
description: true,
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
mysql: {
|
||||
with: {
|
||||
server: true,
|
||||
server: {
|
||||
columns: {
|
||||
name: true,
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
columns: {
|
||||
mysqlId: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
applicationStatus: true,
|
||||
description: true,
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
postgres: {
|
||||
with: {
|
||||
server: true,
|
||||
server: {
|
||||
columns: {
|
||||
name: true,
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
columns: {
|
||||
postgresId: true,
|
||||
name: true,
|
||||
description: true,
|
||||
createdAt: true,
|
||||
applicationStatus: true,
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
redis: {
|
||||
with: {
|
||||
server: true,
|
||||
server: {
|
||||
columns: {
|
||||
name: true,
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
columns: {
|
||||
redisId: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
applicationStatus: true,
|
||||
description: true,
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
compose: {
|
||||
with: {
|
||||
deployments: true,
|
||||
server: true,
|
||||
server: {
|
||||
columns: {
|
||||
name: true,
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
columns: {
|
||||
composeId: true,
|
||||
name: true,
|
||||
createdAt: true,
|
||||
composeStatus: true,
|
||||
description: true,
|
||||
serverId: true,
|
||||
},
|
||||
},
|
||||
project: true,
|
||||
@@ -98,6 +195,12 @@ export const findEnvironmentsByProjectId = async (projectId: string) => {
|
||||
compose: true,
|
||||
project: true,
|
||||
},
|
||||
columns: {
|
||||
name: true,
|
||||
description: true,
|
||||
environmentId: true,
|
||||
isDefault: true,
|
||||
},
|
||||
});
|
||||
return projectEnvironments;
|
||||
};
|
||||
@@ -169,6 +272,7 @@ export const duplicateEnvironment = async (
|
||||
name: input.name,
|
||||
description: input.description || originalEnvironment.description,
|
||||
projectId: originalEnvironment.projectId,
|
||||
env: originalEnvironment.env,
|
||||
})
|
||||
.returning()
|
||||
.then((value) => value[0]);
|
||||
|
||||
@@ -281,17 +281,43 @@ const installRequirements = async (
|
||||
.on("error", (err) => {
|
||||
client.end();
|
||||
if (err.level === "client-authentication") {
|
||||
onData?.(
|
||||
`Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`,
|
||||
);
|
||||
const technicalDetail = `Error: ${err.message} ${err.level}`;
|
||||
const friendlyMessage = [
|
||||
"",
|
||||
"❌ Couldn't connect to your server — the SSH key was not accepted.",
|
||||
"",
|
||||
"This usually means the key doesn't match what's on the server, or the key format is invalid.",
|
||||
"",
|
||||
`Technical details: ${technicalDetail}`,
|
||||
"",
|
||||
"💡 Hints:",
|
||||
" • Check that the SSH key you added in Dokploy is the same one installed on the server (e.g. in ~/.ssh/authorized_keys).",
|
||||
" • Try generating a new SSH key in Dokploy and add only the public key to the server, then try again.",
|
||||
" • Make sure to follow the instructions on the Setup Server Button on the SSH Keys tab",
|
||||
].join("\n");
|
||||
onData?.(friendlyMessage);
|
||||
reject(
|
||||
new Error(
|
||||
`Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`,
|
||||
`Authentication failed: Invalid SSH private key. ${technicalDetail}`,
|
||||
),
|
||||
);
|
||||
} else {
|
||||
onData?.(`SSH connection error: ${err.message} ${err.level}`);
|
||||
reject(new Error(`SSH connection error: ${err.message}`));
|
||||
const technicalDetail = `${err.message} ${err.level ?? ""}`.trim();
|
||||
const friendlyMessage = [
|
||||
"",
|
||||
"❌ Couldn't connect to your server.",
|
||||
"",
|
||||
"The connection failed before setup could run. Common causes: wrong IP or port, firewall blocking access, or the server is offline.",
|
||||
"",
|
||||
`Technical details: ${technicalDetail}`,
|
||||
"",
|
||||
"💡 Hints:",
|
||||
" • Check that the server IP address and SSH port are correct and the server is powered on.",
|
||||
" • If the server is in a private network, ensure Dokploy can reach it (VPN, firewall rules, or correct security groups).",
|
||||
" • Make sure the SSH port (usually 22) is open and the SSH service is running on the server.",
|
||||
].join("\n");
|
||||
onData?.(friendlyMessage);
|
||||
reject(new Error(`SSH connection error: ${technicalDetail}`));
|
||||
}
|
||||
})
|
||||
.connect({
|
||||
|
||||
@@ -106,10 +106,6 @@ export const writeDomainsToCompose = async (
|
||||
compose: Compose,
|
||||
domains: Domain[],
|
||||
) => {
|
||||
if (!domains.length) {
|
||||
return "";
|
||||
}
|
||||
|
||||
try {
|
||||
const composeConverted = await addDomainToCompose(compose, domains);
|
||||
const path = getComposePath(compose);
|
||||
@@ -145,7 +141,7 @@ export const addDomainToCompose = async (
|
||||
result = await loadDockerCompose(compose);
|
||||
}
|
||||
|
||||
if (!result || domains.length === 0) {
|
||||
if (!result) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
@@ -201,14 +201,31 @@ export const execAsyncRemote = async (
|
||||
.on("error", (err) => {
|
||||
conn.end();
|
||||
if (err.level === "client-authentication") {
|
||||
const technicalDetail = `Error: ${err.message} ${err.level}`;
|
||||
const friendlyMessage = [
|
||||
"",
|
||||
"❌ Couldn't connect to your server — the SSH key was not accepted.",
|
||||
"",
|
||||
"This usually means the key doesn't match what's on the server, or the key format is invalid.",
|
||||
"",
|
||||
`Technical details: ${technicalDetail}`,
|
||||
"",
|
||||
"💡 Hints:",
|
||||
" • Check that the SSH key you added in Dokploy is the same one installed on the server (e.g. in ~/.ssh/authorized_keys).",
|
||||
" • Try generating a new SSH key in Dokploy and add only the public key to the server, then try again.",
|
||||
" • Make sure to follow the instructions on the Setup Server Button on the SSH Keys tab and then click on deployments tab and check the logs for more details.",
|
||||
].join("\n");
|
||||
const errorMsg = `Authentication failed: Invalid SSH private key. ❌ Error: ${err.message} ${err.level}`;
|
||||
onData?.(errorMsg);
|
||||
onData?.(friendlyMessage);
|
||||
reject(
|
||||
new ExecError(errorMsg, {
|
||||
command,
|
||||
serverId,
|
||||
originalError: err,
|
||||
}),
|
||||
new ExecError(
|
||||
`Authentication failed: Invalid SSH private key. ${friendlyMessage}`,
|
||||
{
|
||||
command,
|
||||
serverId,
|
||||
originalError: err,
|
||||
},
|
||||
),
|
||||
);
|
||||
} else {
|
||||
const errorMsg = `SSH connection error: ${err.message}`;
|
||||
|
||||
10
pnpm-lock.yaml
generated
10
pnpm-lock.yaml
generated
@@ -138,8 +138,8 @@ importers:
|
||||
specifier: workspace:*
|
||||
version: link:../../packages/server
|
||||
'@dokploy/trpc-openapi':
|
||||
specifier: 0.0.16
|
||||
version: 0.0.16(@trpc/server@11.10.0(typescript@5.9.3))(zod-openapi@5.4.6(zod@4.3.6))(zod@4.3.6)
|
||||
specifier: 0.0.17
|
||||
version: 0.0.17(@trpc/server@11.10.0(typescript@5.9.3))(zod-openapi@5.4.6(zod@4.3.6))(zod@4.3.6)
|
||||
'@faker-js/faker':
|
||||
specifier: ^8.4.1
|
||||
version: 8.4.1
|
||||
@@ -1291,8 +1291,8 @@ packages:
|
||||
'@codemirror/view@6.39.15':
|
||||
resolution: {integrity: sha512-aCWjgweIIXLBHh7bY6cACvXuyrZ0xGafjQ2VInjp4RM4gMfscK5uESiNdrH0pE+e1lZr2B4ONGsjchl2KsKZzg==}
|
||||
|
||||
'@dokploy/trpc-openapi@0.0.16':
|
||||
resolution: {integrity: sha512-95pukFwMkSKLfnUB21OnYVwmR832NVD6ewI65ZgpxYxTrmdVGCUzvphqs85fiq4UKV3qcuUSq3nn47d3Sh09zg==}
|
||||
'@dokploy/trpc-openapi@0.0.17':
|
||||
resolution: {integrity: sha512-pXWbqx2W0MoWav/wehEqcXzORLgn7PhnmLsZza1v6+lOSo0Vwuu47PrITbRYKQ2zZcR1nTL18TrgPuMzXK23Iw==}
|
||||
peerDependencies:
|
||||
'@trpc/server': ^11.1.0
|
||||
zod: ^4.3.6
|
||||
@@ -8867,7 +8867,7 @@ snapshots:
|
||||
style-mod: 4.1.3
|
||||
w3c-keyname: 2.2.8
|
||||
|
||||
'@dokploy/trpc-openapi@0.0.16(@trpc/server@11.10.0(typescript@5.9.3))(zod-openapi@5.4.6(zod@4.3.6))(zod@4.3.6)':
|
||||
'@dokploy/trpc-openapi@0.0.17(@trpc/server@11.10.0(typescript@5.9.3))(zod-openapi@5.4.6(zod@4.3.6))(zod@4.3.6)':
|
||||
dependencies:
|
||||
'@trpc/server': 11.10.0(typescript@5.9.3)
|
||||
co-body: 6.2.0
|
||||
|
||||
Reference in New Issue
Block a user