mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-16 04:35:24 +02:00
Compare commits
21 Commits
dosu/doc-u
...
v0.28.8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d0c92d84ef | ||
|
|
72974e00a6 | ||
|
|
d96e2bbeb7 | ||
|
|
a45d8ee8f4 | ||
|
|
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();
|
||||
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 [upgradeTier, setUpgradeTier] = useState<"hobby" | "startup" | null>(
|
||||
null,
|
||||
@@ -111,6 +114,12 @@ export const ShowBilling = () => {
|
||||
productId: string,
|
||||
) => {
|
||||
const stripe = await stripePromise;
|
||||
const serverQuantity =
|
||||
tier === "startup"
|
||||
? startupServerQuantity
|
||||
: tier === "hobby"
|
||||
? hobbyServerQuantity
|
||||
: hobbyServerQuantity;
|
||||
if (data && data.subscriptions.length === 0) {
|
||||
createCheckoutSession({
|
||||
tier,
|
||||
@@ -679,7 +688,7 @@ export const ShowBilling = () => {
|
||||
<p className="text-2xl font-semibold text-foreground">
|
||||
$
|
||||
{calculatePriceHobby(
|
||||
serverQuantity,
|
||||
hobbyServerQuantity,
|
||||
isAnnual,
|
||||
).toFixed(2)}
|
||||
/{isAnnual ? "yr" : "mo"}
|
||||
@@ -692,7 +701,8 @@ export const ShowBilling = () => {
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
$
|
||||
{(
|
||||
calculatePriceHobby(serverQuantity, true) / 12
|
||||
calculatePriceHobby(hobbyServerQuantity, true) /
|
||||
12
|
||||
).toFixed(2)}
|
||||
/mo
|
||||
</p>
|
||||
@@ -724,19 +734,19 @@ export const ShowBilling = () => {
|
||||
Servers:
|
||||
</span>
|
||||
<Button
|
||||
disabled={serverQuantity <= 1}
|
||||
disabled={hobbyServerQuantity <= 1}
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() =>
|
||||
setServerQuantity((q) => Math.max(1, q - 1))
|
||||
setHobbyServerQuantity((q) => Math.max(1, q - 1))
|
||||
}
|
||||
>
|
||||
<MinusIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
<NumberInput
|
||||
value={serverQuantity}
|
||||
value={hobbyServerQuantity}
|
||||
onChange={(e) =>
|
||||
setServerQuantity(
|
||||
setHobbyServerQuantity(
|
||||
Math.max(
|
||||
1,
|
||||
Number(
|
||||
@@ -750,7 +760,7 @@ export const ShowBilling = () => {
|
||||
<Button
|
||||
variant="outline"
|
||||
size="icon"
|
||||
onClick={() => setServerQuantity((q) => q + 1)}
|
||||
onClick={() => setHobbyServerQuantity((q) => q + 1)}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -775,7 +785,7 @@ export const ShowBilling = () => {
|
||||
onClick={() =>
|
||||
handleCheckout("hobby", data!.hobbyProductId!)
|
||||
}
|
||||
disabled={serverQuantity < 1}
|
||||
disabled={hobbyServerQuantity < 1}
|
||||
>
|
||||
Get Started
|
||||
</Button>
|
||||
@@ -806,7 +816,7 @@ export const ShowBilling = () => {
|
||||
<p className="text-2xl font-semibold text-foreground">
|
||||
$
|
||||
{calculatePriceStartup(
|
||||
serverQuantity,
|
||||
startupServerQuantity,
|
||||
isAnnual,
|
||||
).toFixed(2)}
|
||||
/{isAnnual ? "yr" : "mo"}
|
||||
@@ -819,7 +829,10 @@ export const ShowBilling = () => {
|
||||
<p className="text-xs text-muted-foreground mt-2">
|
||||
$
|
||||
{(
|
||||
calculatePriceStartup(serverQuantity, true) / 12
|
||||
calculatePriceStartup(
|
||||
startupServerQuantity,
|
||||
true,
|
||||
) / 12
|
||||
).toFixed(2)}
|
||||
/mo
|
||||
</p>
|
||||
@@ -856,13 +869,14 @@ export const ShowBilling = () => {
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
disabled={
|
||||
serverQuantity <= STARTUP_SERVERS_INCLUDED
|
||||
startupServerQuantity <=
|
||||
STARTUP_SERVERS_INCLUDED
|
||||
}
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-8 w-8"
|
||||
onClick={() =>
|
||||
setServerQuantity((q) =>
|
||||
setStartupServerQuantity((q) =>
|
||||
Math.max(STARTUP_SERVERS_INCLUDED, q - 1),
|
||||
)
|
||||
}
|
||||
@@ -870,9 +884,9 @@ export const ShowBilling = () => {
|
||||
<MinusIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
<NumberInput
|
||||
value={serverQuantity}
|
||||
value={startupServerQuantity}
|
||||
onChange={(e) =>
|
||||
setServerQuantity(
|
||||
setStartupServerQuantity(
|
||||
Math.max(
|
||||
STARTUP_SERVERS_INCLUDED,
|
||||
Number(
|
||||
@@ -887,7 +901,9 @@ export const ShowBilling = () => {
|
||||
variant="outline"
|
||||
size="icon"
|
||||
className="h-8 w-8"
|
||||
onClick={() => setServerQuantity((q) => q + 1)}
|
||||
onClick={() =>
|
||||
setStartupServerQuantity((q) => q + 1)
|
||||
}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
@@ -917,7 +933,7 @@ export const ShowBilling = () => {
|
||||
)
|
||||
}
|
||||
disabled={
|
||||
serverQuantity < STARTUP_SERVERS_INCLUDED
|
||||
startupServerQuantity < STARTUP_SERVERS_INCLUDED
|
||||
}
|
||||
>
|
||||
Get Started
|
||||
@@ -1009,7 +1025,7 @@ export const ShowBilling = () => {
|
||||
<p className="text-2xl font-semibold tracking-tight text-primary ">
|
||||
${" "}
|
||||
{calculatePrice(
|
||||
serverQuantity,
|
||||
hobbyServerQuantity,
|
||||
isAnnual,
|
||||
).toFixed(2)}{" "}
|
||||
USD
|
||||
@@ -1018,7 +1034,10 @@ export const ShowBilling = () => {
|
||||
<p className="text-base font-semibold tracking-tight text-muted-foreground">
|
||||
${" "}
|
||||
{(
|
||||
calculatePrice(serverQuantity, isAnnual) / 12
|
||||
calculatePrice(
|
||||
hobbyServerQuantity,
|
||||
isAnnual,
|
||||
) / 12
|
||||
).toFixed(2)}{" "}
|
||||
/ Month USD
|
||||
</p>
|
||||
@@ -1026,9 +1045,10 @@ export const ShowBilling = () => {
|
||||
) : (
|
||||
<p className="text-2xl font-semibold tracking-tight text-primary ">
|
||||
${" "}
|
||||
{calculatePrice(serverQuantity, isAnnual).toFixed(
|
||||
2,
|
||||
)}{" "}
|
||||
{calculatePrice(
|
||||
hobbyServerQuantity,
|
||||
isAnnual,
|
||||
).toFixed(2)}{" "}
|
||||
USD
|
||||
</p>
|
||||
)}
|
||||
@@ -1071,26 +1091,28 @@ export const ShowBilling = () => {
|
||||
<div className="flex flex-col gap-2 mt-4">
|
||||
<div className="flex items-center gap-2 justify-center">
|
||||
<span className="text-sm text-muted-foreground">
|
||||
{serverQuantity} Servers
|
||||
{hobbyServerQuantity} Servers
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
disabled={serverQuantity <= 1}
|
||||
disabled={hobbyServerQuantity <= 1}
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
if (serverQuantity <= 1) return;
|
||||
if (hobbyServerQuantity <= 1) return;
|
||||
|
||||
setServerQuantity(serverQuantity - 1);
|
||||
setHobbyServerQuantity(
|
||||
hobbyServerQuantity - 1,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<MinusIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
<NumberInput
|
||||
value={serverQuantity}
|
||||
value={hobbyServerQuantity}
|
||||
onChange={(e) => {
|
||||
setServerQuantity(
|
||||
setHobbyServerQuantity(
|
||||
e.target.value as unknown as number,
|
||||
);
|
||||
}}
|
||||
@@ -1099,7 +1121,9 @@ export const ShowBilling = () => {
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setServerQuantity(serverQuantity + 1);
|
||||
setHobbyServerQuantity(
|
||||
hobbyServerQuantity + 1,
|
||||
);
|
||||
}}
|
||||
>
|
||||
<PlusIcon className="h-4 w-4" />
|
||||
@@ -1125,7 +1149,7 @@ export const ShowBilling = () => {
|
||||
onClick={async () => {
|
||||
handleCheckout("legacy", product.id);
|
||||
}}
|
||||
disabled={serverQuantity < 1}
|
||||
disabled={hobbyServerQuantity < 1}
|
||||
>
|
||||
Subscribe
|
||||
</Button>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { format } from "date-fns";
|
||||
import { Loader2, MoreHorizontal, Users } from "lucide-react";
|
||||
import { toast } from "sonner";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { Badge } from "@/components/ui/badge";
|
||||
import { Button } from "@/components/ui/button";
|
||||
@@ -36,10 +37,19 @@ export const ShowUsers = () => {
|
||||
const { data, isPending, refetch } = api.user.all.useQuery();
|
||||
const { mutateAsync } = api.user.remove.useMutation();
|
||||
const { data: permissions } = api.user.getPermissions.useQuery();
|
||||
const { data: hasValidLicense } =
|
||||
api.licenseKey.haveValidLicenseKey.useQuery();
|
||||
|
||||
const utils = api.useUtils();
|
||||
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 (
|
||||
<div className="w-full">
|
||||
<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 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>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
|
||||
@@ -1,13 +1,20 @@
|
||||
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 { useForm } from "react-hook-form";
|
||||
import { toast } from "sonner";
|
||||
import { z } from "zod";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { EnterpriseFeatureGate } from "@/components/proprietary/enterprise-feature-gate";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { AlertBlock } from "@/components/shared/alert-block";
|
||||
import { DialogAction } from "@/components/shared/dialog-action";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
@@ -24,11 +31,6 @@ import {
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import {
|
||||
Form,
|
||||
FormControl,
|
||||
@@ -38,6 +40,11 @@ import {
|
||||
FormMessage,
|
||||
} from "@/components/ui/form";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import {
|
||||
Popover,
|
||||
PopoverContent,
|
||||
PopoverTrigger,
|
||||
} from "@/components/ui/popover";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
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) */
|
||||
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({
|
||||
roleName: z
|
||||
.string()
|
||||
@@ -552,7 +667,7 @@ function HandleCustomRole({
|
||||
</Button>
|
||||
)}
|
||||
</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>
|
||||
<DialogTitle>
|
||||
{isEdit ? "Edit Role" : "Create Custom Role"}
|
||||
@@ -587,6 +702,32 @@ function HandleCustomRole({
|
||||
/>
|
||||
</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
|
||||
resources={visibleResources}
|
||||
permissions={permissions}
|
||||
@@ -843,7 +984,7 @@ function PermissionEditor({
|
||||
onToggle: (resource: string, action: string) => void;
|
||||
}) {
|
||||
return (
|
||||
<div className="space-y-3">
|
||||
<div className="space-y-3 mt-4">
|
||||
<p className="text-sm font-medium">Permissions</p>
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
|
||||
{resources.map(([resource, actions]) => {
|
||||
|
||||
5
apps/dokploy/drizzle/0150_nappy_blue_blade.sql
Normal file
5
apps/dokploy/drizzle/0150_nappy_blue_blade.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
ALTER TABLE "apikey" ALTER COLUMN "user_id" DROP NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "apikey" ADD COLUMN "config_id" text DEFAULT 'default' NOT NULL;--> statement-breakpoint
|
||||
ALTER TABLE "apikey" ADD COLUMN "reference_id" text;--> statement-breakpoint
|
||||
UPDATE "apikey" SET "reference_id" = "user_id" WHERE "reference_id" IS NULL;--> statement-breakpoint
|
||||
ALTER TABLE "apikey" ALTER COLUMN "reference_id" SET NOT NULL;
|
||||
4
apps/dokploy/drizzle/0151_modern_sunfire.sql
Normal file
4
apps/dokploy/drizzle/0151_modern_sunfire.sql
Normal file
@@ -0,0 +1,4 @@
|
||||
ALTER TABLE "apikey" DROP CONSTRAINT "apikey_user_id_user_id_fk";
|
||||
--> statement-breakpoint
|
||||
ALTER TABLE "apikey" ADD CONSTRAINT "apikey_reference_id_user_id_fk" FOREIGN KEY ("reference_id") REFERENCES "public"."user"("id") ON DELETE cascade ON UPDATE no action;--> statement-breakpoint
|
||||
ALTER TABLE "apikey" DROP COLUMN "user_id";
|
||||
7728
apps/dokploy/drizzle/meta/0150_snapshot.json
Normal file
7728
apps/dokploy/drizzle/meta/0150_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
7722
apps/dokploy/drizzle/meta/0151_snapshot.json
Normal file
7722
apps/dokploy/drizzle/meta/0151_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1051,6 +1051,20 @@
|
||||
"when": 1773637297592,
|
||||
"tag": "0149_rare_radioactive_man",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 150,
|
||||
"version": "7",
|
||||
"when": 1773870095817,
|
||||
"tag": "0150_nappy_blue_blade",
|
||||
"breakpoints": true
|
||||
},
|
||||
{
|
||||
"idx": 151,
|
||||
"version": "7",
|
||||
"when": 1773872561300,
|
||||
"tag": "0151_modern_sunfire",
|
||||
"breakpoints": true
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dokploy",
|
||||
"version": "v0.28.6",
|
||||
"version": "v0.28.8",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
|
||||
@@ -21,7 +21,12 @@ import {
|
||||
STARTUP_PRODUCT_ID,
|
||||
WEBSITE_URL,
|
||||
} from "@/server/utils/stripe";
|
||||
import { adminProcedure, createTRPCRouter, protectedProcedure } from "../trpc";
|
||||
import {
|
||||
adminProcedure,
|
||||
createTRPCRouter,
|
||||
protectedProcedure,
|
||||
withPermission,
|
||||
} from "../trpc";
|
||||
|
||||
export const stripeRouter = createTRPCRouter({
|
||||
/** 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 };
|
||||
}),
|
||||
|
||||
canCreateMoreServers: adminProcedure.query(async ({ ctx }) => {
|
||||
const user = await findUserById(ctx.user.ownerId);
|
||||
const servers = await findServersByUserId(user.id);
|
||||
canCreateMoreServers: withPermission("server", "create").query(
|
||||
async ({ ctx }) => {
|
||||
const user = await findUserById(ctx.user.ownerId);
|
||||
const servers = await findServersByUserId(user.id);
|
||||
|
||||
if (!IS_CLOUD) {
|
||||
return true;
|
||||
}
|
||||
if (!IS_CLOUD) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return servers.length < user.serversQuantity;
|
||||
}),
|
||||
return servers.length < user.serversQuantity;
|
||||
},
|
||||
),
|
||||
|
||||
getInvoices: adminProcedure.query(async ({ ctx }) => {
|
||||
const user = await findUserById(ctx.user.ownerId);
|
||||
|
||||
@@ -465,7 +465,7 @@ export const userRouter = createTRPCRouter({
|
||||
});
|
||||
}
|
||||
|
||||
if (apiKeyToDelete.userId !== ctx.user.id) {
|
||||
if (apiKeyToDelete.referenceId !== ctx.user.id) {
|
||||
throw new TRPCError({
|
||||
code: "UNAUTHORIZED",
|
||||
message: "You are not authorized to delete this API key",
|
||||
|
||||
@@ -214,7 +214,8 @@ export const apikey = pgTable("apikey", {
|
||||
start: text("start"),
|
||||
prefix: text("prefix"),
|
||||
key: text("key").notNull(),
|
||||
userId: text("user_id")
|
||||
configId: text("config_id").default("default").notNull(),
|
||||
referenceId: text("reference_id")
|
||||
.notNull()
|
||||
.references(() => user.id, { onDelete: "cascade" }),
|
||||
refillInterval: integer("refill_interval"),
|
||||
@@ -236,7 +237,7 @@ export const apikey = pgTable("apikey", {
|
||||
|
||||
export const apikeyRelations = relations(apikey, ({ one }) => ({
|
||||
user: one(user, {
|
||||
fields: [apikey.userId],
|
||||
fields: [apikey.referenceId],
|
||||
references: [user.id],
|
||||
}),
|
||||
}));
|
||||
|
||||
@@ -116,7 +116,7 @@ const { handler, api } = betterAuth({
|
||||
emailAndPassword: {
|
||||
enabled: true,
|
||||
autoSignIn: !IS_CLOUD,
|
||||
requireEmailVerification: IS_CLOUD,
|
||||
requireEmailVerification: IS_CLOUD && process.env.NODE_ENV === "production",
|
||||
password: {
|
||||
async hash(password) {
|
||||
return bcrypt.hashSync(password, 10);
|
||||
@@ -367,6 +367,7 @@ const { handler, api } = betterAuth({
|
||||
plugins: [
|
||||
apiKey({
|
||||
enableMetadata: true,
|
||||
references: "user",
|
||||
}),
|
||||
sso(),
|
||||
twoFactor(),
|
||||
|
||||
@@ -432,7 +432,7 @@ export const createApiKey = async (
|
||||
refillInterval?: number;
|
||||
},
|
||||
) => {
|
||||
const apiKey = await auth.createApiKey({
|
||||
const result = await auth.createApiKey({
|
||||
body: {
|
||||
name: input.name,
|
||||
expiresIn: input.expiresIn,
|
||||
@@ -450,10 +450,9 @@ export const createApiKey = async (
|
||||
if (input.metadata) {
|
||||
await db
|
||||
.update(apikey)
|
||||
.set({
|
||||
metadata: JSON.stringify(input.metadata),
|
||||
})
|
||||
.where(eq(apikey.id, apiKey.id));
|
||||
.set({ metadata: JSON.stringify(input.metadata) })
|
||||
.where(eq(apikey.id, result.id));
|
||||
}
|
||||
return apiKey;
|
||||
|
||||
return result;
|
||||
};
|
||||
|
||||
@@ -67,7 +67,7 @@ export const runWebServerBackup = async (backup: BackupSchedule) => {
|
||||
await execAsync(cleanupCommand);
|
||||
|
||||
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");
|
||||
|
||||
@@ -182,7 +182,11 @@ export const mechanizeDockerContainer = async (
|
||||
});
|
||||
} catch (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"),
|
||||
value: `\`\`\`${errorMessage}\`\`\``,
|
||||
value: `\`\`\`${errorMessage.length > 1010 ? `${errorMessage.substring(0, 1010)}...` : errorMessage}\`\`\``,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
|
||||
@@ -161,7 +161,7 @@ export const sendVolumeBackupNotifications = async ({
|
||||
? [
|
||||
{
|
||||
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 baseCommand = `
|
||||
const backupCommand = `
|
||||
set -e
|
||||
echo "Volume name: ${volumeName}"
|
||||
echo "Backup file name: ${backupFileName}"
|
||||
@@ -52,6 +52,9 @@ export const backupVolume = async (
|
||||
ubuntu \
|
||||
bash -c "cd /volume_data && tar cvf /backup/${backupFileName} ."
|
||||
echo "Volume backup done ✅"
|
||||
`;
|
||||
|
||||
const uploadCommand = `
|
||||
echo "Starting upload to S3..."
|
||||
${rcloneCommand}
|
||||
echo "Upload to S3 done ✅"
|
||||
@@ -61,7 +64,10 @@ export const backupVolume = async (
|
||||
`;
|
||||
|
||||
if (!turnOff) {
|
||||
return baseCommand;
|
||||
return `
|
||||
${backupCommand}
|
||||
${uploadCommand}
|
||||
`;
|
||||
}
|
||||
|
||||
const serviceLockId =
|
||||
@@ -110,9 +116,10 @@ export const backupVolume = async (
|
||||
ACTUAL_REPLICAS=$(docker service inspect ${volumeBackup.application?.appName} --format "{{.Spec.Mode.Replicated.Replicas}}")
|
||||
echo "Actual replicas: $ACTUAL_REPLICAS"
|
||||
docker service update --replicas=0 ${volumeBackup.application?.appName}
|
||||
${baseCommand}
|
||||
${backupCommand}
|
||||
echo "Starting application to $ACTUAL_REPLICAS replicas"
|
||||
docker service update --replicas=$ACTUAL_REPLICAS --with-registry-auth ${volumeBackup.application?.appName}
|
||||
${uploadCommand}
|
||||
`);
|
||||
}
|
||||
if (serviceType === "compose") {
|
||||
@@ -147,8 +154,9 @@ export const backupVolume = async (
|
||||
}
|
||||
return lockWrapper(`
|
||||
${stopCommand}
|
||||
${baseCommand}
|
||||
${backupCommand}
|
||||
${startCommand}
|
||||
${uploadCommand}
|
||||
`);
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user