mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-28 09:15:24 +02:00
Compare commits
39 Commits
feat/add-t
...
v0.29.2
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fb6b06f064 | ||
|
|
09824facf8 | ||
|
|
bd46eaec5c | ||
|
|
e9fdc19b96 | ||
|
|
3e81cdac4d | ||
|
|
e72c51444c | ||
|
|
940d18ad25 | ||
|
|
c41b69c925 | ||
|
|
b610f7aeff | ||
|
|
cdd77a04dc | ||
|
|
05f22edfe5 | ||
|
|
29480cde90 | ||
|
|
232ccc9139 | ||
|
|
018e2b153e | ||
|
|
f8c6c8f7cc | ||
|
|
d7af82731c | ||
|
|
c3fa638a56 | ||
|
|
98a586478e | ||
|
|
13248c8d8a | ||
|
|
54417ca8e7 | ||
|
|
598fae0e92 | ||
|
|
b392e58001 | ||
|
|
d9945c0a4f | ||
|
|
f6e2c033ba | ||
|
|
5c787adae1 | ||
|
|
2ba1df1eaa | ||
|
|
e7859395b1 | ||
|
|
6f0ed89ce7 | ||
|
|
4277a509b2 | ||
|
|
f7b576cbf3 | ||
|
|
425fef6e28 | ||
|
|
958372c5f9 | ||
|
|
e7c581476e | ||
|
|
0cae8330e2 | ||
|
|
4a271c11e7 | ||
|
|
fda367b2c5 | ||
|
|
ea1238b1d1 | ||
|
|
eafbd0353e | ||
|
|
91ebf3b6f5 |
36
.github/workflows/sync-version.yml
vendored
36
.github/workflows/sync-version.yml
vendored
@@ -3,6 +3,10 @@ name: Sync version to MCP and CLI repos
|
|||||||
on:
|
on:
|
||||||
release:
|
release:
|
||||||
types: [published]
|
types: [published]
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- 'v*'
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
sync-version:
|
sync-version:
|
||||||
@@ -15,18 +19,14 @@ jobs:
|
|||||||
- name: Get version
|
- name: Get version
|
||||||
id: get_version
|
id: get_version
|
||||||
run: |
|
run: |
|
||||||
VERSION=$(jq -r .version apps/dokploy/package.json)
|
VERSION=$(jq -r .version apps/dokploy/package.json | sed 's/^v//')
|
||||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||||
echo "Version: $VERSION"
|
echo "Version: $VERSION"
|
||||||
|
|
||||||
- name: Sync version to MCP repository
|
- name: Sync version to MCP repository
|
||||||
run: |
|
run: |
|
||||||
git clone https://x-access-token:${{ secrets.DOCS_SYNC_TOKEN }}@github.com/dokploy/mcp.git mcp-repo
|
git clone https://x-access-token:${{ secrets.DOCS_SYNC_TOKEN }}@github.com/dokploy/mcp.git /tmp/mcp-repo
|
||||||
cd mcp-repo
|
cd /tmp/mcp-repo
|
||||||
|
|
||||||
# Bump version
|
|
||||||
jq --arg v "${{ steps.get_version.outputs.version }}" '.version = $v' package.json > package.json.tmp
|
|
||||||
mv package.json.tmp package.json
|
|
||||||
|
|
||||||
# Regenerate tools from latest OpenAPI spec
|
# Regenerate tools from latest OpenAPI spec
|
||||||
npm install -g pnpm
|
npm install -g pnpm
|
||||||
@@ -34,6 +34,10 @@ jobs:
|
|||||||
pnpm run fetch-openapi
|
pnpm run fetch-openapi
|
||||||
pnpm run generate
|
pnpm run generate
|
||||||
|
|
||||||
|
# Bump version after install so pnpm install doesn't overwrite it
|
||||||
|
jq --arg v "${{ steps.get_version.outputs.version }}" '.version = $v' package.json > package.json.tmp
|
||||||
|
mv package.json.tmp package.json
|
||||||
|
|
||||||
git config user.name "Dokploy Bot"
|
git config user.name "Dokploy Bot"
|
||||||
git config user.email "bot@dokploy.com"
|
git config user.email "bot@dokploy.com"
|
||||||
|
|
||||||
@@ -48,22 +52,22 @@ jobs:
|
|||||||
|
|
||||||
- name: Sync version to CLI repository
|
- name: Sync version to CLI repository
|
||||||
run: |
|
run: |
|
||||||
git clone https://x-access-token:${{ secrets.DOCS_SYNC_TOKEN }}@github.com/dokploy/cli.git cli-repo
|
git clone https://x-access-token:${{ secrets.DOCS_SYNC_TOKEN }}@github.com/dokploy/cli.git /tmp/cli-repo
|
||||||
|
|
||||||
cd cli-repo
|
cd /tmp/cli-repo
|
||||||
|
|
||||||
# Bump version
|
# Copy latest openapi spec and regenerate commands
|
||||||
|
cp ${{ github.workspace }}/openapi.json ./openapi.json
|
||||||
|
npm install -g pnpm
|
||||||
|
pnpm install
|
||||||
|
pnpm run generate
|
||||||
|
|
||||||
|
# Bump version after install so pnpm install doesn't overwrite it
|
||||||
if [ -f package.json ]; then
|
if [ -f package.json ]; then
|
||||||
jq --arg v "${{ steps.get_version.outputs.version }}" '.version = $v' package.json > package.json.tmp
|
jq --arg v "${{ steps.get_version.outputs.version }}" '.version = $v' package.json > package.json.tmp
|
||||||
mv package.json.tmp package.json
|
mv package.json.tmp package.json
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Copy latest openapi spec and regenerate commands
|
|
||||||
cp ../openapi.json ./openapi.json
|
|
||||||
npm install -g pnpm
|
|
||||||
pnpm install
|
|
||||||
pnpm run generate
|
|
||||||
|
|
||||||
git config user.name "Dokploy Bot"
|
git config user.name "Dokploy Bot"
|
||||||
git config user.email "bot@dokploy.com"
|
git config user.email "bot@dokploy.com"
|
||||||
|
|
||||||
|
|||||||
@@ -666,7 +666,7 @@ export const AddDomain = ({ id, type, domainId = "", children }: Props) => {
|
|||||||
<div className="space-y-0.5">
|
<div className="space-y-0.5">
|
||||||
<FormLabel>Custom Entrypoint</FormLabel>
|
<FormLabel>Custom Entrypoint</FormLabel>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
Use custom entrypoint for domina
|
Use custom entrypoint for domain
|
||||||
<br />
|
<br />
|
||||||
"web" and/or "websecure" is used by default.
|
"web" and/or "websecure" is used by default.
|
||||||
</FormDescription>
|
</FormDescription>
|
||||||
|
|||||||
291
apps/dokploy/components/dashboard/home/show-home.tsx
Normal file
291
apps/dokploy/components/dashboard/home/show-home.tsx
Normal file
@@ -0,0 +1,291 @@
|
|||||||
|
import { formatDistanceToNow } from "date-fns";
|
||||||
|
import { ArrowRight, Rocket, Server } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useMemo } from "react";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { Card } from "@/components/ui/card";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
|
type DeploymentStatus = "idle" | "running" | "done" | "error";
|
||||||
|
|
||||||
|
const statusDotClass: Record<string, string> = {
|
||||||
|
done: "bg-emerald-500",
|
||||||
|
running: "bg-amber-500",
|
||||||
|
error: "bg-red-500",
|
||||||
|
idle: "bg-muted-foreground/40",
|
||||||
|
};
|
||||||
|
|
||||||
|
function getServiceInfo(d: any) {
|
||||||
|
const app = d.application;
|
||||||
|
const comp = d.compose;
|
||||||
|
const serverName: string =
|
||||||
|
d.server?.name ?? app?.server?.name ?? comp?.server?.name ?? "Dokploy";
|
||||||
|
if (app?.environment?.project && app.environment) {
|
||||||
|
return {
|
||||||
|
name: app.name as string,
|
||||||
|
environment: app.environment.name as string,
|
||||||
|
projectName: app.environment.project.name as string,
|
||||||
|
serverName,
|
||||||
|
href: `/dashboard/project/${app.environment.project.projectId}/environment/${app.environment.environmentId}/services/application/${app.applicationId}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
if (comp?.environment?.project && comp.environment) {
|
||||||
|
return {
|
||||||
|
name: comp.name as string,
|
||||||
|
environment: comp.environment.name as string,
|
||||||
|
projectName: comp.environment.project.name as string,
|
||||||
|
serverName,
|
||||||
|
href: `/dashboard/project/${comp.environment.project.projectId}/environment/${comp.environment.environmentId}/services/compose/${comp.composeId}`,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function StatCard({
|
||||||
|
label,
|
||||||
|
value,
|
||||||
|
delta,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
value: string;
|
||||||
|
delta?: string;
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="rounded-xl border bg-background p-5 min-h-[140px] flex flex-col justify-between">
|
||||||
|
<span className="text-xs uppercase tracking-wider text-muted-foreground">
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
<div className="flex flex-col gap-1">
|
||||||
|
<span className="text-3xl font-semibold tracking-tight">{value}</span>
|
||||||
|
{delta && (
|
||||||
|
<span className="text-xs text-muted-foreground">{delta}</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function StatusListCard({
|
||||||
|
label,
|
||||||
|
items,
|
||||||
|
}: {
|
||||||
|
label: string;
|
||||||
|
items: { dotClass: string; label: string; count: number }[];
|
||||||
|
}) {
|
||||||
|
return (
|
||||||
|
<div className="rounded-xl border bg-background p-5 min-h-[140px] flex flex-col gap-3">
|
||||||
|
<span className="text-xs uppercase tracking-wider text-muted-foreground">
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
<ul className="flex flex-col gap-1.5">
|
||||||
|
{items.map((item) => (
|
||||||
|
<li key={item.label} className="flex items-center gap-2.5 text-sm">
|
||||||
|
<span
|
||||||
|
className={`size-2 rounded-full shrink-0 ${item.dotClass}`}
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
<span className="font-semibold tabular-nums w-8">{item.count}</span>
|
||||||
|
<span className="text-muted-foreground">{item.label}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ShowHome = () => {
|
||||||
|
const { data: auth } = api.user.get.useQuery();
|
||||||
|
const { data: homeStats } = api.project.homeStats.useQuery();
|
||||||
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
|
const canReadDeployments = !!permissions?.deployment.read;
|
||||||
|
const { data: deployments } = api.deployment.allCentralized.useQuery(
|
||||||
|
undefined,
|
||||||
|
{
|
||||||
|
enabled: canReadDeployments,
|
||||||
|
refetchInterval: 10000,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
const firstName = auth?.user?.firstName?.trim();
|
||||||
|
|
||||||
|
const totals = homeStats ?? {
|
||||||
|
projects: 0,
|
||||||
|
environments: 0,
|
||||||
|
applications: 0,
|
||||||
|
compose: 0,
|
||||||
|
databases: 0,
|
||||||
|
services: 0,
|
||||||
|
};
|
||||||
|
const statusBreakdown = homeStats?.status ?? {
|
||||||
|
running: 0,
|
||||||
|
error: 0,
|
||||||
|
idle: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const recentDeployments = useMemo(() => {
|
||||||
|
if (!deployments) return [];
|
||||||
|
return [...deployments]
|
||||||
|
.sort(
|
||||||
|
(a, b) =>
|
||||||
|
new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime(),
|
||||||
|
)
|
||||||
|
.slice(0, 10);
|
||||||
|
}, [deployments]);
|
||||||
|
|
||||||
|
const deployStats = useMemo(() => {
|
||||||
|
const now = Date.now();
|
||||||
|
const weekMs = 7 * 24 * 60 * 60 * 1000;
|
||||||
|
const lastStart = now - weekMs;
|
||||||
|
const prevStart = now - 2 * weekMs;
|
||||||
|
|
||||||
|
const last: NonNullable<typeof deployments> = [];
|
||||||
|
const prev: NonNullable<typeof deployments> = [];
|
||||||
|
for (const d of deployments ?? []) {
|
||||||
|
const t = new Date(d.createdAt).getTime();
|
||||||
|
if (t >= lastStart) last.push(d);
|
||||||
|
else if (t >= prevStart) prev.push(d);
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastCount = last.length;
|
||||||
|
const prevCount = prev.length;
|
||||||
|
let delta: string | undefined;
|
||||||
|
if (prevCount > 0) {
|
||||||
|
const pct = Math.round(((lastCount - prevCount) / prevCount) * 100);
|
||||||
|
delta = `${pct >= 0 ? "+" : ""}${pct}% vs prev 7d`;
|
||||||
|
} else if (lastCount > 0) {
|
||||||
|
delta = "no prior data";
|
||||||
|
} else {
|
||||||
|
delta = "no activity yet";
|
||||||
|
}
|
||||||
|
|
||||||
|
return { value: String(lastCount), delta };
|
||||||
|
}, [deployments]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="w-full">
|
||||||
|
<Card className="h-full bg-sidebar p-2.5 rounded-xl min-h-[85vh]">
|
||||||
|
<div className="rounded-xl bg-background shadow-md p-6 flex flex-col gap-6 h-full">
|
||||||
|
<div className="flex flex-col gap-6 sm:flex-row sm:items-end sm:justify-between">
|
||||||
|
<h1 className="text-3xl font-semibold tracking-tight">
|
||||||
|
{firstName ? `Welcome back, ${firstName}` : "Welcome back"}
|
||||||
|
</h1>
|
||||||
|
<Button asChild variant="secondary" className="w-fit">
|
||||||
|
<Link href="/dashboard/projects">
|
||||||
|
Go to projects
|
||||||
|
<ArrowRight className="size-4" />
|
||||||
|
</Link>
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||||
|
<StatCard
|
||||||
|
label="Projects"
|
||||||
|
value={String(totals.projects)}
|
||||||
|
delta={`${totals.environments} ${totals.environments === 1 ? "environment" : "environments"}`}
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
label="Services"
|
||||||
|
value={String(totals.services)}
|
||||||
|
delta={`${totals.applications} apps · ${totals.compose} compose · ${totals.databases} db`}
|
||||||
|
/>
|
||||||
|
<StatCard
|
||||||
|
label="Deploys / 7d"
|
||||||
|
value={deployStats.value}
|
||||||
|
delta={deployStats.delta}
|
||||||
|
/>
|
||||||
|
<StatusListCard
|
||||||
|
label="Status"
|
||||||
|
items={[
|
||||||
|
{
|
||||||
|
dotClass: "bg-emerald-500",
|
||||||
|
label: "running",
|
||||||
|
count: statusBreakdown.running,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dotClass: "bg-red-500",
|
||||||
|
label: "errored",
|
||||||
|
count: statusBreakdown.error,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
dotClass: "bg-muted-foreground/40",
|
||||||
|
label: "idle",
|
||||||
|
count: statusBreakdown.idle,
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="rounded-xl border bg-background">
|
||||||
|
<div className="flex items-center justify-between px-5 py-4 border-b">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Rocket className="size-4 text-muted-foreground" />
|
||||||
|
<h2 className="text-sm font-semibold">Recent deployments</h2>
|
||||||
|
</div>
|
||||||
|
{canReadDeployments && (
|
||||||
|
<Link
|
||||||
|
href="/dashboard/deployments"
|
||||||
|
className="text-xs text-muted-foreground hover:text-foreground transition-colors"
|
||||||
|
>
|
||||||
|
view all →
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{!canReadDeployments ? (
|
||||||
|
<div className="min-h-[400px] flex flex-col items-center justify-center gap-3 text-center text-sm text-muted-foreground p-10">
|
||||||
|
<Rocket className="size-8 opacity-40" />
|
||||||
|
<span>You do not have permission to view deployments.</span>
|
||||||
|
</div>
|
||||||
|
) : recentDeployments.length === 0 ? (
|
||||||
|
<div className="min-h-[400px] flex flex-col items-center justify-center gap-3 text-center text-sm text-muted-foreground p-10">
|
||||||
|
<Rocket className="size-8 opacity-40" />
|
||||||
|
<span>No deployments yet.</span>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<ul className="divide-y">
|
||||||
|
{recentDeployments.map((d) => {
|
||||||
|
const info = getServiceInfo(d);
|
||||||
|
if (!info) return null;
|
||||||
|
const status = (d.status ?? "idle") as DeploymentStatus;
|
||||||
|
return (
|
||||||
|
<li key={d.deploymentId}>
|
||||||
|
<Link
|
||||||
|
href={info.href}
|
||||||
|
className="flex items-center gap-4 px-5 py-4 hover:bg-muted/40 transition-colors"
|
||||||
|
>
|
||||||
|
<span
|
||||||
|
className={`size-2 rounded-full shrink-0 ${statusDotClass[status] ?? statusDotClass.idle}`}
|
||||||
|
aria-hidden
|
||||||
|
/>
|
||||||
|
<div className="flex flex-col min-w-0 flex-1">
|
||||||
|
<span className="text-sm truncate">{info.name}</span>
|
||||||
|
<span className="text-xs text-muted-foreground truncate">
|
||||||
|
{info.projectName} · {info.environment}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<span className="text-xs text-muted-foreground w-36 hidden lg:flex items-center justify-end gap-1.5 truncate">
|
||||||
|
<Server className="size-3 shrink-0" />
|
||||||
|
<span className="truncate">{info.serverName}</span>
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-muted-foreground w-20 text-right hidden sm:inline">
|
||||||
|
{status}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-muted-foreground w-24 text-right hidden md:inline">
|
||||||
|
{formatDistanceToNow(new Date(d.createdAt), {
|
||||||
|
addSuffix: true,
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
<span className="text-xs text-muted-foreground hover:text-foreground transition-colors">
|
||||||
|
logs →
|
||||||
|
</span>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -166,6 +166,7 @@ export const ShowProjects = () => {
|
|||||||
return (
|
return (
|
||||||
total +
|
total +
|
||||||
(env.applications?.length || 0) +
|
(env.applications?.length || 0) +
|
||||||
|
(env.libsql?.length || 0) +
|
||||||
(env.mariadb?.length || 0) +
|
(env.mariadb?.length || 0) +
|
||||||
(env.mongo?.length || 0) +
|
(env.mongo?.length || 0) +
|
||||||
(env.mysql?.length || 0) +
|
(env.mysql?.length || 0) +
|
||||||
@@ -178,6 +179,7 @@ export const ShowProjects = () => {
|
|||||||
return (
|
return (
|
||||||
total +
|
total +
|
||||||
(env.applications?.length || 0) +
|
(env.applications?.length || 0) +
|
||||||
|
(env.libsql?.length || 0) +
|
||||||
(env.mariadb?.length || 0) +
|
(env.mariadb?.length || 0) +
|
||||||
(env.mongo?.length || 0) +
|
(env.mongo?.length || 0) +
|
||||||
(env.mysql?.length || 0) +
|
(env.mysql?.length || 0) +
|
||||||
|
|||||||
@@ -79,8 +79,11 @@ export const columns: ColumnDef<LogEntry>[] = [
|
|||||||
: log.RequestPath}
|
: log.RequestPath}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-row gap-3 w-full">
|
<div className="flex flex-row gap-3 w-full">
|
||||||
<Badge variant={getStatusColor(log.OriginStatus)}>
|
<Badge
|
||||||
Status: {formatStatusLabel(log.OriginStatus)}
|
variant={getStatusColor(log.OriginStatus || log.DownstreamStatus)}
|
||||||
|
>
|
||||||
|
Status:{" "}
|
||||||
|
{formatStatusLabel(log.OriginStatus || log.DownstreamStatus)}
|
||||||
</Badge>
|
</Badge>
|
||||||
<Badge variant={"secondary"}>
|
<Badge variant={"secondary"}>
|
||||||
Exec Time: {formatDuration(log.Duration)}
|
Exec Time: {formatDuration(log.Duration)}
|
||||||
|
|||||||
@@ -185,7 +185,7 @@ export const RequestsTable = ({ dateRange }: RequestsTableProps) => {
|
|||||||
<div className="flex flex-col gap-4 w-full overflow-auto">
|
<div className="flex flex-col gap-4 w-full overflow-auto">
|
||||||
<div className="flex items-center gap-2 max-sm:flex-wrap">
|
<div className="flex items-center gap-2 max-sm:flex-wrap">
|
||||||
<Input
|
<Input
|
||||||
placeholder="Filter by name..."
|
placeholder="Filter by hostname..."
|
||||||
value={search}
|
value={search}
|
||||||
onChange={(event) => setSearch(event.target.value)}
|
onChange={(event) => setSearch(event.target.value)}
|
||||||
className="md:max-w-sm"
|
className="md:max-w-sm"
|
||||||
|
|||||||
@@ -167,7 +167,7 @@ export const SearchCommand = () => {
|
|||||||
<CommandGroup heading={"Application"} hidden={true}>
|
<CommandGroup heading={"Application"} hidden={true}>
|
||||||
<CommandItem
|
<CommandItem
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
router.push("/dashboard/projects");
|
router.push("/dashboard/home");
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -425,7 +425,7 @@ export const WelcomeSubscription = () => {
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (stepper.isLast) {
|
if (stepper.isLast) {
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
push("/dashboard/projects");
|
push("/dashboard/home");
|
||||||
} else {
|
} else {
|
||||||
stepper.next();
|
stepper.next();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import {
|
|||||||
Forward,
|
Forward,
|
||||||
GalleryVerticalEnd,
|
GalleryVerticalEnd,
|
||||||
GitBranch,
|
GitBranch,
|
||||||
|
House,
|
||||||
Key,
|
Key,
|
||||||
KeyRound,
|
KeyRound,
|
||||||
Loader2,
|
Loader2,
|
||||||
@@ -148,6 +149,12 @@ type Menu = {
|
|||||||
// The `isEnabled` function is called to determine if the item should be displayed
|
// The `isEnabled` function is called to determine if the item should be displayed
|
||||||
const MENU: Menu = {
|
const MENU: Menu = {
|
||||||
home: [
|
home: [
|
||||||
|
{
|
||||||
|
isSingle: true,
|
||||||
|
title: "Home",
|
||||||
|
url: "/dashboard/home",
|
||||||
|
icon: House,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
isSingle: true,
|
isSingle: true,
|
||||||
title: "Projects",
|
title: "Projects",
|
||||||
|
|||||||
@@ -80,7 +80,7 @@ export const UserNav = () => {
|
|||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
className="cursor-pointer"
|
className="cursor-pointer"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
router.push("/dashboard/projects");
|
router.push("/dashboard/home");
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Projects
|
Projects
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ export function SignInWithSSO({ children }: SignInWithSSOProps) {
|
|||||||
try {
|
try {
|
||||||
const { data, error } = await authClient.signIn.sso({
|
const { data, error } = await authClient.signIn.sso({
|
||||||
email: values.email,
|
email: values.email,
|
||||||
callbackURL: "/dashboard/projects",
|
callbackURL: "/dashboard/home",
|
||||||
});
|
});
|
||||||
if (error) {
|
if (error) {
|
||||||
toast.error(error.message ?? "Failed to sign in with SSO");
|
toast.error(error.message ?? "Failed to sign in with SSO");
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dokploy",
|
"name": "dokploy",
|
||||||
"version": "v0.29.0",
|
"version": "v0.29.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -147,7 +147,7 @@
|
|||||||
"shell-quote": "^1.8.1",
|
"shell-quote": "^1.8.1",
|
||||||
"slugify": "^1.6.6",
|
"slugify": "^1.6.6",
|
||||||
"sonner": "^1.7.4",
|
"sonner": "^1.7.4",
|
||||||
"ssh2": "1.15.0",
|
"ssh2": "~1.16.0",
|
||||||
"stripe": "17.2.0",
|
"stripe": "17.2.0",
|
||||||
"superjson": "^2.2.2",
|
"superjson": "^2.2.2",
|
||||||
"swagger-ui-react": "^5.31.2",
|
"swagger-ui-react": "^5.31.2",
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ export default function Custom404({ statusCode, error }: Props) {
|
|||||||
|
|
||||||
<div className="mt-5 flex flex-col justify-center items-center gap-2 sm:flex-row sm:gap-3">
|
<div className="mt-5 flex flex-col justify-center items-center gap-2 sm:flex-row sm:gap-3">
|
||||||
<Link
|
<Link
|
||||||
href="/dashboard/projects"
|
href="/dashboard/home"
|
||||||
className={buttonVariants({
|
className={buttonVariants({
|
||||||
variant: "secondary",
|
variant: "secondary",
|
||||||
className: "flex flex-row gap-2",
|
className: "flex flex-row gap-2",
|
||||||
|
|||||||
@@ -12,6 +12,15 @@ import type { DeploymentJob } from "@/server/queues/queue-types";
|
|||||||
import { myQueue } from "@/server/queues/queueSetup";
|
import { myQueue } from "@/server/queues/queueSetup";
|
||||||
import { deploy } from "@/server/utils/deploy";
|
import { deploy } from "@/server/utils/deploy";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Log a webhook handler error server-side without leaking its shape to the HTTP
|
||||||
|
* response. Drizzle errors carry the raw SQL query, column list and parameters,
|
||||||
|
* so we never forward the error object to the client.
|
||||||
|
*/
|
||||||
|
export const logWebhookError = (context: string, error: unknown) => {
|
||||||
|
console.error(context, error);
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper function to get package_version from registry_package events
|
* Helper function to get package_version from registry_package events
|
||||||
*/
|
*/
|
||||||
@@ -262,14 +271,15 @@ export default async function handler(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(400).json({ message: "Error deploying Application", error });
|
logWebhookError("Error deploying Application:", error);
|
||||||
|
res.status(400).json({ message: "Error deploying Application" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(200).json({ message: "Application deployed successfully" });
|
res.status(200).json({ message: "Application deployed successfully" });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
logWebhookError("Error deploying Application:", error);
|
||||||
res.status(400).json({ message: "Error deploying Application", error });
|
res.status(400).json({ message: "Error deploying Application" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import {
|
|||||||
extractCommittedPaths,
|
extractCommittedPaths,
|
||||||
extractHash,
|
extractHash,
|
||||||
getProviderByHeader,
|
getProviderByHeader,
|
||||||
|
logWebhookError,
|
||||||
} from "../[refreshToken]";
|
} from "../[refreshToken]";
|
||||||
|
|
||||||
export default async function handler(
|
export default async function handler(
|
||||||
@@ -195,13 +196,14 @@ export default async function handler(
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(400).json({ message: "Error deploying Compose", error });
|
logWebhookError("Error deploying Compose:", error);
|
||||||
|
res.status(400).json({ message: "Error deploying Compose" });
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
res.status(200).json({ message: "Compose deployed successfully" });
|
res.status(200).json({ message: "Compose deployed successfully" });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
logWebhookError("Error deploying Compose:", error);
|
||||||
res.status(400).json({ message: "Error deploying Compose", error });
|
res.status(400).json({ message: "Error deploying Compose" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -17,7 +17,11 @@ import { applications, compose, github } from "@/server/db/schema";
|
|||||||
import type { DeploymentJob } from "@/server/queues/queue-types";
|
import type { DeploymentJob } from "@/server/queues/queue-types";
|
||||||
import { myQueue } from "@/server/queues/queueSetup";
|
import { myQueue } from "@/server/queues/queueSetup";
|
||||||
import { deploy } from "@/server/utils/deploy";
|
import { deploy } from "@/server/utils/deploy";
|
||||||
import { extractCommitMessage, extractHash } from "./[refreshToken]";
|
import {
|
||||||
|
extractCommitMessage,
|
||||||
|
extractHash,
|
||||||
|
logWebhookError,
|
||||||
|
} from "./[refreshToken]";
|
||||||
|
|
||||||
export default async function handler(
|
export default async function handler(
|
||||||
req: NextApiRequest,
|
req: NextApiRequest,
|
||||||
@@ -197,10 +201,8 @@ export default async function handler(
|
|||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error("Error deploying applications on tag:", error);
|
logWebhookError("Error deploying applications on tag:", error);
|
||||||
res
|
res.status(400).json({ message: "Error deploying applications on tag" });
|
||||||
.status(400)
|
|
||||||
.json({ message: "Error deploying applications on tag", error });
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -322,7 +324,8 @@ export default async function handler(
|
|||||||
}
|
}
|
||||||
res.status(200).json({ message: `Deployed ${totalApps} apps` });
|
res.status(200).json({ message: `Deployed ${totalApps} apps` });
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
res.status(400).json({ message: "Error deploying Application", error });
|
logWebhookError("Error deploying Application:", error);
|
||||||
|
res.status(400).json({ message: "Error deploying Application" });
|
||||||
}
|
}
|
||||||
} else if (req.headers["x-github-event"] === "pull_request") {
|
} else if (req.headers["x-github-event"] === "pull_request") {
|
||||||
const prId = githubBody?.pull_request?.id;
|
const prId = githubBody?.pull_request?.id;
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
|||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: false,
|
||||||
destination: "/dashboard/projects",
|
destination: "/dashboard/home",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export async function getServerSideProps(
|
|||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
destination: "/dashboard/projects",
|
destination: "/dashboard/home",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
53
apps/dokploy/pages/dashboard/home.tsx
Normal file
53
apps/dokploy/pages/dashboard/home.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import { validateRequest } from "@dokploy/server/lib/auth";
|
||||||
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
|
import type { GetServerSidePropsContext } from "next";
|
||||||
|
import type { ReactElement } from "react";
|
||||||
|
import superjson from "superjson";
|
||||||
|
import { ShowHome } from "@/components/dashboard/home/show-home";
|
||||||
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
|
import { appRouter } from "@/server/api/root";
|
||||||
|
|
||||||
|
const Home = () => {
|
||||||
|
return <ShowHome />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Home;
|
||||||
|
|
||||||
|
Home.getLayout = (page: ReactElement) => {
|
||||||
|
return <DashboardLayout>{page}</DashboardLayout>;
|
||||||
|
};
|
||||||
|
|
||||||
|
export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
||||||
|
const { req, res } = ctx;
|
||||||
|
const { user, session } = await validateRequest(req);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
permanent: true,
|
||||||
|
destination: "/",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const helpers = createServerSideHelpers({
|
||||||
|
router: appRouter,
|
||||||
|
ctx: {
|
||||||
|
req: req as any,
|
||||||
|
res: res as any,
|
||||||
|
db: null as any,
|
||||||
|
session: session as any,
|
||||||
|
user: user as any,
|
||||||
|
},
|
||||||
|
transformer: superjson,
|
||||||
|
});
|
||||||
|
|
||||||
|
await helpers.settings.isCloud.prefetch();
|
||||||
|
await helpers.user.get.prefetch();
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
trpcState: helpers.dehydrate(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -96,7 +96,7 @@ export async function getServerSideProps(
|
|||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
destination: "/dashboard/projects",
|
destination: "/dashboard/home",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -122,7 +122,7 @@ export async function getServerSideProps(
|
|||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: false,
|
||||||
destination: "/dashboard/projects",
|
destination: "/dashboard/home",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -509,6 +509,14 @@ const EnvironmentPage = (
|
|||||||
deploy: api.mongo.deploy.useMutation(),
|
deploy: api.mongo.deploy.useMutation(),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const libsqlActions = {
|
||||||
|
start: api.libsql.start.useMutation(),
|
||||||
|
stop: api.libsql.stop.useMutation(),
|
||||||
|
move: api.libsql.move.useMutation(),
|
||||||
|
delete: api.libsql.remove.useMutation(),
|
||||||
|
deploy: api.libsql.deploy.useMutation(),
|
||||||
|
};
|
||||||
|
|
||||||
const handleBulkStart = async () => {
|
const handleBulkStart = async () => {
|
||||||
let success = 0;
|
let success = 0;
|
||||||
setIsBulkActionLoading(true);
|
setIsBulkActionLoading(true);
|
||||||
@@ -541,6 +549,9 @@ const EnvironmentPage = (
|
|||||||
case "mongo":
|
case "mongo":
|
||||||
await mongoActions.start.mutateAsync({ mongoId: serviceId });
|
await mongoActions.start.mutateAsync({ mongoId: serviceId });
|
||||||
break;
|
break;
|
||||||
|
case "libsql":
|
||||||
|
await libsqlActions.start.mutateAsync({ libsqlId: serviceId });
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
success++;
|
success++;
|
||||||
} catch {
|
} catch {
|
||||||
@@ -588,6 +599,9 @@ const EnvironmentPage = (
|
|||||||
case "mongo":
|
case "mongo":
|
||||||
await mongoActions.stop.mutateAsync({ mongoId: serviceId });
|
await mongoActions.stop.mutateAsync({ mongoId: serviceId });
|
||||||
break;
|
break;
|
||||||
|
case "libsql":
|
||||||
|
await libsqlActions.stop.mutateAsync({ libsqlId: serviceId });
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
success++;
|
success++;
|
||||||
} catch {
|
} catch {
|
||||||
@@ -664,6 +678,12 @@ const EnvironmentPage = (
|
|||||||
targetEnvironmentId: selectedTargetEnvironment,
|
targetEnvironmentId: selectedTargetEnvironment,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
case "libsql":
|
||||||
|
await libsqlActions.move.mutateAsync({
|
||||||
|
libsqlId: serviceId,
|
||||||
|
targetEnvironmentId: selectedTargetEnvironment,
|
||||||
|
});
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
await utils.environment.one.invalidate({
|
await utils.environment.one.invalidate({
|
||||||
environmentId,
|
environmentId,
|
||||||
@@ -733,6 +753,11 @@ const EnvironmentPage = (
|
|||||||
mongoId: serviceId,
|
mongoId: serviceId,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
case "libsql":
|
||||||
|
await libsqlActions.delete.mutateAsync({
|
||||||
|
libsqlId: serviceId,
|
||||||
|
});
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
await utils.environment.one.invalidate({
|
await utils.environment.one.invalidate({
|
||||||
environmentId,
|
environmentId,
|
||||||
@@ -799,6 +824,11 @@ const EnvironmentPage = (
|
|||||||
mongoId: serviceId,
|
mongoId: serviceId,
|
||||||
});
|
});
|
||||||
break;
|
break;
|
||||||
|
case "libsql":
|
||||||
|
await libsqlActions.deploy.mutateAsync({
|
||||||
|
libsqlId: serviceId,
|
||||||
|
});
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
success++;
|
success++;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1856,7 +1886,7 @@ export async function getServerSideProps(
|
|||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: false,
|
||||||
destination: "/dashboard/projects",
|
destination: "/dashboard/home",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -490,7 +490,7 @@ export async function getServerSideProps(
|
|||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: false,
|
||||||
destination: "/dashboard/projects",
|
destination: "/dashboard/home",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -492,7 +492,7 @@ export async function getServerSideProps(
|
|||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: false,
|
||||||
destination: "/dashboard/projects",
|
destination: "/dashboard/home",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -343,7 +343,7 @@ export async function getServerSideProps(
|
|||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: false,
|
||||||
destination: "/dashboard/projects",
|
destination: "/dashboard/home",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -372,7 +372,7 @@ export async function getServerSideProps(
|
|||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: false,
|
||||||
destination: "/dashboard/projects",
|
destination: "/dashboard/home",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -376,7 +376,7 @@ export async function getServerSideProps(
|
|||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: false,
|
||||||
destination: "/dashboard/projects",
|
destination: "/dashboard/home",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -353,7 +353,7 @@ export async function getServerSideProps(
|
|||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: false,
|
||||||
destination: "/dashboard/projects",
|
destination: "/dashboard/home",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -360,7 +360,7 @@ export async function getServerSideProps(
|
|||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: false,
|
||||||
destination: "/dashboard/projects",
|
destination: "/dashboard/home",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -363,7 +363,7 @@ export async function getServerSideProps(
|
|||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: false,
|
permanent: false,
|
||||||
destination: "/dashboard/projects",
|
destination: "/dashboard/home",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ export async function getServerSideProps(
|
|||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
destination: "/dashboard/projects",
|
destination: "/dashboard/home",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export async function getServerSideProps(
|
|||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
destination: "/dashboard/projects",
|
destination: "/dashboard/home",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export async function getServerSideProps(
|
|||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
destination: "/dashboard/projects",
|
destination: "/dashboard/home",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export async function getServerSideProps(
|
|||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
destination: "/dashboard/projects",
|
destination: "/dashboard/home",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export async function getServerSideProps(
|
|||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
destination: "/dashboard/projects",
|
destination: "/dashboard/home",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ export async function getServerSideProps(
|
|||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
destination: "/dashboard/projects",
|
destination: "/dashboard/home",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ export async function getServerSideProps(
|
|||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
destination: "/dashboard/projects",
|
destination: "/dashboard/home",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ export async function getServerSideProps(
|
|||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
destination: "/dashboard/projects",
|
destination: "/dashboard/home",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,6 +82,16 @@ export default function Home({ IS_CLOUD }: Props) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (error) {
|
if (error) {
|
||||||
|
const isEmailNotVerified =
|
||||||
|
error.code === "EMAIL_NOT_VERIFIED" ||
|
||||||
|
error.message?.toLowerCase().includes("email not verified");
|
||||||
|
if (isEmailNotVerified) {
|
||||||
|
const msg =
|
||||||
|
"Your email is not verified. We've sent a new verification link to your email.";
|
||||||
|
toast.info(msg);
|
||||||
|
setError(msg);
|
||||||
|
return;
|
||||||
|
}
|
||||||
toast.error(error.message);
|
toast.error(error.message);
|
||||||
setError(error.message || "An error occurred while logging in");
|
setError(error.message || "An error occurred while logging in");
|
||||||
return;
|
return;
|
||||||
@@ -96,7 +106,7 @@ export default function Home({ IS_CLOUD }: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toast.success("Logged in successfully");
|
toast.success("Logged in successfully");
|
||||||
router.push("/dashboard/projects");
|
router.push("/dashboard/home");
|
||||||
} catch {
|
} catch {
|
||||||
toast.error("An error occurred while logging in");
|
toast.error("An error occurred while logging in");
|
||||||
} finally {
|
} finally {
|
||||||
@@ -123,7 +133,7 @@ export default function Home({ IS_CLOUD }: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toast.success("Logged in successfully");
|
toast.success("Logged in successfully");
|
||||||
router.push("/dashboard/projects");
|
router.push("/dashboard/home");
|
||||||
} catch {
|
} catch {
|
||||||
toast.error("An error occurred while verifying 2FA code");
|
toast.error("An error occurred while verifying 2FA code");
|
||||||
} finally {
|
} finally {
|
||||||
@@ -153,7 +163,7 @@ export default function Home({ IS_CLOUD }: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
toast.success("Logged in successfully");
|
toast.success("Logged in successfully");
|
||||||
router.push("/dashboard/projects");
|
router.push("/dashboard/home");
|
||||||
} catch {
|
} catch {
|
||||||
toast.error("An error occurred while verifying backup code");
|
toast.error("An error occurred while verifying backup code");
|
||||||
} finally {
|
} finally {
|
||||||
@@ -398,7 +408,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
destination: "/dashboard/projects",
|
destination: "/dashboard/home",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -427,7 +437,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
destination: "/dashboard/projects",
|
destination: "/dashboard/home",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ const Invitation = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
toast.success("Account created successfully");
|
toast.success("Account created successfully");
|
||||||
router.push("/dashboard/projects");
|
router.push("/dashboard/home");
|
||||||
} catch {
|
} catch {
|
||||||
toast.error("An error occurred while creating your account");
|
toast.error("An error occurred while creating your account");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -303,7 +303,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
permanent: true,
|
permanent: true,
|
||||||
destination: "/dashboard/projects",
|
destination: "/dashboard/home",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -458,9 +458,26 @@ export const backupRouter = createTRPCRouter({
|
|||||||
serverId: z.string().optional(),
|
serverId: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
const destination = await findDestinationById(input.destinationId);
|
const destination = await findDestinationById(input.destinationId);
|
||||||
|
if (destination.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You don't have access to this destination.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (input.serverId) {
|
||||||
|
const targetServer = await findServerById(input.serverId);
|
||||||
|
if (
|
||||||
|
targetServer.organizationId !== ctx.session.activeOrganizationId
|
||||||
|
) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You don't have access to this server.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
const rcloneFlags = getS3Credentials(destination);
|
const rcloneFlags = getS3Credentials(destination);
|
||||||
const bucketPath = `:s3:${destination.bucket}`;
|
const bucketPath = `:s3:${destination.bucket}`;
|
||||||
|
|
||||||
|
|||||||
@@ -18,7 +18,16 @@ export const clusterRouter = createTRPCRouter({
|
|||||||
serverId: z.string().optional(),
|
serverId: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
|
if (input.serverId) {
|
||||||
|
const targetServer = await findServerById(input.serverId);
|
||||||
|
if (targetServer.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You don't have access to this server.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
const docker = await getRemoteDocker(input.serverId);
|
const docker = await getRemoteDocker(input.serverId);
|
||||||
const workers: DockerNode[] = await docker.listNodes();
|
const workers: DockerNode[] = await docker.listNodes();
|
||||||
return workers;
|
return workers;
|
||||||
@@ -32,6 +41,15 @@ export const clusterRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
if (input.serverId) {
|
||||||
|
const targetServer = await findServerById(input.serverId);
|
||||||
|
if (targetServer.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You don't have access to this server.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
try {
|
try {
|
||||||
const drainCommand = `docker node update --availability drain ${input.nodeId}`;
|
const drainCommand = `docker node update --availability drain ${input.nodeId}`;
|
||||||
const removeCommand = `docker node rm ${input.nodeId} --force`;
|
const removeCommand = `docker node rm ${input.nodeId} --force`;
|
||||||
@@ -65,7 +83,16 @@ export const clusterRouter = createTRPCRouter({
|
|||||||
serverId: z.string().optional(),
|
serverId: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
|
if (input.serverId) {
|
||||||
|
const targetServer = await findServerById(input.serverId);
|
||||||
|
if (targetServer.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You don't have access to this server.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
const docker = await getRemoteDocker(input.serverId);
|
const docker = await getRemoteDocker(input.serverId);
|
||||||
const result = await docker.swarmInspect();
|
const result = await docker.swarmInspect();
|
||||||
const docker_version = await docker.version();
|
const docker_version = await docker.version();
|
||||||
@@ -88,7 +115,16 @@ export const clusterRouter = createTRPCRouter({
|
|||||||
serverId: z.string().optional(),
|
serverId: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
|
if (input.serverId) {
|
||||||
|
const targetServer = await findServerById(input.serverId);
|
||||||
|
if (targetServer.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You don't have access to this server.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
const docker = await getRemoteDocker(input.serverId);
|
const docker = await getRemoteDocker(input.serverId);
|
||||||
const result = await docker.swarmInspect();
|
const result = await docker.swarmInspect();
|
||||||
const docker_version = await docker.version();
|
const docker_version = await docker.version();
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import {
|
|||||||
checkServicePermissionAndAccess,
|
checkServicePermissionAndAccess,
|
||||||
findMemberByUserId,
|
findMemberByUserId,
|
||||||
} from "@dokploy/server/services/permission";
|
} from "@dokploy/server/services/permission";
|
||||||
|
import { findServerById } from "@dokploy/server/services/server";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { desc, eq } from "drizzle-orm";
|
import { desc, eq } from "drizzle-orm";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -52,7 +53,14 @@ export const deploymentRouter = createTRPCRouter({
|
|||||||
}),
|
}),
|
||||||
allByServer: withPermission("deployment", "read")
|
allByServer: withPermission("deployment", "read")
|
||||||
.input(apiFindAllByServer)
|
.input(apiFindAllByServer)
|
||||||
.query(async ({ input }) => {
|
.query(async ({ input, ctx }) => {
|
||||||
|
const targetServer = await findServerById(input.serverId);
|
||||||
|
if (targetServer.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You don't have access to this server.",
|
||||||
|
});
|
||||||
|
}
|
||||||
return await findAllDeploymentsByServerId(input.serverId);
|
return await findAllDeploymentsByServerId(input.serverId);
|
||||||
}),
|
}),
|
||||||
allCentralized: withPermission("deployment", "read").query(
|
allCentralized: withPermission("deployment", "read").query(
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { db } from "@dokploy/server/db";
|
import { db } from "@dokploy/server/db";
|
||||||
import { IS_CLOUD } from "@dokploy/server/index";
|
import { IS_CLOUD, sendInvitationEmail } from "@dokploy/server/index";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { and, desc, eq, exists } from "drizzle-orm";
|
import { and, desc, eq, exists } from "drizzle-orm";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
@@ -325,6 +325,24 @@ export const organizationRouter = createTRPCRouter({
|
|||||||
})
|
})
|
||||||
.returning();
|
.returning();
|
||||||
|
|
||||||
|
if (IS_CLOUD && created) {
|
||||||
|
const host =
|
||||||
|
process.env.NODE_ENV === "development"
|
||||||
|
? "http://localhost:3000"
|
||||||
|
: "https://app.dokploy.com";
|
||||||
|
const inviteLink = `${host}/invitation?token=${created.id}`;
|
||||||
|
|
||||||
|
const org = await db.query.organization.findFirst({
|
||||||
|
where: eq(organization.id, orgId),
|
||||||
|
});
|
||||||
|
|
||||||
|
await sendInvitationEmail({
|
||||||
|
email,
|
||||||
|
inviteLink,
|
||||||
|
organizationName: org?.name || "organization",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
await audit(ctx, {
|
await audit(ctx, {
|
||||||
action: "create",
|
action: "create",
|
||||||
resourceType: "organization",
|
resourceType: "organization",
|
||||||
|
|||||||
@@ -487,6 +487,148 @@ export const projectRouter = createTRPCRouter({
|
|||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
|
||||||
|
homeStats: protectedProcedure.query(async ({ ctx }) => {
|
||||||
|
const isPrivileged = ctx.user.role === "owner" || ctx.user.role === "admin";
|
||||||
|
|
||||||
|
let accessedProjects: string[] = [];
|
||||||
|
let accessedEnvironments: string[] = [];
|
||||||
|
let accessedServices: string[] = [];
|
||||||
|
|
||||||
|
if (!isPrivileged) {
|
||||||
|
const member = await findMemberByUserId(
|
||||||
|
ctx.user.id,
|
||||||
|
ctx.session.activeOrganizationId,
|
||||||
|
);
|
||||||
|
accessedProjects = member.accessedProjects;
|
||||||
|
accessedEnvironments = member.accessedEnvironments;
|
||||||
|
accessedServices = member.accessedServices;
|
||||||
|
|
||||||
|
if (accessedProjects.length === 0) {
|
||||||
|
return {
|
||||||
|
projects: 0,
|
||||||
|
environments: 0,
|
||||||
|
applications: 0,
|
||||||
|
compose: 0,
|
||||||
|
databases: 0,
|
||||||
|
services: 0,
|
||||||
|
status: { running: 0, error: 0, idle: 0 },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const projectIdFilter = isPrivileged
|
||||||
|
? eq(projects.organizationId, ctx.session.activeOrganizationId)
|
||||||
|
: and(
|
||||||
|
sql`${projects.projectId} IN (${sql.join(
|
||||||
|
accessedProjects.map((id) => sql`${id}`),
|
||||||
|
sql`, `,
|
||||||
|
)})`,
|
||||||
|
eq(projects.organizationId, ctx.session.activeOrganizationId),
|
||||||
|
);
|
||||||
|
|
||||||
|
const environmentFilter = isPrivileged
|
||||||
|
? undefined
|
||||||
|
: accessedEnvironments.length === 0
|
||||||
|
? sql`false`
|
||||||
|
: sql`${environments.environmentId} IN (${sql.join(
|
||||||
|
accessedEnvironments.map((envId) => sql`${envId}`),
|
||||||
|
sql`, `,
|
||||||
|
)})`;
|
||||||
|
|
||||||
|
const applyFilter = (col: AnyPgColumn) =>
|
||||||
|
isPrivileged ? undefined : buildServiceFilter(col, accessedServices);
|
||||||
|
|
||||||
|
const rows = await db.query.projects.findMany({
|
||||||
|
where: projectIdFilter,
|
||||||
|
columns: { projectId: true },
|
||||||
|
with: {
|
||||||
|
environments: {
|
||||||
|
where: environmentFilter,
|
||||||
|
columns: { environmentId: true },
|
||||||
|
with: {
|
||||||
|
applications: {
|
||||||
|
where: applyFilter(applications.applicationId),
|
||||||
|
columns: { applicationStatus: true },
|
||||||
|
},
|
||||||
|
compose: {
|
||||||
|
where: applyFilter(compose.composeId),
|
||||||
|
columns: { composeStatus: true },
|
||||||
|
},
|
||||||
|
libsql: {
|
||||||
|
where: applyFilter(libsql.libsqlId),
|
||||||
|
columns: { applicationStatus: true },
|
||||||
|
},
|
||||||
|
mariadb: {
|
||||||
|
where: applyFilter(mariadb.mariadbId),
|
||||||
|
columns: { applicationStatus: true },
|
||||||
|
},
|
||||||
|
mongo: {
|
||||||
|
where: applyFilter(mongo.mongoId),
|
||||||
|
columns: { applicationStatus: true },
|
||||||
|
},
|
||||||
|
mysql: {
|
||||||
|
where: applyFilter(mysql.mysqlId),
|
||||||
|
columns: { applicationStatus: true },
|
||||||
|
},
|
||||||
|
postgres: {
|
||||||
|
where: applyFilter(postgres.postgresId),
|
||||||
|
columns: { applicationStatus: true },
|
||||||
|
},
|
||||||
|
redis: {
|
||||||
|
where: applyFilter(redis.redisId),
|
||||||
|
columns: { applicationStatus: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
let applicationsCount = 0;
|
||||||
|
let composeCount = 0;
|
||||||
|
let databasesCount = 0;
|
||||||
|
let environmentsCount = 0;
|
||||||
|
const status = { running: 0, error: 0, idle: 0 };
|
||||||
|
const bump = (s?: string | null) => {
|
||||||
|
if (s === "done") status.running++;
|
||||||
|
else if (s === "error") status.error++;
|
||||||
|
else status.idle++;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (const project of rows) {
|
||||||
|
for (const env of project.environments) {
|
||||||
|
environmentsCount++;
|
||||||
|
applicationsCount += env.applications.length;
|
||||||
|
composeCount += env.compose.length;
|
||||||
|
databasesCount +=
|
||||||
|
env.libsql.length +
|
||||||
|
env.mariadb.length +
|
||||||
|
env.mongo.length +
|
||||||
|
env.mysql.length +
|
||||||
|
env.postgres.length +
|
||||||
|
env.redis.length;
|
||||||
|
|
||||||
|
for (const a of env.applications) bump(a.applicationStatus);
|
||||||
|
for (const c of env.compose) bump(c.composeStatus);
|
||||||
|
for (const s of env.libsql) bump(s.applicationStatus);
|
||||||
|
for (const s of env.mariadb) bump(s.applicationStatus);
|
||||||
|
for (const s of env.mongo) bump(s.applicationStatus);
|
||||||
|
for (const s of env.mysql) bump(s.applicationStatus);
|
||||||
|
for (const s of env.postgres) bump(s.applicationStatus);
|
||||||
|
for (const s of env.redis) bump(s.applicationStatus);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
projects: rows.length,
|
||||||
|
environments: environmentsCount,
|
||||||
|
applications: applicationsCount,
|
||||||
|
compose: composeCount,
|
||||||
|
databases: databasesCount,
|
||||||
|
services: applicationsCount + composeCount + databasesCount,
|
||||||
|
status,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
|
||||||
search: protectedProcedure
|
search: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
|
|||||||
@@ -7,19 +7,25 @@ import {
|
|||||||
updateScheduleSchema,
|
updateScheduleSchema,
|
||||||
} from "@dokploy/server/db/schema/schedule";
|
} from "@dokploy/server/db/schema/schedule";
|
||||||
import { runCommand } from "@dokploy/server/index";
|
import { runCommand } from "@dokploy/server/index";
|
||||||
import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission";
|
import {
|
||||||
|
checkPermission,
|
||||||
|
checkServicePermissionAndAccess,
|
||||||
|
findMemberByUserId,
|
||||||
|
} from "@dokploy/server/services/permission";
|
||||||
import {
|
import {
|
||||||
createSchedule,
|
createSchedule,
|
||||||
deleteSchedule,
|
deleteSchedule,
|
||||||
findScheduleById,
|
findScheduleById,
|
||||||
updateSchedule,
|
updateSchedule,
|
||||||
} from "@dokploy/server/services/schedule";
|
} from "@dokploy/server/services/schedule";
|
||||||
|
import { findServerById } from "@dokploy/server/services/server";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { asc, desc, eq } from "drizzle-orm";
|
import { asc, desc, eq } from "drizzle-orm";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { audit } from "@/server/api/utils/audit";
|
import { audit } from "@/server/api/utils/audit";
|
||||||
import { removeJob, schedule } from "@/server/utils/backup";
|
import { removeJob, schedule } from "@/server/utils/backup";
|
||||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||||
|
|
||||||
export const scheduleRouter = createTRPCRouter({
|
export const scheduleRouter = createTRPCRouter({
|
||||||
create: protectedProcedure
|
create: protectedProcedure
|
||||||
.input(createScheduleSchema)
|
.input(createScheduleSchema)
|
||||||
@@ -29,6 +35,45 @@ export const scheduleRouter = createTRPCRouter({
|
|||||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||||
schedule: ["create"],
|
schedule: ["create"],
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
if (input.scheduleType === "dokploy-server" && IS_CLOUD) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "FORBIDDEN",
|
||||||
|
message:
|
||||||
|
"Host-level schedules are not available in the cloud version.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await checkPermission(ctx, { schedule: ["create"] });
|
||||||
|
|
||||||
|
if (
|
||||||
|
input.scheduleType === "server" ||
|
||||||
|
input.scheduleType === "dokploy-server"
|
||||||
|
) {
|
||||||
|
const member = await findMemberByUserId(
|
||||||
|
ctx.user.id,
|
||||||
|
ctx.session.activeOrganizationId,
|
||||||
|
);
|
||||||
|
if (member.role !== "owner" && member.role !== "admin") {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "FORBIDDEN",
|
||||||
|
message:
|
||||||
|
"Only owners and admins can manage server-level schedules.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (input.scheduleType === "server" && input.serverId) {
|
||||||
|
const targetServer = await findServerById(input.serverId);
|
||||||
|
if (
|
||||||
|
targetServer.organizationId !== ctx.session.activeOrganizationId
|
||||||
|
) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You don't have access to this server.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const newSchedule = await createSchedule(input);
|
const newSchedule = await createSchedule(input);
|
||||||
|
|
||||||
@@ -57,12 +102,77 @@ export const scheduleRouter = createTRPCRouter({
|
|||||||
.input(updateScheduleSchema)
|
.input(updateScheduleSchema)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
const existingSchedule = await findScheduleById(input.scheduleId);
|
const existingSchedule = await findScheduleById(input.scheduleId);
|
||||||
|
|
||||||
|
if (
|
||||||
|
IS_CLOUD &&
|
||||||
|
input.scheduleType &&
|
||||||
|
input.scheduleType !== existingSchedule.scheduleType
|
||||||
|
) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "FORBIDDEN",
|
||||||
|
message: "Changing scheduleType is not allowed in the cloud version.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
const serviceId =
|
const serviceId =
|
||||||
existingSchedule.applicationId || existingSchedule.composeId;
|
existingSchedule.applicationId || existingSchedule.composeId;
|
||||||
if (serviceId) {
|
if (serviceId) {
|
||||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||||
schedule: ["update"],
|
schedule: ["update"],
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
if (existingSchedule.scheduleType === "dokploy-server" && IS_CLOUD) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "FORBIDDEN",
|
||||||
|
message:
|
||||||
|
"Host-level schedules are not available in the cloud version.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await checkPermission(ctx, { schedule: ["update"] });
|
||||||
|
|
||||||
|
if (
|
||||||
|
existingSchedule.scheduleType === "server" ||
|
||||||
|
existingSchedule.scheduleType === "dokploy-server"
|
||||||
|
) {
|
||||||
|
const member = await findMemberByUserId(
|
||||||
|
ctx.user.id,
|
||||||
|
ctx.session.activeOrganizationId,
|
||||||
|
);
|
||||||
|
if (member.role !== "owner" && member.role !== "admin") {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "FORBIDDEN",
|
||||||
|
message:
|
||||||
|
"Only owners and admins can manage server-level schedules.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
existingSchedule.scheduleType === "server" &&
|
||||||
|
existingSchedule.serverId
|
||||||
|
) {
|
||||||
|
const targetServer = await findServerById(existingSchedule.serverId);
|
||||||
|
if (
|
||||||
|
targetServer.organizationId !== ctx.session.activeOrganizationId
|
||||||
|
) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You don't have access to this server.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
existingSchedule.scheduleType === "dokploy-server" &&
|
||||||
|
existingSchedule.userId &&
|
||||||
|
existingSchedule.userId !== ctx.user.id
|
||||||
|
) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You can only manage your own host-level schedules.",
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const updatedSchedule = await updateSchedule(input);
|
const updatedSchedule = await updateSchedule(input);
|
||||||
|
|
||||||
@@ -107,6 +217,56 @@ export const scheduleRouter = createTRPCRouter({
|
|||||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||||
schedule: ["delete"],
|
schedule: ["delete"],
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
if (scheduleItem.scheduleType === "dokploy-server" && IS_CLOUD) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "FORBIDDEN",
|
||||||
|
message:
|
||||||
|
"Host-level schedules are not available in the cloud version.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await checkPermission(ctx, { schedule: ["delete"] });
|
||||||
|
|
||||||
|
if (
|
||||||
|
scheduleItem.scheduleType === "server" ||
|
||||||
|
scheduleItem.scheduleType === "dokploy-server"
|
||||||
|
) {
|
||||||
|
const member = await findMemberByUserId(
|
||||||
|
ctx.user.id,
|
||||||
|
ctx.session.activeOrganizationId,
|
||||||
|
);
|
||||||
|
if (member.role !== "owner" && member.role !== "admin") {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "FORBIDDEN",
|
||||||
|
message:
|
||||||
|
"Only owners and admins can manage server-level schedules.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scheduleItem.scheduleType === "server" && scheduleItem.serverId) {
|
||||||
|
const targetServer = await findServerById(scheduleItem.serverId);
|
||||||
|
if (
|
||||||
|
targetServer.organizationId !== ctx.session.activeOrganizationId
|
||||||
|
) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You don't have access to this server.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
scheduleItem.scheduleType === "dokploy-server" &&
|
||||||
|
scheduleItem.userId &&
|
||||||
|
scheduleItem.userId !== ctx.user.id
|
||||||
|
) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You can only manage your own host-level schedules.",
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
await deleteSchedule(input.scheduleId);
|
await deleteSchedule(input.scheduleId);
|
||||||
|
|
||||||
@@ -148,6 +308,30 @@ export const scheduleRouter = createTRPCRouter({
|
|||||||
await checkServicePermissionAndAccess(ctx, input.id, {
|
await checkServicePermissionAndAccess(ctx, input.id, {
|
||||||
schedule: ["read"],
|
schedule: ["read"],
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
await checkPermission(ctx, { schedule: ["read"] });
|
||||||
|
|
||||||
|
if (input.scheduleType === "server") {
|
||||||
|
const targetServer = await findServerById(input.id);
|
||||||
|
if (
|
||||||
|
targetServer.organizationId !== ctx.session.activeOrganizationId
|
||||||
|
) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You don't have access to this server.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
input.scheduleType === "dokploy-server" &&
|
||||||
|
input.id !== ctx.user.id
|
||||||
|
) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You can only list your own host-level schedules.",
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
const where = {
|
const where = {
|
||||||
application: eq(schedules.applicationId, input.id),
|
application: eq(schedules.applicationId, input.id),
|
||||||
@@ -178,6 +362,31 @@ export const scheduleRouter = createTRPCRouter({
|
|||||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||||
schedule: ["read"],
|
schedule: ["read"],
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
await checkPermission(ctx, { schedule: ["read"] });
|
||||||
|
|
||||||
|
if (schedule.scheduleType === "server" && schedule.serverId) {
|
||||||
|
const targetServer = await findServerById(schedule.serverId);
|
||||||
|
if (
|
||||||
|
targetServer.organizationId !== ctx.session.activeOrganizationId
|
||||||
|
) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You don't have access to this schedule.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
schedule.scheduleType === "dokploy-server" &&
|
||||||
|
schedule.userId &&
|
||||||
|
schedule.userId !== ctx.user.id
|
||||||
|
) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You don't have access to this schedule.",
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return schedule;
|
return schedule;
|
||||||
}),
|
}),
|
||||||
@@ -191,6 +400,56 @@ export const scheduleRouter = createTRPCRouter({
|
|||||||
await checkServicePermissionAndAccess(ctx, serviceId, {
|
await checkServicePermissionAndAccess(ctx, serviceId, {
|
||||||
schedule: ["create"],
|
schedule: ["create"],
|
||||||
});
|
});
|
||||||
|
} else {
|
||||||
|
if (scheduleItem.scheduleType === "dokploy-server" && IS_CLOUD) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "FORBIDDEN",
|
||||||
|
message:
|
||||||
|
"Host-level schedules are not available in the cloud version.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await checkPermission(ctx, { schedule: ["create"] });
|
||||||
|
|
||||||
|
if (
|
||||||
|
scheduleItem.scheduleType === "server" ||
|
||||||
|
scheduleItem.scheduleType === "dokploy-server"
|
||||||
|
) {
|
||||||
|
const member = await findMemberByUserId(
|
||||||
|
ctx.user.id,
|
||||||
|
ctx.session.activeOrganizationId,
|
||||||
|
);
|
||||||
|
if (member.role !== "owner" && member.role !== "admin") {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "FORBIDDEN",
|
||||||
|
message:
|
||||||
|
"Only owners and admins can manage server-level schedules.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scheduleItem.scheduleType === "server" && scheduleItem.serverId) {
|
||||||
|
const targetServer = await findServerById(scheduleItem.serverId);
|
||||||
|
if (
|
||||||
|
targetServer.organizationId !== ctx.session.activeOrganizationId
|
||||||
|
) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You don't have access to this server.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
scheduleItem.scheduleType === "dokploy-server" &&
|
||||||
|
scheduleItem.userId &&
|
||||||
|
scheduleItem.userId !== ctx.user.id
|
||||||
|
) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You can only manage your own host-level schedules.",
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await runCommand(input.scheduleId);
|
await runCommand(input.scheduleId);
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ import {
|
|||||||
getWebServerSettings,
|
getWebServerSettings,
|
||||||
IS_CLOUD,
|
IS_CLOUD,
|
||||||
removeUserById,
|
removeUserById,
|
||||||
|
renderInvitationEmail,
|
||||||
sendEmailNotification,
|
sendEmailNotification,
|
||||||
sendResendNotification,
|
sendResendNotification,
|
||||||
updateUser,
|
updateUser,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { db } from "@dokploy/server/db";
|
import { db } from "@dokploy/server/db";
|
||||||
import { hasValidLicense } from "@dokploy/server/services/proprietary/license-key";
|
|
||||||
import {
|
import {
|
||||||
account,
|
account,
|
||||||
apiAssignPermissions,
|
apiAssignPermissions,
|
||||||
@@ -29,6 +29,7 @@ import {
|
|||||||
hasPermission,
|
hasPermission,
|
||||||
resolvePermissions,
|
resolvePermissions,
|
||||||
} from "@dokploy/server/services/permission";
|
} from "@dokploy/server/services/permission";
|
||||||
|
import { hasValidLicense } from "@dokploy/server/services/proprietary/license-key";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import * as bcrypt from "bcrypt";
|
import * as bcrypt from "bcrypt";
|
||||||
import { and, asc, eq, gt } from "drizzle-orm";
|
import { and, asc, eq, gt } from "drizzle-orm";
|
||||||
@@ -639,27 +640,26 @@ export const userRouter = createTRPCRouter({
|
|||||||
);
|
);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const htmlContent = `
|
const toEmail = currentInvitation?.email || "";
|
||||||
\t\t\t\t<p>You are invited to join ${organization?.name || "organization"} on Dokploy. Click the link to accept the invitation: <a href="${inviteLink}">Accept Invitation</a></p>
|
const orgName = organization?.name || "organization";
|
||||||
\t\t\t\t`;
|
const subject = `You've been invited to join ${orgName} on Dokploy`;
|
||||||
|
const html = await renderInvitationEmail({
|
||||||
|
email: toEmail,
|
||||||
|
inviteLink,
|
||||||
|
organizationName: orgName,
|
||||||
|
});
|
||||||
|
|
||||||
if (email) {
|
if (email) {
|
||||||
await sendEmailNotification(
|
await sendEmailNotification(
|
||||||
{
|
{ ...email, toAddresses: [toEmail] },
|
||||||
...email,
|
subject,
|
||||||
toAddresses: [currentInvitation?.email || ""],
|
html,
|
||||||
},
|
|
||||||
"Invitation to join organization",
|
|
||||||
htmlContent,
|
|
||||||
);
|
);
|
||||||
} else if (resend) {
|
} else if (resend) {
|
||||||
await sendResendNotification(
|
await sendResendNotification(
|
||||||
{
|
{ ...resend, toAddresses: [toEmail] },
|
||||||
...resend,
|
subject,
|
||||||
toAddresses: [currentInvitation?.email || ""],
|
html,
|
||||||
},
|
|
||||||
"Invitation to join organization",
|
|
||||||
htmlContent,
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -15,7 +15,9 @@ import {
|
|||||||
updateVolumeBackupSchema,
|
updateVolumeBackupSchema,
|
||||||
volumeBackups,
|
volumeBackups,
|
||||||
} from "@dokploy/server/db/schema";
|
} from "@dokploy/server/db/schema";
|
||||||
|
import { findDestinationById } from "@dokploy/server/services/destination";
|
||||||
import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission";
|
import { checkServicePermissionAndAccess } from "@dokploy/server/services/permission";
|
||||||
|
import { findServerById } from "@dokploy/server/services/server";
|
||||||
import {
|
import {
|
||||||
execAsyncRemote,
|
execAsyncRemote,
|
||||||
execAsyncStream,
|
execAsyncStream,
|
||||||
@@ -265,7 +267,23 @@ export const volumeBackupsRouter = createTRPCRouter({
|
|||||||
serverId: z.string().optional(),
|
serverId: z.string().optional(),
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
.subscription(async ({ input }) => {
|
.subscription(async ({ input, ctx }) => {
|
||||||
|
const destination = await findDestinationById(input.destinationId);
|
||||||
|
if (destination.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You don't have access to this destination.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (input.serverId) {
|
||||||
|
const targetServer = await findServerById(input.serverId);
|
||||||
|
if (targetServer.organizationId !== ctx.session.activeOrganizationId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "UNAUTHORIZED",
|
||||||
|
message: "You don't have access to this server.",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
return observable<string>((emit) => {
|
return observable<string>((emit) => {
|
||||||
const runRestore = async () => {
|
const runRestore = async () => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -85,6 +85,11 @@ export const setupDockerContainerLogsWebSocketServer = (
|
|||||||
if (serverId) {
|
if (serverId) {
|
||||||
const server = await findServerById(serverId);
|
const server = await findServerById(serverId);
|
||||||
|
|
||||||
|
if (server.organizationId !== session.activeOrganizationId) {
|
||||||
|
ws.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!server.sshKeyId) return;
|
if (!server.sshKeyId) return;
|
||||||
const client = new Client();
|
const client = new Client();
|
||||||
client
|
client
|
||||||
|
|||||||
@@ -61,6 +61,12 @@ export const setupDockerContainerTerminalWebSocketServer = (
|
|||||||
try {
|
try {
|
||||||
if (serverId) {
|
if (serverId) {
|
||||||
const server = await findServerById(serverId);
|
const server = await findServerById(serverId);
|
||||||
|
|
||||||
|
if (server.organizationId !== session.activeOrganizationId) {
|
||||||
|
ws.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!server.sshKeyId)
|
if (!server.sshKeyId)
|
||||||
throw new Error("No SSH key available for this server");
|
throw new Error("No SSH key available for this server");
|
||||||
|
|
||||||
|
|||||||
@@ -57,6 +57,11 @@ export const setupDeploymentLogsWebSocketServer = (
|
|||||||
if (serverId) {
|
if (serverId) {
|
||||||
const server = await findServerById(serverId);
|
const server = await findServerById(serverId);
|
||||||
|
|
||||||
|
if (server.organizationId !== session.activeOrganizationId) {
|
||||||
|
ws.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
if (!server.sshKeyId) {
|
if (!server.sshKeyId) {
|
||||||
ws.close();
|
ws.close();
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -154,6 +154,11 @@ export const setupTerminalWebSocketServer = (
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (server.organizationId !== session.activeOrganizationId) {
|
||||||
|
ws.close();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const { ipAddress: host, port, username, sshKey, sshKeyId } = server;
|
const { ipAddress: host, port, username, sshKey, sshKeyId } = server;
|
||||||
|
|
||||||
if (!sshKeyId) {
|
if (!sshKeyId) {
|
||||||
|
|||||||
@@ -80,7 +80,7 @@
|
|||||||
"semver": "7.7.3",
|
"semver": "7.7.3",
|
||||||
"shell-quote": "^1.8.1",
|
"shell-quote": "^1.8.1",
|
||||||
"slugify": "^1.6.6",
|
"slugify": "^1.6.6",
|
||||||
"ssh2": "1.15.0",
|
"ssh2": "~1.16.0",
|
||||||
"toml": "3.0.0",
|
"toml": "3.0.0",
|
||||||
"ws": "8.16.0",
|
"ws": "8.16.0",
|
||||||
"yaml": "2.8.1",
|
"yaml": "2.8.1",
|
||||||
|
|||||||
@@ -14,21 +14,18 @@ import {
|
|||||||
Text,
|
Text,
|
||||||
} from "@react-email/components";
|
} from "@react-email/components";
|
||||||
|
|
||||||
export type TemplateProps = {
|
interface InvitationEmailProps {
|
||||||
email: string;
|
|
||||||
name: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
interface VercelInviteUserEmailProps {
|
|
||||||
inviteLink: string;
|
inviteLink: string;
|
||||||
toEmail: string;
|
toEmail: string;
|
||||||
|
organizationName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const InvitationEmail = ({
|
export const InvitationEmail = ({
|
||||||
inviteLink,
|
inviteLink,
|
||||||
toEmail,
|
toEmail,
|
||||||
}: VercelInviteUserEmailProps) => {
|
organizationName = "an organization",
|
||||||
const previewText = "Join to Dokploy";
|
}: InvitationEmailProps) => {
|
||||||
|
const previewText = `You've been invited to join ${organizationName} on Dokploy`;
|
||||||
return (
|
return (
|
||||||
<Html>
|
<Html>
|
||||||
<Head />
|
<Head />
|
||||||
@@ -44,50 +41,67 @@ export const InvitationEmail = ({
|
|||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Body className="bg-white my-auto mx-auto font-sans px-2">
|
<Body className="bg-[#f4f4f5] my-auto mx-auto font-sans">
|
||||||
<Container className="border border-solid border-[#eaeaea] rounded-lg my-[40px] mx-auto p-[20px] max-w-[465px]">
|
<Container className="my-[40px] mx-auto max-w-[520px]">
|
||||||
<Section className="mt-[32px]">
|
{/* Header */}
|
||||||
|
<Section className="bg-[#09090b] rounded-t-xl px-[40px] py-[32px] text-center">
|
||||||
<Img
|
<Img
|
||||||
src={
|
src="https://raw.githubusercontent.com/Dokploy/website/refs/heads/main/apps/docs/public/logo-dokploy-blackpng.png"
|
||||||
"https://raw.githubusercontent.com/Dokploy/dokploy/refs/heads/canary/apps/dokploy/logo.png"
|
width="190"
|
||||||
}
|
height="120"
|
||||||
width="100"
|
|
||||||
height="50"
|
|
||||||
alt="Dokploy"
|
alt="Dokploy"
|
||||||
className="my-0 mx-auto"
|
className="my-0 mx-auto"
|
||||||
/>
|
/>
|
||||||
</Section>
|
</Section>
|
||||||
<Heading className="text-black text-[24px] font-normal text-center p-0 my-[30px] mx-0">
|
|
||||||
Join to <strong>Dokploy</strong>
|
{/* Body */}
|
||||||
</Heading>
|
<Section className="bg-white px-[40px] py-[32px]">
|
||||||
<Text className="text-black text-[14px] leading-[24px]">
|
<Heading className="text-[#09090b] text-[22px] font-semibold m-0 mb-[8px]">
|
||||||
Hello,
|
You've been invited to join {organizationName}
|
||||||
</Text>
|
</Heading>
|
||||||
<Text className="text-black text-[14px] leading-[24px]">
|
<Text className="text-[#71717a] text-[14px] leading-[22px] m-0 mb-[24px]">
|
||||||
You have been invited to join <strong>Dokploy</strong>, a platform
|
You have been invited to join{" "}
|
||||||
that helps for deploying your apps to the cloud.
|
<strong className="text-[#09090b]">{organizationName}</strong>{" "}
|
||||||
</Text>
|
on Dokploy, the platform for deploying your apps to the cloud.
|
||||||
<Section className="text-center mt-[32px] mb-[32px]">
|
Click the button below to accept the invitation.
|
||||||
<Button
|
</Text>
|
||||||
href={inviteLink}
|
|
||||||
className="bg-[#000000] rounded text-white text-[12px] font-semibold no-underline text-center px-5 py-3"
|
{/* CTA Button */}
|
||||||
>
|
<Section className="text-center mb-[24px]">
|
||||||
Join the team 🚀
|
<Button
|
||||||
</Button>
|
href={inviteLink}
|
||||||
|
className="bg-[#09090b] rounded-lg text-white text-[14px] font-semibold no-underline text-center px-[24px] py-[12px]"
|
||||||
|
>
|
||||||
|
Accept Invitation
|
||||||
|
</Button>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Text className="text-[#a1a1aa] text-[13px] leading-[20px] m-0 text-center mb-[16px]">
|
||||||
|
If the button above doesn't work, copy and paste the following
|
||||||
|
link into your browser:
|
||||||
|
</Text>
|
||||||
|
<Text className="text-[#71717a] text-[12px] leading-[18px] m-0 text-center break-all">
|
||||||
|
{inviteLink}
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<Section className="bg-[#fafafa] rounded-b-xl px-[40px] py-[24px] text-center border-t border-solid border-[#e4e4e7]">
|
||||||
|
<Hr className="border border-solid border-[#e4e4e7] my-0 mb-[16px] mx-0 w-full" />
|
||||||
|
<Text className="text-[#a1a1aa] text-[12px] leading-[18px] m-0">
|
||||||
|
This invitation was intended for{" "}
|
||||||
|
<span className="text-[#71717a]">{toEmail}</span>. This invite
|
||||||
|
was sent from{" "}
|
||||||
|
<Link
|
||||||
|
href="https://dokploy.com"
|
||||||
|
className="text-[#71717a] underline"
|
||||||
|
>
|
||||||
|
Dokploy Cloud
|
||||||
|
</Link>
|
||||||
|
. If you were not expecting this invitation, you can safely
|
||||||
|
ignore this email.
|
||||||
|
</Text>
|
||||||
</Section>
|
</Section>
|
||||||
<Text className="text-black text-[14px] leading-[24px]">
|
|
||||||
or copy and paste this URL into your browser:{" "}
|
|
||||||
<Link href={inviteLink} className="text-blue-600 no-underline">
|
|
||||||
https://dokploy.com
|
|
||||||
</Link>
|
|
||||||
</Text>
|
|
||||||
<Hr className="border border-solid border-[#eaeaea] my-[26px] mx-0 w-full" />
|
|
||||||
<Text className="text-[#666666] text-[12px] leading-[24px]">
|
|
||||||
This invitation was intended for {toEmail}. This invite was sent
|
|
||||||
from <strong className="text-black">dokploy.com</strong>. If you
|
|
||||||
were not expecting this invitation, you can ignore this email. If
|
|
||||||
you are concerned about your account's safety, please reply to
|
|
||||||
</Text>
|
|
||||||
</Container>
|
</Container>
|
||||||
</Body>
|
</Body>
|
||||||
</Tailwind>
|
</Tailwind>
|
||||||
|
|||||||
104
packages/server/src/emails/emails/verify-email.tsx
Normal file
104
packages/server/src/emails/emails/verify-email.tsx
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import {
|
||||||
|
Body,
|
||||||
|
Button,
|
||||||
|
Container,
|
||||||
|
Head,
|
||||||
|
Heading,
|
||||||
|
Html,
|
||||||
|
Img,
|
||||||
|
Link,
|
||||||
|
Preview,
|
||||||
|
Section,
|
||||||
|
Tailwind,
|
||||||
|
Text,
|
||||||
|
} from "@react-email/components";
|
||||||
|
|
||||||
|
export type TemplateProps = {
|
||||||
|
userName: string;
|
||||||
|
verificationUrl: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const VerifyEmailTemplate = ({
|
||||||
|
userName = "User",
|
||||||
|
verificationUrl = "https://app.dokploy.com/verify",
|
||||||
|
}: TemplateProps) => {
|
||||||
|
const previewText = "Verify your email address to get started with Dokploy";
|
||||||
|
return (
|
||||||
|
<Html>
|
||||||
|
<Head />
|
||||||
|
<Preview>{previewText}</Preview>
|
||||||
|
<Tailwind
|
||||||
|
config={{
|
||||||
|
theme: {
|
||||||
|
extend: {
|
||||||
|
colors: {
|
||||||
|
brand: "#007291",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Body className="bg-[#f4f4f5] my-auto mx-auto font-sans">
|
||||||
|
<Container className="my-[40px] mx-auto max-w-[520px]">
|
||||||
|
{/* Header */}
|
||||||
|
<Section className="bg-[#09090b] rounded-t-xl px-[40px] py-[32px] text-center">
|
||||||
|
<Img
|
||||||
|
src="https://raw.githubusercontent.com/Dokploy/website/refs/heads/main/apps/docs/public/logo-dokploy-blackpng.png"
|
||||||
|
width="190"
|
||||||
|
height="120"
|
||||||
|
alt="Dokploy"
|
||||||
|
className="my-0 mx-auto"
|
||||||
|
/>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{/* Body */}
|
||||||
|
<Section className="bg-white px-[40px] py-[32px]">
|
||||||
|
<Heading className="text-[#09090b] text-[22px] font-semibold m-0 mb-[8px]">
|
||||||
|
Verify Your Email
|
||||||
|
</Heading>
|
||||||
|
<Text className="text-[#71717a] text-[14px] leading-[22px] m-0 mb-[24px]">
|
||||||
|
Hello {userName}, thank you for signing up for Dokploy. Please
|
||||||
|
verify your email address to activate your account.
|
||||||
|
</Text>
|
||||||
|
|
||||||
|
{/* CTA Button */}
|
||||||
|
<Section className="text-center mb-[24px]">
|
||||||
|
<Button
|
||||||
|
href={verificationUrl}
|
||||||
|
className="bg-[#09090b] rounded-lg text-white text-[14px] font-semibold no-underline text-center px-[24px] py-[12px]"
|
||||||
|
>
|
||||||
|
Verify Email Address
|
||||||
|
</Button>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
<Text className="text-[#a1a1aa] text-[13px] leading-[20px] m-0 text-center mb-[16px]">
|
||||||
|
If the button above doesn't work, copy and paste the following
|
||||||
|
link into your browser:
|
||||||
|
</Text>
|
||||||
|
<Text className="text-[#71717a] text-[12px] leading-[18px] m-0 text-center break-all">
|
||||||
|
{verificationUrl}
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
|
||||||
|
{/* Footer */}
|
||||||
|
<Section className="bg-[#fafafa] rounded-b-xl px-[40px] py-[24px] text-center border-t border-solid border-[#e4e4e7]">
|
||||||
|
<Text className="text-[#a1a1aa] text-[12px] leading-[18px] m-0">
|
||||||
|
This is an automated email from{" "}
|
||||||
|
<Link
|
||||||
|
href="https://dokploy.com"
|
||||||
|
className="text-[#71717a] underline"
|
||||||
|
>
|
||||||
|
Dokploy Cloud
|
||||||
|
</Link>
|
||||||
|
. If you didn't create an account, you can safely ignore this
|
||||||
|
email.
|
||||||
|
</Text>
|
||||||
|
</Section>
|
||||||
|
</Container>
|
||||||
|
</Body>
|
||||||
|
</Tailwind>
|
||||||
|
</Html>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default VerifyEmailTemplate;
|
||||||
@@ -108,6 +108,7 @@ export * from "./utils/notifications/docker-cleanup";
|
|||||||
export * from "./utils/notifications/dokploy-restart";
|
export * from "./utils/notifications/dokploy-restart";
|
||||||
export * from "./utils/notifications/server-threshold";
|
export * from "./utils/notifications/server-threshold";
|
||||||
export * from "./utils/notifications/utils";
|
export * from "./utils/notifications/utils";
|
||||||
|
export * from "./verification/send-verification-email";
|
||||||
export * from "./utils/process/execAsync";
|
export * from "./utils/process/execAsync";
|
||||||
export * from "./utils/process/spawnAsync";
|
export * from "./utils/process/spawnAsync";
|
||||||
export * from "./utils/providers/bitbucket";
|
export * from "./utils/providers/bitbucket";
|
||||||
|
|||||||
@@ -21,7 +21,10 @@ import {
|
|||||||
updateWebServerSettings,
|
updateWebServerSettings,
|
||||||
} from "../services/web-server-settings";
|
} from "../services/web-server-settings";
|
||||||
import { getHubSpotUTK, submitToHubSpot } from "../utils/tracking/hubspot";
|
import { getHubSpotUTK, submitToHubSpot } from "../utils/tracking/hubspot";
|
||||||
import { sendEmail } from "../verification/send-verification-email";
|
import {
|
||||||
|
sendEmail,
|
||||||
|
sendVerificationEmail,
|
||||||
|
} from "../verification/send-verification-email";
|
||||||
import { getPublicIpWithFallback } from "../wss/utils";
|
import { getPublicIpWithFallback } from "../wss/utils";
|
||||||
import { ac, adminRole, memberRole, ownerRole } from "./access-control";
|
import { ac, adminRole, memberRole, ownerRole } from "./access-control";
|
||||||
|
|
||||||
@@ -106,14 +109,13 @@ const { handler, api } = betterAuth({
|
|||||||
emailVerification: {
|
emailVerification: {
|
||||||
sendOnSignUp: true,
|
sendOnSignUp: true,
|
||||||
autoSignInAfterVerification: true,
|
autoSignInAfterVerification: true,
|
||||||
|
sendOnSignIn: true,
|
||||||
sendVerificationEmail: async ({ user, url }) => {
|
sendVerificationEmail: async ({ user, url }) => {
|
||||||
if (IS_CLOUD) {
|
if (IS_CLOUD) {
|
||||||
await sendEmail({
|
await sendVerificationEmail({
|
||||||
|
userName: user.name || "User",
|
||||||
email: user.email,
|
email: user.email,
|
||||||
subject: "Verify your email",
|
verificationUrl: url,
|
||||||
text: `
|
|
||||||
<p>Click the link to verify your email: <a href="${url}">Verify Email</a></p>
|
|
||||||
`,
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -407,23 +409,6 @@ const { handler, api } = betterAuth({
|
|||||||
enabled: true,
|
enabled: true,
|
||||||
maximumRolesPerOrganization: 10,
|
maximumRolesPerOrganization: 10,
|
||||||
},
|
},
|
||||||
async sendInvitationEmail(data, _request) {
|
|
||||||
if (IS_CLOUD) {
|
|
||||||
const host =
|
|
||||||
process.env.NODE_ENV === "development"
|
|
||||||
? "http://localhost:3000"
|
|
||||||
: "https://app.dokploy.com";
|
|
||||||
const inviteLink = `${host}/invitation?token=${data.id}`;
|
|
||||||
|
|
||||||
await sendEmail({
|
|
||||||
email: data.email,
|
|
||||||
subject: "Invitation to join organization",
|
|
||||||
text: `
|
|
||||||
<p>You are invited to join ${data.organization.name} on Dokploy. Click the link to accept the invitation: <a href="${inviteLink}">Accept Invitation</a></p>
|
|
||||||
`,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}),
|
}),
|
||||||
...(IS_CLOUD
|
...(IS_CLOUD
|
||||||
? [
|
? [
|
||||||
@@ -479,8 +464,10 @@ export const validateRequest = async (request: IncomingMessage) => {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
const organizationId = JSON.parse(
|
const organizationId = (
|
||||||
apiKeyRecord.metadata || "{}",
|
JSON.parse(apiKeyRecord.metadata || "{}") as {
|
||||||
|
organizationId?: string;
|
||||||
|
}
|
||||||
).organizationId;
|
).organizationId;
|
||||||
|
|
||||||
if (!organizationId) {
|
if (!organizationId) {
|
||||||
|
|||||||
@@ -30,13 +30,9 @@ export const findPreviewDeploymentById = async (
|
|||||||
with: {
|
with: {
|
||||||
domain: true,
|
domain: true,
|
||||||
application: {
|
application: {
|
||||||
with: {
|
columns: {
|
||||||
server: true,
|
applicationId: true,
|
||||||
environment: {
|
serverId: true,
|
||||||
with: {
|
|
||||||
project: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ export function parseRawConfig(
|
|||||||
|
|
||||||
if (search) {
|
if (search) {
|
||||||
parsedLogs = parsedLogs.filter((log) =>
|
parsedLogs = parsedLogs.filter((log) =>
|
||||||
log.RequestPath.toLowerCase().includes(search.toLowerCase()),
|
log.RequestHost.toLowerCase().includes(search.toLowerCase()),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
|
import { renderAsync } from "@react-email/components";
|
||||||
|
import InvitationEmail from "../emails/emails/invitation";
|
||||||
|
import VerifyEmailTemplate from "../emails/emails/verify-email";
|
||||||
import { sendEmailNotification } from "../utils/notifications/utils";
|
import { sendEmailNotification } from "../utils/notifications/utils";
|
||||||
|
|
||||||
export const sendEmail = async ({
|
export const sendEmail = async ({
|
||||||
email,
|
email,
|
||||||
subject,
|
subject,
|
||||||
@@ -26,3 +30,64 @@ export const sendEmail = async ({
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const sendVerificationEmail = async ({
|
||||||
|
userName,
|
||||||
|
email,
|
||||||
|
verificationUrl,
|
||||||
|
}: {
|
||||||
|
userName: string;
|
||||||
|
email: string;
|
||||||
|
verificationUrl: string;
|
||||||
|
}) => {
|
||||||
|
const html = await renderAsync(
|
||||||
|
VerifyEmailTemplate({
|
||||||
|
userName: userName || "User",
|
||||||
|
verificationUrl,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
await sendEmail({
|
||||||
|
email,
|
||||||
|
subject: "Verify your email",
|
||||||
|
text: html,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const renderInvitationEmail = async ({
|
||||||
|
email,
|
||||||
|
inviteLink,
|
||||||
|
organizationName,
|
||||||
|
}: {
|
||||||
|
email: string;
|
||||||
|
inviteLink: string;
|
||||||
|
organizationName: string;
|
||||||
|
}) => {
|
||||||
|
return renderAsync(
|
||||||
|
InvitationEmail({
|
||||||
|
inviteLink,
|
||||||
|
toEmail: email,
|
||||||
|
organizationName,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const sendInvitationEmail = async ({
|
||||||
|
email,
|
||||||
|
inviteLink,
|
||||||
|
organizationName,
|
||||||
|
}: {
|
||||||
|
email: string;
|
||||||
|
inviteLink: string;
|
||||||
|
organizationName: string;
|
||||||
|
}) => {
|
||||||
|
const html = await renderInvitationEmail({
|
||||||
|
email,
|
||||||
|
inviteLink,
|
||||||
|
organizationName,
|
||||||
|
});
|
||||||
|
await sendEmail({
|
||||||
|
email,
|
||||||
|
subject: `You've been invited to join ${organizationName} on Dokploy`,
|
||||||
|
text: html,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
17
pnpm-lock.yaml
generated
17
pnpm-lock.yaml
generated
@@ -417,8 +417,8 @@ importers:
|
|||||||
specifier: ^1.7.4
|
specifier: ^1.7.4
|
||||||
version: 1.7.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
version: 1.7.4(react-dom@18.2.0(react@18.2.0))(react@18.2.0)
|
||||||
ssh2:
|
ssh2:
|
||||||
specifier: 1.15.0
|
specifier: ~1.16.0
|
||||||
version: 1.15.0
|
version: 1.16.0
|
||||||
stripe:
|
stripe:
|
||||||
specifier: 17.2.0
|
specifier: 17.2.0
|
||||||
version: 17.2.0
|
version: 17.2.0
|
||||||
@@ -758,8 +758,8 @@ importers:
|
|||||||
specifier: ^1.6.6
|
specifier: ^1.6.6
|
||||||
version: 1.6.6
|
version: 1.6.6
|
||||||
ssh2:
|
ssh2:
|
||||||
specifier: 1.15.0
|
specifier: ~1.16.0
|
||||||
version: 1.15.0
|
version: 1.16.0
|
||||||
toml:
|
toml:
|
||||||
specifier: 3.0.0
|
specifier: 3.0.0
|
||||||
version: 3.0.0
|
version: 3.0.0
|
||||||
@@ -4196,6 +4196,7 @@ packages:
|
|||||||
'@xmldom/xmldom@0.8.11':
|
'@xmldom/xmldom@0.8.11':
|
||||||
resolution: {integrity: sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==}
|
resolution: {integrity: sha512-cQzWCtO6C8TQiYl1ruKNn2U6Ao4o4WBBcbL61yJl84x+j5sOWWFU9X7DpND8XZG3daDppSsigMdfAIl2upQBRw==}
|
||||||
engines: {node: '>=10.0.0'}
|
engines: {node: '>=10.0.0'}
|
||||||
|
deprecated: this version has critical issues, please update to the latest version
|
||||||
|
|
||||||
'@xterm/addon-attach@0.10.0':
|
'@xterm/addon-attach@0.10.0':
|
||||||
resolution: {integrity: sha512-ES/XO8pC1tPHSkh4j7qzM8ajFt++u8KMvfRc9vKIbjHTDOxjl9IUVo+vcQgLn3FTCM3w2czTvBss8nMWlD83Cg==}
|
resolution: {integrity: sha512-ES/XO8pC1tPHSkh4j7qzM8ajFt++u8KMvfRc9vKIbjHTDOxjl9IUVo+vcQgLn3FTCM3w2czTvBss8nMWlD83Cg==}
|
||||||
@@ -7599,8 +7600,8 @@ packages:
|
|||||||
resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==}
|
resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==}
|
||||||
engines: {node: '>= 0.6'}
|
engines: {node: '>= 0.6'}
|
||||||
|
|
||||||
ssh2@1.15.0:
|
ssh2@1.16.0:
|
||||||
resolution: {integrity: sha512-C0PHgX4h6lBxYx7hcXwu3QWdh4tg6tZZsTfXcdvc5caW/EMxaB4H9dWsl7qk+F7LAW762hp8VbXOX7x4xUYvEw==}
|
resolution: {integrity: sha512-r1X4KsBGedJqo7h8F5c4Ybpcr5RjyP+aWIG007uBPRjmdQWfEiVLzSK71Zji1B9sKxwaCvD8y8cwSkYrlLiRRg==}
|
||||||
engines: {node: '>=10.16.0'}
|
engines: {node: '>=10.16.0'}
|
||||||
|
|
||||||
stackback@0.0.2:
|
stackback@0.0.2:
|
||||||
@@ -13139,7 +13140,7 @@ snapshots:
|
|||||||
debug: 4.4.3
|
debug: 4.4.3
|
||||||
readable-stream: 3.6.2
|
readable-stream: 3.6.2
|
||||||
split-ca: 1.0.1
|
split-ca: 1.0.1
|
||||||
ssh2: 1.15.0
|
ssh2: 1.16.0
|
||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
@@ -15783,7 +15784,7 @@ snapshots:
|
|||||||
|
|
||||||
sqlstring@2.3.3: {}
|
sqlstring@2.3.3: {}
|
||||||
|
|
||||||
ssh2@1.15.0:
|
ssh2@1.16.0:
|
||||||
dependencies:
|
dependencies:
|
||||||
asn1: 0.2.6
|
asn1: 0.2.6
|
||||||
bcrypt-pbkdf: 1.0.2
|
bcrypt-pbkdf: 1.0.2
|
||||||
|
|||||||
Reference in New Issue
Block a user