mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-07-16 19:35:25 +02:00
Compare commits
17 Commits
dosu/doc-u
...
v0.28.7
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9067452a38 | ||
|
|
1fa4d5b2ba | ||
|
|
bade36ea9d | ||
|
|
0c22041623 | ||
|
|
cccee05173 | ||
|
|
9f9c8fccf2 | ||
|
|
ad2e53a67a | ||
|
|
00f3853bd7 | ||
|
|
2880327e94 | ||
|
|
827b84f57e | ||
|
|
11aa8fe0c5 | ||
|
|
b9ac720d99 | ||
|
|
77b0ff7bbf | ||
|
|
e7af2c0ebd | ||
|
|
6a1bedb90f | ||
|
|
a2f142174b | ||
|
|
2f37235aea |
@@ -91,7 +91,10 @@ export const ShowBilling = () => {
|
|||||||
api.stripe.upgradeSubscription.useMutation();
|
api.stripe.upgradeSubscription.useMutation();
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
|
||||||
const [serverQuantity, setServerQuantity] = useState(3);
|
const [hobbyServerQuantity, setHobbyServerQuantity] = useState(1);
|
||||||
|
const [startupServerQuantity, setStartupServerQuantity] = useState(
|
||||||
|
STARTUP_SERVERS_INCLUDED,
|
||||||
|
);
|
||||||
const [isAnnual, setIsAnnual] = useState(false);
|
const [isAnnual, setIsAnnual] = useState(false);
|
||||||
const [upgradeTier, setUpgradeTier] = useState<"hobby" | "startup" | null>(
|
const [upgradeTier, setUpgradeTier] = useState<"hobby" | "startup" | null>(
|
||||||
null,
|
null,
|
||||||
@@ -111,6 +114,12 @@ export const ShowBilling = () => {
|
|||||||
productId: string,
|
productId: string,
|
||||||
) => {
|
) => {
|
||||||
const stripe = await stripePromise;
|
const stripe = await stripePromise;
|
||||||
|
const serverQuantity =
|
||||||
|
tier === "startup"
|
||||||
|
? startupServerQuantity
|
||||||
|
: tier === "hobby"
|
||||||
|
? hobbyServerQuantity
|
||||||
|
: hobbyServerQuantity;
|
||||||
if (data && data.subscriptions.length === 0) {
|
if (data && data.subscriptions.length === 0) {
|
||||||
createCheckoutSession({
|
createCheckoutSession({
|
||||||
tier,
|
tier,
|
||||||
@@ -679,7 +688,7 @@ export const ShowBilling = () => {
|
|||||||
<p className="text-2xl font-semibold text-foreground">
|
<p className="text-2xl font-semibold text-foreground">
|
||||||
$
|
$
|
||||||
{calculatePriceHobby(
|
{calculatePriceHobby(
|
||||||
serverQuantity,
|
hobbyServerQuantity,
|
||||||
isAnnual,
|
isAnnual,
|
||||||
).toFixed(2)}
|
).toFixed(2)}
|
||||||
/{isAnnual ? "yr" : "mo"}
|
/{isAnnual ? "yr" : "mo"}
|
||||||
@@ -692,7 +701,8 @@ export const ShowBilling = () => {
|
|||||||
<p className="text-xs text-muted-foreground mt-2">
|
<p className="text-xs text-muted-foreground mt-2">
|
||||||
$
|
$
|
||||||
{(
|
{(
|
||||||
calculatePriceHobby(serverQuantity, true) / 12
|
calculatePriceHobby(hobbyServerQuantity, true) /
|
||||||
|
12
|
||||||
).toFixed(2)}
|
).toFixed(2)}
|
||||||
/mo
|
/mo
|
||||||
</p>
|
</p>
|
||||||
@@ -724,19 +734,19 @@ export const ShowBilling = () => {
|
|||||||
Servers:
|
Servers:
|
||||||
</span>
|
</span>
|
||||||
<Button
|
<Button
|
||||||
disabled={serverQuantity <= 1}
|
disabled={hobbyServerQuantity <= 1}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setServerQuantity((q) => Math.max(1, q - 1))
|
setHobbyServerQuantity((q) => Math.max(1, q - 1))
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<MinusIcon className="h-4 w-4" />
|
<MinusIcon className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
value={serverQuantity}
|
value={hobbyServerQuantity}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setServerQuantity(
|
setHobbyServerQuantity(
|
||||||
Math.max(
|
Math.max(
|
||||||
1,
|
1,
|
||||||
Number(
|
Number(
|
||||||
@@ -750,7 +760,7 @@ export const ShowBilling = () => {
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
onClick={() => setServerQuantity((q) => q + 1)}
|
onClick={() => setHobbyServerQuantity((q) => q + 1)}
|
||||||
>
|
>
|
||||||
<PlusIcon className="h-4 w-4" />
|
<PlusIcon className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -775,7 +785,7 @@ export const ShowBilling = () => {
|
|||||||
onClick={() =>
|
onClick={() =>
|
||||||
handleCheckout("hobby", data!.hobbyProductId!)
|
handleCheckout("hobby", data!.hobbyProductId!)
|
||||||
}
|
}
|
||||||
disabled={serverQuantity < 1}
|
disabled={hobbyServerQuantity < 1}
|
||||||
>
|
>
|
||||||
Get Started
|
Get Started
|
||||||
</Button>
|
</Button>
|
||||||
@@ -806,7 +816,7 @@ export const ShowBilling = () => {
|
|||||||
<p className="text-2xl font-semibold text-foreground">
|
<p className="text-2xl font-semibold text-foreground">
|
||||||
$
|
$
|
||||||
{calculatePriceStartup(
|
{calculatePriceStartup(
|
||||||
serverQuantity,
|
startupServerQuantity,
|
||||||
isAnnual,
|
isAnnual,
|
||||||
).toFixed(2)}
|
).toFixed(2)}
|
||||||
/{isAnnual ? "yr" : "mo"}
|
/{isAnnual ? "yr" : "mo"}
|
||||||
@@ -819,7 +829,10 @@ export const ShowBilling = () => {
|
|||||||
<p className="text-xs text-muted-foreground mt-2">
|
<p className="text-xs text-muted-foreground mt-2">
|
||||||
$
|
$
|
||||||
{(
|
{(
|
||||||
calculatePriceStartup(serverQuantity, true) / 12
|
calculatePriceStartup(
|
||||||
|
startupServerQuantity,
|
||||||
|
true,
|
||||||
|
) / 12
|
||||||
).toFixed(2)}
|
).toFixed(2)}
|
||||||
/mo
|
/mo
|
||||||
</p>
|
</p>
|
||||||
@@ -856,13 +869,14 @@ export const ShowBilling = () => {
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Button
|
<Button
|
||||||
disabled={
|
disabled={
|
||||||
serverQuantity <= STARTUP_SERVERS_INCLUDED
|
startupServerQuantity <=
|
||||||
|
STARTUP_SERVERS_INCLUDED
|
||||||
}
|
}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-8 w-8"
|
className="h-8 w-8"
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
setServerQuantity((q) =>
|
setStartupServerQuantity((q) =>
|
||||||
Math.max(STARTUP_SERVERS_INCLUDED, q - 1),
|
Math.max(STARTUP_SERVERS_INCLUDED, q - 1),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -870,9 +884,9 @@ export const ShowBilling = () => {
|
|||||||
<MinusIcon className="h-4 w-4" />
|
<MinusIcon className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
value={serverQuantity}
|
value={startupServerQuantity}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setServerQuantity(
|
setStartupServerQuantity(
|
||||||
Math.max(
|
Math.max(
|
||||||
STARTUP_SERVERS_INCLUDED,
|
STARTUP_SERVERS_INCLUDED,
|
||||||
Number(
|
Number(
|
||||||
@@ -887,7 +901,9 @@ export const ShowBilling = () => {
|
|||||||
variant="outline"
|
variant="outline"
|
||||||
size="icon"
|
size="icon"
|
||||||
className="h-8 w-8"
|
className="h-8 w-8"
|
||||||
onClick={() => setServerQuantity((q) => q + 1)}
|
onClick={() =>
|
||||||
|
setStartupServerQuantity((q) => q + 1)
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<PlusIcon className="h-4 w-4" />
|
<PlusIcon className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
@@ -917,7 +933,7 @@ export const ShowBilling = () => {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
disabled={
|
disabled={
|
||||||
serverQuantity < STARTUP_SERVERS_INCLUDED
|
startupServerQuantity < STARTUP_SERVERS_INCLUDED
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
Get Started
|
Get Started
|
||||||
@@ -1009,7 +1025,7 @@ export const ShowBilling = () => {
|
|||||||
<p className="text-2xl font-semibold tracking-tight text-primary ">
|
<p className="text-2xl font-semibold tracking-tight text-primary ">
|
||||||
${" "}
|
${" "}
|
||||||
{calculatePrice(
|
{calculatePrice(
|
||||||
serverQuantity,
|
hobbyServerQuantity,
|
||||||
isAnnual,
|
isAnnual,
|
||||||
).toFixed(2)}{" "}
|
).toFixed(2)}{" "}
|
||||||
USD
|
USD
|
||||||
@@ -1018,7 +1034,10 @@ export const ShowBilling = () => {
|
|||||||
<p className="text-base font-semibold tracking-tight text-muted-foreground">
|
<p className="text-base font-semibold tracking-tight text-muted-foreground">
|
||||||
${" "}
|
${" "}
|
||||||
{(
|
{(
|
||||||
calculatePrice(serverQuantity, isAnnual) / 12
|
calculatePrice(
|
||||||
|
hobbyServerQuantity,
|
||||||
|
isAnnual,
|
||||||
|
) / 12
|
||||||
).toFixed(2)}{" "}
|
).toFixed(2)}{" "}
|
||||||
/ Month USD
|
/ Month USD
|
||||||
</p>
|
</p>
|
||||||
@@ -1026,9 +1045,10 @@ export const ShowBilling = () => {
|
|||||||
) : (
|
) : (
|
||||||
<p className="text-2xl font-semibold tracking-tight text-primary ">
|
<p className="text-2xl font-semibold tracking-tight text-primary ">
|
||||||
${" "}
|
${" "}
|
||||||
{calculatePrice(serverQuantity, isAnnual).toFixed(
|
{calculatePrice(
|
||||||
2,
|
hobbyServerQuantity,
|
||||||
)}{" "}
|
isAnnual,
|
||||||
|
).toFixed(2)}{" "}
|
||||||
USD
|
USD
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
@@ -1071,26 +1091,28 @@ export const ShowBilling = () => {
|
|||||||
<div className="flex flex-col gap-2 mt-4">
|
<div className="flex flex-col gap-2 mt-4">
|
||||||
<div className="flex items-center gap-2 justify-center">
|
<div className="flex items-center gap-2 justify-center">
|
||||||
<span className="text-sm text-muted-foreground">
|
<span className="text-sm text-muted-foreground">
|
||||||
{serverQuantity} Servers
|
{hobbyServerQuantity} Servers
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center space-x-2">
|
<div className="flex items-center space-x-2">
|
||||||
<Button
|
<Button
|
||||||
disabled={serverQuantity <= 1}
|
disabled={hobbyServerQuantity <= 1}
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
if (serverQuantity <= 1) return;
|
if (hobbyServerQuantity <= 1) return;
|
||||||
|
|
||||||
setServerQuantity(serverQuantity - 1);
|
setHobbyServerQuantity(
|
||||||
|
hobbyServerQuantity - 1,
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<MinusIcon className="h-4 w-4" />
|
<MinusIcon className="h-4 w-4" />
|
||||||
</Button>
|
</Button>
|
||||||
<NumberInput
|
<NumberInput
|
||||||
value={serverQuantity}
|
value={hobbyServerQuantity}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
setServerQuantity(
|
setHobbyServerQuantity(
|
||||||
e.target.value as unknown as number,
|
e.target.value as unknown as number,
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
@@ -1099,7 +1121,9 @@ export const ShowBilling = () => {
|
|||||||
<Button
|
<Button
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setServerQuantity(serverQuantity + 1);
|
setHobbyServerQuantity(
|
||||||
|
hobbyServerQuantity + 1,
|
||||||
|
);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<PlusIcon className="h-4 w-4" />
|
<PlusIcon className="h-4 w-4" />
|
||||||
@@ -1125,7 +1149,7 @@ export const ShowBilling = () => {
|
|||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
handleCheckout("legacy", product.id);
|
handleCheckout("legacy", product.id);
|
||||||
}}
|
}}
|
||||||
disabled={serverQuantity < 1}
|
disabled={hobbyServerQuantity < 1}
|
||||||
>
|
>
|
||||||
Subscribe
|
Subscribe
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { format } from "date-fns";
|
import { format } from "date-fns";
|
||||||
import { Loader2, MoreHorizontal, Users } from "lucide-react";
|
import { Loader2, MoreHorizontal, Users } from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
import { Badge } from "@/components/ui/badge";
|
import { Badge } from "@/components/ui/badge";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
@@ -36,10 +37,19 @@ export const ShowUsers = () => {
|
|||||||
const { data, isPending, refetch } = api.user.all.useQuery();
|
const { data, isPending, refetch } = api.user.all.useQuery();
|
||||||
const { mutateAsync } = api.user.remove.useMutation();
|
const { mutateAsync } = api.user.remove.useMutation();
|
||||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||||
|
const { data: hasValidLicense } =
|
||||||
|
api.licenseKey.haveValidLicenseKey.useQuery();
|
||||||
|
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const { data: session } = api.user.session.useQuery();
|
const { data: session } = api.user.session.useQuery();
|
||||||
|
|
||||||
|
const FREE_ROLES = ["owner", "admin", "member"];
|
||||||
|
const membersWithCustomRoles = data?.filter(
|
||||||
|
(member) => !FREE_ROLES.includes(member.role),
|
||||||
|
);
|
||||||
|
const hasCustomRolesWithoutLicense =
|
||||||
|
!hasValidLicense && (membersWithCustomRoles?.length ?? 0) > 0;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
|
<Card className="h-full bg-sidebar p-2.5 rounded-xl max-w-5xl mx-auto">
|
||||||
@@ -70,6 +80,18 @@ export const ShowUsers = () => {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<div className="flex flex-col gap-4 min-h-[25vh]">
|
<div className="flex flex-col gap-4 min-h-[25vh]">
|
||||||
|
{hasCustomRolesWithoutLicense && (
|
||||||
|
<AlertBlock type="warning">
|
||||||
|
You have{" "}
|
||||||
|
{membersWithCustomRoles?.length === 1
|
||||||
|
? "1 user"
|
||||||
|
: `${membersWithCustomRoles?.length} users`}{" "}
|
||||||
|
assigned to custom roles. Custom roles will not work
|
||||||
|
without a valid Enterprise license. Please activate your
|
||||||
|
license or change these users to a free role (Admin or
|
||||||
|
Member).
|
||||||
|
</AlertBlock>
|
||||||
|
)}
|
||||||
<Table>
|
<Table>
|
||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
|
|||||||
@@ -1,13 +1,20 @@
|
|||||||
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
|
||||||
import { Loader2, PlusIcon, ShieldCheck, TrashIcon, Users } from "lucide-react";
|
import {
|
||||||
|
Loader2,
|
||||||
|
PlusIcon,
|
||||||
|
ShieldCheck,
|
||||||
|
Sparkles,
|
||||||
|
TrashIcon,
|
||||||
|
Users,
|
||||||
|
} from "lucide-react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
import { DialogAction } from "@/components/shared/dialog-action";
|
|
||||||
import { EnterpriseFeatureGate } from "@/components/proprietary/enterprise-feature-gate";
|
import { EnterpriseFeatureGate } from "@/components/proprietary/enterprise-feature-gate";
|
||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import { AlertBlock } from "@/components/shared/alert-block";
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { DialogAction } from "@/components/shared/dialog-action";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
CardContent,
|
CardContent,
|
||||||
@@ -24,11 +31,6 @@ import {
|
|||||||
DialogTitle,
|
DialogTitle,
|
||||||
DialogTrigger,
|
DialogTrigger,
|
||||||
} from "@/components/ui/dialog";
|
} from "@/components/ui/dialog";
|
||||||
import {
|
|
||||||
Popover,
|
|
||||||
PopoverContent,
|
|
||||||
PopoverTrigger,
|
|
||||||
} from "@/components/ui/popover";
|
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
@@ -38,6 +40,11 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Popover,
|
||||||
|
PopoverContent,
|
||||||
|
PopoverTrigger,
|
||||||
|
} from "@/components/ui/popover";
|
||||||
import { Switch } from "@/components/ui/switch";
|
import { Switch } from "@/components/ui/switch";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
|
|
||||||
@@ -407,6 +414,114 @@ const ACTION_META: Record<
|
|||||||
/** Resources that should be hidden from the custom role editor (better-auth internals) */
|
/** Resources that should be hidden from the custom role editor (better-auth internals) */
|
||||||
const HIDDEN_RESOURCES = ["organization", "invitation", "team", "ac"];
|
const HIDDEN_RESOURCES = ["organization", "invitation", "team", "ac"];
|
||||||
|
|
||||||
|
/** Predefined role presets with sensible permission defaults */
|
||||||
|
const ROLE_PRESETS: {
|
||||||
|
name: string;
|
||||||
|
label: string;
|
||||||
|
description: string;
|
||||||
|
permissions: Record<string, string[]>;
|
||||||
|
}[] = [
|
||||||
|
{
|
||||||
|
name: "viewer",
|
||||||
|
label: "Viewer",
|
||||||
|
description: "Read-only access across all resources",
|
||||||
|
permissions: {
|
||||||
|
service: ["read"],
|
||||||
|
environment: ["read"],
|
||||||
|
docker: ["read"],
|
||||||
|
sshKeys: ["read"],
|
||||||
|
gitProviders: ["read"],
|
||||||
|
traefikFiles: ["read"],
|
||||||
|
api: ["read"],
|
||||||
|
volume: ["read"],
|
||||||
|
deployment: ["read"],
|
||||||
|
envVars: ["read"],
|
||||||
|
projectEnvVars: ["read"],
|
||||||
|
environmentEnvVars: ["read"],
|
||||||
|
server: ["read"],
|
||||||
|
registry: ["read"],
|
||||||
|
certificate: ["read"],
|
||||||
|
backup: ["read"],
|
||||||
|
volumeBackup: ["read"],
|
||||||
|
schedule: ["read"],
|
||||||
|
domain: ["read"],
|
||||||
|
destination: ["read"],
|
||||||
|
notification: ["read"],
|
||||||
|
member: ["read"],
|
||||||
|
logs: ["read"],
|
||||||
|
monitoring: ["read"],
|
||||||
|
auditLog: ["read"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "developer",
|
||||||
|
label: "Developer",
|
||||||
|
description: "Deploy services, manage env vars, domains, and view logs",
|
||||||
|
permissions: {
|
||||||
|
project: ["create"],
|
||||||
|
service: ["create", "read"],
|
||||||
|
environment: ["create", "read"],
|
||||||
|
docker: ["read"],
|
||||||
|
gitProviders: ["read"],
|
||||||
|
api: ["read"],
|
||||||
|
volume: ["read", "create", "delete"],
|
||||||
|
deployment: ["read", "create", "cancel"],
|
||||||
|
envVars: ["read", "write"],
|
||||||
|
projectEnvVars: ["read"],
|
||||||
|
environmentEnvVars: ["read"],
|
||||||
|
domain: ["read", "create", "delete"],
|
||||||
|
schedule: ["read", "create", "update", "delete"],
|
||||||
|
logs: ["read"],
|
||||||
|
monitoring: ["read"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "deployer",
|
||||||
|
label: "Deployer",
|
||||||
|
description: "Trigger and manage deployments only",
|
||||||
|
permissions: {
|
||||||
|
service: ["read"],
|
||||||
|
environment: ["read"],
|
||||||
|
deployment: ["read", "create", "cancel"],
|
||||||
|
logs: ["read"],
|
||||||
|
monitoring: ["read"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "devops",
|
||||||
|
label: "DevOps",
|
||||||
|
description:
|
||||||
|
"Full infrastructure access: servers, registries, certs, backups, and deployments",
|
||||||
|
permissions: {
|
||||||
|
project: ["create", "delete"],
|
||||||
|
service: ["create", "read", "delete"],
|
||||||
|
environment: ["create", "read", "delete"],
|
||||||
|
docker: ["read"],
|
||||||
|
sshKeys: ["read", "create", "delete"],
|
||||||
|
gitProviders: ["read", "create", "delete"],
|
||||||
|
traefikFiles: ["read", "write"],
|
||||||
|
api: ["read"],
|
||||||
|
volume: ["read", "create", "delete"],
|
||||||
|
deployment: ["read", "create", "cancel"],
|
||||||
|
envVars: ["read", "write"],
|
||||||
|
projectEnvVars: ["read", "write"],
|
||||||
|
environmentEnvVars: ["read", "write"],
|
||||||
|
server: ["read", "create", "delete"],
|
||||||
|
registry: ["read", "create", "delete"],
|
||||||
|
certificate: ["read", "create", "delete"],
|
||||||
|
backup: ["read", "create", "delete", "restore"],
|
||||||
|
volumeBackup: ["read", "create", "update", "delete", "restore"],
|
||||||
|
schedule: ["read", "create", "update", "delete"],
|
||||||
|
domain: ["read", "create", "delete"],
|
||||||
|
destination: ["read", "create", "delete"],
|
||||||
|
notification: ["read", "create", "delete"],
|
||||||
|
logs: ["read"],
|
||||||
|
monitoring: ["read"],
|
||||||
|
auditLog: ["read"],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
const createRoleSchema = z.object({
|
const createRoleSchema = z.object({
|
||||||
roleName: z
|
roleName: z
|
||||||
.string()
|
.string()
|
||||||
@@ -552,7 +667,7 @@ function HandleCustomRole({
|
|||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<DialogContent className="max-h-[85vh] sm:max-w-5xl overflow-y-auto">
|
<DialogContent className="max-h-[85vh] sm:max-w-5xl overflow-y-auto space-y-2">
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>
|
<DialogTitle>
|
||||||
{isEdit ? "Edit Role" : "Create Custom Role"}
|
{isEdit ? "Edit Role" : "Create Custom Role"}
|
||||||
@@ -587,6 +702,32 @@ function HandleCustomRole({
|
|||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
</Form>
|
</Form>
|
||||||
|
{!isEdit && (
|
||||||
|
<div className="space-y-2 mt-4">
|
||||||
|
<p className="text-sm font-medium flex items-center gap-1.5">
|
||||||
|
<Sparkles className="size-3.5 text-muted-foreground" />
|
||||||
|
Start from a preset
|
||||||
|
</p>
|
||||||
|
<div className="grid grid-cols-2 sm:grid-cols-4 gap-2">
|
||||||
|
{ROLE_PRESETS.map((preset) => (
|
||||||
|
<button
|
||||||
|
key={preset.name}
|
||||||
|
type="button"
|
||||||
|
className="rounded-lg border p-3 text-left hover:bg-muted/50 transition-colors cursor-pointer space-y-1"
|
||||||
|
onClick={() => {
|
||||||
|
form.setValue("roleName", preset.name);
|
||||||
|
setPermissions({ ...preset.permissions });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<p className="text-sm font-medium">{preset.label}</p>
|
||||||
|
<p className="text-xs text-muted-foreground leading-snug">
|
||||||
|
{preset.description}
|
||||||
|
</p>
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<PermissionEditor
|
<PermissionEditor
|
||||||
resources={visibleResources}
|
resources={visibleResources}
|
||||||
permissions={permissions}
|
permissions={permissions}
|
||||||
@@ -843,7 +984,7 @@ function PermissionEditor({
|
|||||||
onToggle: (resource: string, action: string) => void;
|
onToggle: (resource: string, action: string) => void;
|
||||||
}) {
|
}) {
|
||||||
return (
|
return (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3 mt-4">
|
||||||
<p className="text-sm font-medium">Permissions</p>
|
<p className="text-sm font-medium">Permissions</p>
|
||||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||||
{resources.map(([resource, actions]) => {
|
{resources.map(([resource, actions]) => {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dokploy",
|
"name": "dokploy",
|
||||||
"version": "v0.28.6",
|
"version": "v0.28.7",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
@@ -21,7 +21,12 @@ import {
|
|||||||
STARTUP_PRODUCT_ID,
|
STARTUP_PRODUCT_ID,
|
||||||
WEBSITE_URL,
|
WEBSITE_URL,
|
||||||
} from "@/server/utils/stripe";
|
} from "@/server/utils/stripe";
|
||||||
import { adminProcedure, createTRPCRouter, protectedProcedure } from "../trpc";
|
import {
|
||||||
|
adminProcedure,
|
||||||
|
createTRPCRouter,
|
||||||
|
protectedProcedure,
|
||||||
|
withPermission,
|
||||||
|
} from "../trpc";
|
||||||
|
|
||||||
export const stripeRouter = createTRPCRouter({
|
export const stripeRouter = createTRPCRouter({
|
||||||
/** Returns the current billing plan for the user's organization. Used to gate features like chat (Startup only). */
|
/** Returns the current billing plan for the user's organization. Used to gate features like chat (Startup only). */
|
||||||
@@ -314,16 +319,18 @@ export const stripeRouter = createTRPCRouter({
|
|||||||
return { ok: true };
|
return { ok: true };
|
||||||
}),
|
}),
|
||||||
|
|
||||||
canCreateMoreServers: adminProcedure.query(async ({ ctx }) => {
|
canCreateMoreServers: withPermission("server", "create").query(
|
||||||
const user = await findUserById(ctx.user.ownerId);
|
async ({ ctx }) => {
|
||||||
const servers = await findServersByUserId(user.id);
|
const user = await findUserById(ctx.user.ownerId);
|
||||||
|
const servers = await findServersByUserId(user.id);
|
||||||
|
|
||||||
if (!IS_CLOUD) {
|
if (!IS_CLOUD) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
return servers.length < user.serversQuantity;
|
return servers.length < user.serversQuantity;
|
||||||
}),
|
},
|
||||||
|
),
|
||||||
|
|
||||||
getInvoices: adminProcedure.query(async ({ ctx }) => {
|
getInvoices: adminProcedure.query(async ({ ctx }) => {
|
||||||
const user = await findUserById(ctx.user.ownerId);
|
const user = await findUserById(ctx.user.ownerId);
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ const { handler, api } = betterAuth({
|
|||||||
emailAndPassword: {
|
emailAndPassword: {
|
||||||
enabled: true,
|
enabled: true,
|
||||||
autoSignIn: !IS_CLOUD,
|
autoSignIn: !IS_CLOUD,
|
||||||
requireEmailVerification: IS_CLOUD,
|
requireEmailVerification: IS_CLOUD && process.env.NODE_ENV === "production",
|
||||||
password: {
|
password: {
|
||||||
async hash(password) {
|
async hash(password) {
|
||||||
return bcrypt.hashSync(password, 10);
|
return bcrypt.hashSync(password, 10);
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ export const runWebServerBackup = async (backup: BackupSchedule) => {
|
|||||||
await execAsync(cleanupCommand);
|
await execAsync(cleanupCommand);
|
||||||
|
|
||||||
await execAsync(
|
await execAsync(
|
||||||
`rsync -a --ignore-errors --no-specials --no-devices ${BASE_PATH}/ ${tempDir}/filesystem/`,
|
`rsync -a --ignore-errors --no-specials --no-devices --exclude='volume-backups/' ${BASE_PATH}/ ${tempDir}/filesystem/`,
|
||||||
);
|
);
|
||||||
|
|
||||||
writeStream.write("Copied filesystem to temp directory\n");
|
writeStream.write("Copied filesystem to temp directory\n");
|
||||||
|
|||||||
@@ -182,7 +182,11 @@ export const mechanizeDockerContainer = async (
|
|||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.log(error);
|
console.log(error);
|
||||||
await docker.createService(settings);
|
if (authConfig) {
|
||||||
|
await docker.createService(authConfig, settings);
|
||||||
|
} else {
|
||||||
|
await docker.createService(settings);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ export const sendDatabaseBackupNotifications = async ({
|
|||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
name: decorate("`⚠️`", "Error Message"),
|
name: decorate("`⚠️`", "Error Message"),
|
||||||
value: `\`\`\`${errorMessage}\`\`\``,
|
value: `\`\`\`${errorMessage.length > 1010 ? `${errorMessage.substring(0, 1010)}...` : errorMessage}\`\`\``,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ export const sendVolumeBackupNotifications = async ({
|
|||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
name: decorate("`⚠️`", "Error Message"),
|
name: decorate("`⚠️`", "Error Message"),
|
||||||
value: `\`\`\`${errorMessage}\`\`\``,
|
value: `\`\`\`${errorMessage.substring(0, 1010)}\`\`\``,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ export const backupVolume = async (
|
|||||||
|
|
||||||
const rcloneCommand = `rclone copyto ${rcloneFlags.join(" ")} "${volumeBackupPath}/${backupFileName}" "${rcloneDestination}"`;
|
const rcloneCommand = `rclone copyto ${rcloneFlags.join(" ")} "${volumeBackupPath}/${backupFileName}" "${rcloneDestination}"`;
|
||||||
|
|
||||||
const baseCommand = `
|
const backupCommand = `
|
||||||
set -e
|
set -e
|
||||||
echo "Volume name: ${volumeName}"
|
echo "Volume name: ${volumeName}"
|
||||||
echo "Backup file name: ${backupFileName}"
|
echo "Backup file name: ${backupFileName}"
|
||||||
@@ -52,6 +52,9 @@ export const backupVolume = async (
|
|||||||
ubuntu \
|
ubuntu \
|
||||||
bash -c "cd /volume_data && tar cvf /backup/${backupFileName} ."
|
bash -c "cd /volume_data && tar cvf /backup/${backupFileName} ."
|
||||||
echo "Volume backup done ✅"
|
echo "Volume backup done ✅"
|
||||||
|
`;
|
||||||
|
|
||||||
|
const uploadCommand = `
|
||||||
echo "Starting upload to S3..."
|
echo "Starting upload to S3..."
|
||||||
${rcloneCommand}
|
${rcloneCommand}
|
||||||
echo "Upload to S3 done ✅"
|
echo "Upload to S3 done ✅"
|
||||||
@@ -61,7 +64,10 @@ export const backupVolume = async (
|
|||||||
`;
|
`;
|
||||||
|
|
||||||
if (!turnOff) {
|
if (!turnOff) {
|
||||||
return baseCommand;
|
return `
|
||||||
|
${backupCommand}
|
||||||
|
${uploadCommand}
|
||||||
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const serviceLockId =
|
const serviceLockId =
|
||||||
@@ -110,9 +116,10 @@ export const backupVolume = async (
|
|||||||
ACTUAL_REPLICAS=$(docker service inspect ${volumeBackup.application?.appName} --format "{{.Spec.Mode.Replicated.Replicas}}")
|
ACTUAL_REPLICAS=$(docker service inspect ${volumeBackup.application?.appName} --format "{{.Spec.Mode.Replicated.Replicas}}")
|
||||||
echo "Actual replicas: $ACTUAL_REPLICAS"
|
echo "Actual replicas: $ACTUAL_REPLICAS"
|
||||||
docker service update --replicas=0 ${volumeBackup.application?.appName}
|
docker service update --replicas=0 ${volumeBackup.application?.appName}
|
||||||
${baseCommand}
|
${backupCommand}
|
||||||
echo "Starting application to $ACTUAL_REPLICAS replicas"
|
echo "Starting application to $ACTUAL_REPLICAS replicas"
|
||||||
docker service update --replicas=$ACTUAL_REPLICAS --with-registry-auth ${volumeBackup.application?.appName}
|
docker service update --replicas=$ACTUAL_REPLICAS --with-registry-auth ${volumeBackup.application?.appName}
|
||||||
|
${uploadCommand}
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
if (serviceType === "compose") {
|
if (serviceType === "compose") {
|
||||||
@@ -147,8 +154,9 @@ export const backupVolume = async (
|
|||||||
}
|
}
|
||||||
return lockWrapper(`
|
return lockWrapper(`
|
||||||
${stopCommand}
|
${stopCommand}
|
||||||
${baseCommand}
|
${backupCommand}
|
||||||
${startCommand}
|
${startCommand}
|
||||||
|
${uploadCommand}
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user