Compare commits

...

17 Commits

Author SHA1 Message Date
Mauricio Siu
9067452a38 feat: add role presets for custom role management
- Introduced predefined role presets with default permissions for viewer, developer, deployer, and devops roles to streamline custom role creation.
- Enhanced the UI to allow users to start from a preset role, improving the user experience in managing custom roles.
- Updated imports and adjusted component formatting for better readability.
2026-03-17 23:33:45 -06:00
Mauricio Siu
1fa4d5b2ba refactor: improve formatting and readability in billing and users components
- Enhanced code readability by adjusting formatting in the ShowBilling component, ensuring consistent line breaks and indentation.
- Updated the ShowUsers component to improve the layout of the warning message for users with custom roles without a valid license, maintaining clarity in the alert presentation.
2026-03-17 23:17:30 -06:00
Mauricio Siu
bade36ea9d feat: add alert for users with custom roles without a valid license
- Introduced an AlertBlock to notify users with custom roles that a valid Enterprise license is required for those roles to function.
- Implemented logic to check for users assigned to custom roles and display a warning if the license is invalid.
2026-03-17 23:16:17 -06:00
Mauricio Siu
0c22041623 refactor: update billing component to manage server quantities for hobby and startup tiers
- Replaced single server quantity state with separate states for hobby and startup server quantities.
- Adjusted calculations and UI elements to reflect the new state management for each tier.
- Ensured proper handling of server quantity in pricing calculations and button states.
2026-03-17 23:11:50 -06:00
Mauricio Siu
cccee05173 Merge pull request #4023 from Dokploy/4021-discord-error-notifications-fail-due-to-content-exceeding-max-embed-length
fix: truncate error message in backup notifications to 1010 characters
2026-03-17 22:47:35 -06:00
Mauricio Siu
9f9c8fccf2 Update packages/server/src/utils/notifications/database-backup.ts
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-03-17 22:47:26 -06:00
Mauricio Siu
ad2e53a67a fix: truncate error message in backup notifications to 1010 characters
- Updated the error message formatting in both database and volume backup notification functions to limit the displayed message length, ensuring better readability and preventing overflow.
2026-03-17 22:17:36 -06:00
Mauricio Siu
00f3853bd7 chore: remove settings.json file for command permissions in Claude
- Deleted the settings.json file that defined permissions for various Bash commands and the default mode for Claude.
2026-03-17 18:19:37 -06:00
Mauricio Siu
2880327e94 feat: add settings configuration for command permissions in Claude
- Introduced a new settings.json file to define permissions for various Bash commands and set the default mode to bypassPermissions.
- Updated the version in package.json to v0.28.7.
2026-03-17 18:18:04 -06:00
Mauricio Siu
827b84f57e Merge pull request #4001 from WalidDevIO/fix/volume-backup-turn-off
fix(volume-backups): restart container before S3 upload in volume backup
2026-03-17 08:53:02 -06:00
Mauricio Siu
11aa8fe0c5 Update packages/server/src/utils/volume-backups/backup.ts 2026-03-17 08:51:31 -06:00
autofix-ci[bot]
b9ac720d99 [autofix.ci] apply automated fixes 2026-03-17 06:25:03 +00:00
Mauricio Siu
77b0ff7bbf Merge pull request #4016 from Dokploy/4003-first-application-deploy-to-swarm-worker-fails-with-unauthorized-no-such-image-retry-succeeds
fix: handle optional authConfig in mechanizeDockerContainer function
2026-03-17 00:18:14 -06:00
Mauricio Siu
e7af2c0ebd fix: handle optional authConfig in mechanizeDockerContainer function
- Updated the mechanizeDockerContainer function to conditionally use authConfig when creating a Docker service, ensuring proper service creation based on authentication settings.
2026-03-17 00:17:51 -06:00
Mauricio Siu
6a1bedb90f Merge pull request #4015 from Dokploy/3971-abnormal-webserver-backup-file-size-increase-500-kb-4-gb-overnight
fix: exclude volume-backups from web server backup rsync command
2026-03-16 23:27:16 -06:00
Mauricio Siu
a2f142174b fix: exclude volume-backups from web server backup rsync command
- Updated the rsync command in the runWebServerBackup function to exclude the 'volume-backups/' directory, ensuring that unnecessary data is not copied during the backup process.
2026-03-16 23:26:33 -06:00
EL OUAZIZI Walid
2f37235aea fix(volume-backups): restart container before S3 upload in volume backup 2026-03-15 06:46:33 +01:00
11 changed files with 265 additions and 59 deletions

View File

@@ -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>

View File

@@ -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>

View File

@@ -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]) => {

View File

@@ -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",

View File

@@ -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);

View File

@@ -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);

View File

@@ -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");

View File

@@ -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);
}
} }
}; };

View File

@@ -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}\`\`\``,
}, },
] ]
: []), : []),

View File

@@ -161,7 +161,7 @@ export const sendVolumeBackupNotifications = async ({
? [ ? [
{ {
name: decorate("`⚠️`", "Error Message"), name: decorate("`⚠️`", "Error Message"),
value: `\`\`\`${errorMessage}\`\`\``, value: `\`\`\`${errorMessage.substring(0, 1010)}\`\`\``,
}, },
] ]
: []), : []),

View File

@@ -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}
`); `);
} }
}; };