Compare commits

..

1 Commits

Author SHA1 Message Date
dosubot[bot]
d287e2a56d docs: restructure database update endpoints documentation 2026-03-16 22:22:27 +00:00
12 changed files with 433 additions and 265 deletions

View File

@@ -6,3 +6,377 @@ npm run dev
```
open http://localhost:3000
```
## Environment Variables
The API server requires the following environment variables for configuration:
### Inngest Configuration
Required for the GET /jobs endpoint to list deployment jobs:
- **INNGEST_BASE_URL** - The base URL for the Inngest instance
- Self-hosted: `http://localhost:8288`
- Production: `https://dev-inngest.dokploy.com`
- **INNGEST_SIGNING_KEY** - The signing key for authenticating with Inngest
Optional configuration for filtering and pagination:
- **INNGEST_EVENTS_RECEIVED_AFTER** (optional) - An RFC3339 timestamp to filter events received after a specific date (e.g., `2024-01-01T00:00:00Z`). If unset, no date filter is applied.
- **INNGEST_JOBS_MAX_EVENTS** (optional) - Maximum number of events to fetch when listing jobs. Default is 100, maximum is 10000. Used for pagination with cursor.
### Lemon Squeezy Integration
- **LEMON_SQUEEZY_API_KEY** - API key for Lemon Squeezy integration
- **LEMON_SQUEEZY_STORE_ID** - Store ID for Lemon Squeezy integration
### Docker Configuration
Optional configuration for customizing Docker daemon connections:
- **DOCKER_API_VERSION** (optional) - Specifies which Docker API version to use when connecting to the Docker daemon. If not set, the Docker client uses the default API version.
- **DOKPLOY_DOCKER_HOST** (optional) - Specifies the Docker daemon host to connect to. If not set, uses the default Docker socket connection.
- **DOKPLOY_DOCKER_PORT** (optional) - Specifies the port for connecting to the Docker daemon. If not set, uses the default port.
These variables allow advanced users to customize how the Dokploy API server connects to Docker, which can be useful for connecting to remote Docker daemons or using specific API versions.
## API Endpoints
### GET /jobs
Lists deployment jobs (Inngest runs) for a specified server.
**Query Parameters:**
- `serverId` (required) - The ID of the server to list deployment jobs for
**Response:**
Returns an array of deployment job objects with the same shape as BullMQ queue jobs:
```json
[
{
"id": "string",
"name": "string",
"data": {},
"timestamp": 0,
"processedOn": 0,
"finishedOn": 0,
"failedReason": "string",
"state": "string"
}
]
```
**Error Responses:**
- `400` - serverId is not provided
- `503` - INNGEST_BASE_URL is not configured
- `200` - Empty array on other errors
This endpoint is used by the UI to display deployment queue information in the dashboard.
## Search Endpoints
The following search endpoints provide flexible querying capabilities with pagination support. All search endpoints respect member permissions, returning only resources the user has access to.
### application.search
Search applications across name, appName, description, repository, owner, and dockerImage fields.
**Query Parameters:**
- `q` (optional string) - General search term that searches across name, appName, description, repository, owner, and dockerImage
- `name` (optional string) - Filter by application name
- `appName` (optional string) - Filter by app name
- `description` (optional string) - Filter by description
- `repository` (optional string) - Filter by repository
- `owner` (optional string) - Filter by owner
- `dockerImage` (optional string) - Filter by Docker image
- `projectId` (optional string) - Filter by project ID
- `environmentId` (optional string) - Filter by environment ID
- `limit` (number, default 20, min 1, max 100) - Maximum number of results
- `offset` (number, default 0, min 0) - Pagination offset
**Response:**
```json
{
"items": [
{
"applicationId": "string",
"name": "string",
"appName": "string",
"description": "string",
"environmentId": "string",
"applicationStatus": "string",
"sourceType": "string",
"createdAt": "string"
}
],
"total": 0
}
```
### compose.search
Search compose services with filtering by name, appName, and description.
**Query Parameters:**
- `q` (optional string) - General search term across name, appName, description
- `name` (optional string) - Filter by name
- `appName` (optional string) - Filter by app name
- `description` (optional string) - Filter by description
- `projectId` (optional string) - Filter by project ID
- `environmentId` (optional string) - Filter by environment ID
- `limit` (number, default 20, min 1, max 100) - Maximum results
- `offset` (number, default 0, min 0) - Pagination offset
**Response:**
```json
{
"items": [
{
"composeId": "string",
"name": "string",
"appName": "string",
"description": "string",
"environmentId": "string",
"composeStatus": "string",
"sourceType": "string",
"createdAt": "string"
}
],
"total": 0
}
```
### environment.search
Search environments by name and description.
**Query Parameters:**
- `q` (optional string) - General search term across name and description
- `name` (optional string) - Filter by name
- `description` (optional string) - Filter by description
- `projectId` (optional string) - Filter by project ID
- `limit` (number, default 20, min 1, max 100) - Maximum results
- `offset` (number, default 0, min 0) - Pagination offset
**Response:**
```json
{
"items": [
{
"environmentId": "string",
"name": "string",
"description": "string",
"createdAt": "string",
"env": "string",
"projectId": "string",
"isDefault": true
}
],
"total": 0
}
```
### project.search
Search projects by name and description.
**Query Parameters:**
- `q` (optional string) - General search term across name and description
- `name` (optional string) - Filter by name
- `description` (optional string) - Filter by description
- `limit` (number, default 20, min 1, max 100) - Maximum results
- `offset` (number, default 0, min 0) - Pagination offset
**Response:**
```json
{
"items": [
{
"projectId": "string",
"name": "string",
"description": "string",
"createdAt": "string",
"organizationId": "string",
"env": "string"
}
],
"total": 0
}
```
### Database Service Search Endpoints
The following database services all share the same search interface:
- **postgres.search**
- **mysql.search**
- **mariadb.search**
- **mongo.search**
- **redis.search**
**Query Parameters:**
- `q` (optional string) - General search term across name, appName, description
- `name` (optional string) - Filter by name
- `appName` (optional string) - Filter by app name
- `description` (optional string) - Filter by description
- `projectId` (optional string) - Filter by project ID
- `environmentId` (optional string) - Filter by environment ID
- `limit` (number, default 20, min 1, max 100) - Maximum results
- `offset` (number, default 0, min 0) - Pagination offset
**Response:**
```json
{
"items": [
{
"postgresId": "string",
"name": "string",
"appName": "string",
"description": "string",
"environmentId": "string",
"applicationStatus": "string",
"createdAt": "string"
}
],
"total": 0
}
```
*Note: The response shape is similar across all database services, with the ID field varying (e.g., `mysqlId`, `mariadbId`, `mongoId`, `redisId`).*
**Search Behavior:**
- All searches use case-insensitive pattern matching with wildcards
- Results are ordered by creation date (descending)
- Members only see services they have access to
- Returns total count for pagination UI
## Database Service Update Endpoints
The following database services support update operations with similar parameters:
- **postgres.update** (apiUpdatePostgres)
- **mysql.update** (apiUpdateMySql)
- **mariadb.update** (apiUpdateMariaDB)
- **mongo.update** (apiUpdateMongo)
- **redis.update** (apiUpdateRedis)
**Input Parameters:**
Each update endpoint accepts the following parameters (all optional except the service ID):
- `postgresId` / `mysqlId` / `mariadbId` / `mongoId` / `redisId` (required) - The ID of the database service to update
- `dockerImage` (optional string) - Custom Docker image to use for the database service
- Other service-specific parameters as defined in the schema
**Example Input:**
```json
{
"postgresId": "string",
"dockerImage": "postgres:16-alpine"
}
```
**Response:**
Returns the updated database service object.
**Purpose:**
The `dockerImage` parameter allows users to specify custom Docker images for database services, providing flexibility for version selection, custom builds, or alternative distributions.
## Whitelabeling Endpoints
The whitelabeling endpoints allow enterprise/self-hosted Dokploy instances to customize branding, logos, colors, and UI appearance. These endpoints are only available in self-hosted mode (not cloud).
### whitelabeling.get
Get the current whitelabeling configuration.
**Requirements:**
- Enterprise license required
- Only available for self-hosted (not cloud)
**Response:**
Returns the whitelabeling configuration object or null if not configured.
```json
{
"appName": "string | null",
"appDescription": "string | null",
"logoUrl": "string | null",
"faviconUrl": "string | null",
"primaryColor": "string | null",
"customCss": "string | null",
"loginLogoUrl": "string | null",
"supportUrl": "string | null",
"docsUrl": "string | null",
"errorPageTitle": "string | null",
"errorPageDescription": "string | null",
"metaTitle": "string | null",
"footerText": "string | null"
}
```
### whitelabeling.update
Update the whitelabeling configuration.
**Requirements:**
- Enterprise license required
- Owner role required
- Only available for self-hosted (not cloud)
**Input:**
```json
{
"whitelabelingConfig": {
"appName": "string | null",
"appDescription": "string | null",
"logoUrl": "string | null",
"faviconUrl": "string | null",
"primaryColor": "string | null",
"customCss": "string | null",
"loginLogoUrl": "string | null",
"supportUrl": "string | null",
"docsUrl": "string | null",
"errorPageTitle": "string | null",
"errorPageDescription": "string | null",
"metaTitle": "string | null",
"footerText": "string | null"
}
}
```
**Response:**
```json
{
"success": true
}
```
### whitelabeling.reset
Reset whitelabeling configuration to default values (all fields set to null).
**Requirements:**
- Enterprise license required
- Owner role required
- Only available for self-hosted (not cloud)
**Response:**
```json
{
"success": true
}
```
### whitelabeling.getPublic
Public endpoint to fetch whitelabeling configuration. This endpoint can be accessed without authentication, allowing the whitelabeling settings to be applied globally (including on the login page before auth).
**Requirements:**
- No authentication required
- Only available for self-hosted (not cloud)
**Response:**
Returns the whitelabeling configuration object or null if not configured. Response shape is identical to `whitelabeling.get`.

View File

@@ -91,10 +91,7 @@ export const ShowBilling = () => {
api.stripe.upgradeSubscription.useMutation();
const utils = api.useUtils();
const [hobbyServerQuantity, setHobbyServerQuantity] = useState(1);
const [startupServerQuantity, setStartupServerQuantity] = useState(
STARTUP_SERVERS_INCLUDED,
);
const [serverQuantity, setServerQuantity] = useState(3);
const [isAnnual, setIsAnnual] = useState(false);
const [upgradeTier, setUpgradeTier] = useState<"hobby" | "startup" | null>(
null,
@@ -114,12 +111,6 @@ 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,
@@ -688,7 +679,7 @@ export const ShowBilling = () => {
<p className="text-2xl font-semibold text-foreground">
$
{calculatePriceHobby(
hobbyServerQuantity,
serverQuantity,
isAnnual,
).toFixed(2)}
/{isAnnual ? "yr" : "mo"}
@@ -701,8 +692,7 @@ export const ShowBilling = () => {
<p className="text-xs text-muted-foreground mt-2">
$
{(
calculatePriceHobby(hobbyServerQuantity, true) /
12
calculatePriceHobby(serverQuantity, true) / 12
).toFixed(2)}
/mo
</p>
@@ -734,19 +724,19 @@ export const ShowBilling = () => {
Servers:
</span>
<Button
disabled={hobbyServerQuantity <= 1}
disabled={serverQuantity <= 1}
variant="outline"
size="icon"
onClick={() =>
setHobbyServerQuantity((q) => Math.max(1, q - 1))
setServerQuantity((q) => Math.max(1, q - 1))
}
>
<MinusIcon className="h-4 w-4" />
</Button>
<NumberInput
value={hobbyServerQuantity}
value={serverQuantity}
onChange={(e) =>
setHobbyServerQuantity(
setServerQuantity(
Math.max(
1,
Number(
@@ -760,7 +750,7 @@ export const ShowBilling = () => {
<Button
variant="outline"
size="icon"
onClick={() => setHobbyServerQuantity((q) => q + 1)}
onClick={() => setServerQuantity((q) => q + 1)}
>
<PlusIcon className="h-4 w-4" />
</Button>
@@ -785,7 +775,7 @@ export const ShowBilling = () => {
onClick={() =>
handleCheckout("hobby", data!.hobbyProductId!)
}
disabled={hobbyServerQuantity < 1}
disabled={serverQuantity < 1}
>
Get Started
</Button>
@@ -816,7 +806,7 @@ export const ShowBilling = () => {
<p className="text-2xl font-semibold text-foreground">
$
{calculatePriceStartup(
startupServerQuantity,
serverQuantity,
isAnnual,
).toFixed(2)}
/{isAnnual ? "yr" : "mo"}
@@ -829,10 +819,7 @@ export const ShowBilling = () => {
<p className="text-xs text-muted-foreground mt-2">
$
{(
calculatePriceStartup(
startupServerQuantity,
true,
) / 12
calculatePriceStartup(serverQuantity, true) / 12
).toFixed(2)}
/mo
</p>
@@ -869,14 +856,13 @@ export const ShowBilling = () => {
<div className="flex items-center gap-2">
<Button
disabled={
startupServerQuantity <=
STARTUP_SERVERS_INCLUDED
serverQuantity <= STARTUP_SERVERS_INCLUDED
}
variant="outline"
size="icon"
className="h-8 w-8"
onClick={() =>
setStartupServerQuantity((q) =>
setServerQuantity((q) =>
Math.max(STARTUP_SERVERS_INCLUDED, q - 1),
)
}
@@ -884,9 +870,9 @@ export const ShowBilling = () => {
<MinusIcon className="h-4 w-4" />
</Button>
<NumberInput
value={startupServerQuantity}
value={serverQuantity}
onChange={(e) =>
setStartupServerQuantity(
setServerQuantity(
Math.max(
STARTUP_SERVERS_INCLUDED,
Number(
@@ -901,9 +887,7 @@ export const ShowBilling = () => {
variant="outline"
size="icon"
className="h-8 w-8"
onClick={() =>
setStartupServerQuantity((q) => q + 1)
}
onClick={() => setServerQuantity((q) => q + 1)}
>
<PlusIcon className="h-4 w-4" />
</Button>
@@ -933,7 +917,7 @@ export const ShowBilling = () => {
)
}
disabled={
startupServerQuantity < STARTUP_SERVERS_INCLUDED
serverQuantity < STARTUP_SERVERS_INCLUDED
}
>
Get Started
@@ -1025,7 +1009,7 @@ export const ShowBilling = () => {
<p className="text-2xl font-semibold tracking-tight text-primary ">
${" "}
{calculatePrice(
hobbyServerQuantity,
serverQuantity,
isAnnual,
).toFixed(2)}{" "}
USD
@@ -1034,10 +1018,7 @@ export const ShowBilling = () => {
<p className="text-base font-semibold tracking-tight text-muted-foreground">
${" "}
{(
calculatePrice(
hobbyServerQuantity,
isAnnual,
) / 12
calculatePrice(serverQuantity, isAnnual) / 12
).toFixed(2)}{" "}
/ Month USD
</p>
@@ -1045,10 +1026,9 @@ export const ShowBilling = () => {
) : (
<p className="text-2xl font-semibold tracking-tight text-primary ">
${" "}
{calculatePrice(
hobbyServerQuantity,
isAnnual,
).toFixed(2)}{" "}
{calculatePrice(serverQuantity, isAnnual).toFixed(
2,
)}{" "}
USD
</p>
)}
@@ -1091,28 +1071,26 @@ 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">
{hobbyServerQuantity} Servers
{serverQuantity} Servers
</span>
</div>
<div className="flex items-center space-x-2">
<Button
disabled={hobbyServerQuantity <= 1}
disabled={serverQuantity <= 1}
variant="outline"
onClick={() => {
if (hobbyServerQuantity <= 1) return;
if (serverQuantity <= 1) return;
setHobbyServerQuantity(
hobbyServerQuantity - 1,
);
setServerQuantity(serverQuantity - 1);
}}
>
<MinusIcon className="h-4 w-4" />
</Button>
<NumberInput
value={hobbyServerQuantity}
value={serverQuantity}
onChange={(e) => {
setHobbyServerQuantity(
setServerQuantity(
e.target.value as unknown as number,
);
}}
@@ -1121,9 +1099,7 @@ export const ShowBilling = () => {
<Button
variant="outline"
onClick={() => {
setHobbyServerQuantity(
hobbyServerQuantity + 1,
);
setServerQuantity(serverQuantity + 1);
}}
>
<PlusIcon className="h-4 w-4" />
@@ -1149,7 +1125,7 @@ export const ShowBilling = () => {
onClick={async () => {
handleCheckout("legacy", product.id);
}}
disabled={hobbyServerQuantity < 1}
disabled={serverQuantity < 1}
>
Subscribe
</Button>

View File

@@ -1,7 +1,6 @@
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";
@@ -37,19 +36,10 @@ 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">
@@ -80,18 +70,6 @@ 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>

View File

@@ -1,20 +1,13 @@
import { standardSchemaResolver as zodResolver } from "@hookform/resolvers/standard-schema";
import {
Loader2,
PlusIcon,
ShieldCheck,
Sparkles,
TrashIcon,
Users,
} from "lucide-react";
import { Loader2, PlusIcon, ShieldCheck, 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 { EnterpriseFeatureGate } from "@/components/proprietary/enterprise-feature-gate";
import { AlertBlock } from "@/components/shared/alert-block";
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 {
Card,
CardContent,
@@ -31,6 +24,11 @@ import {
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@/components/ui/popover";
import {
Form,
FormControl,
@@ -40,11 +38,6 @@ 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";
@@ -414,114 +407,6 @@ 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()
@@ -667,7 +552,7 @@ function HandleCustomRole({
</Button>
)}
</DialogTrigger>
<DialogContent className="max-h-[85vh] sm:max-w-5xl overflow-y-auto space-y-2">
<DialogContent className="max-h-[85vh] sm:max-w-5xl overflow-y-auto">
<DialogHeader>
<DialogTitle>
{isEdit ? "Edit Role" : "Create Custom Role"}
@@ -702,32 +587,6 @@ 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}
@@ -984,7 +843,7 @@ function PermissionEditor({
onToggle: (resource: string, action: string) => void;
}) {
return (
<div className="space-y-3 mt-4">
<div className="space-y-3">
<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]) => {

View File

@@ -1,6 +1,6 @@
{
"name": "dokploy",
"version": "v0.28.7",
"version": "v0.28.6",
"private": true,
"license": "Apache-2.0",
"type": "module",

View File

@@ -21,12 +21,7 @@ import {
STARTUP_PRODUCT_ID,
WEBSITE_URL,
} from "@/server/utils/stripe";
import {
adminProcedure,
createTRPCRouter,
protectedProcedure,
withPermission,
} from "../trpc";
import { adminProcedure, createTRPCRouter, protectedProcedure } from "../trpc";
export const stripeRouter = createTRPCRouter({
/** Returns the current billing plan for the user's organization. Used to gate features like chat (Startup only). */
@@ -319,18 +314,16 @@ export const stripeRouter = createTRPCRouter({
return { ok: true };
}),
canCreateMoreServers: withPermission("server", "create").query(
async ({ ctx }) => {
const user = await findUserById(ctx.user.ownerId);
const servers = await findServersByUserId(user.id);
canCreateMoreServers: adminProcedure.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);

View File

@@ -116,7 +116,7 @@ const { handler, api } = betterAuth({
emailAndPassword: {
enabled: true,
autoSignIn: !IS_CLOUD,
requireEmailVerification: IS_CLOUD && process.env.NODE_ENV === "production",
requireEmailVerification: IS_CLOUD,
password: {
async hash(password) {
return bcrypt.hashSync(password, 10);

View File

@@ -67,7 +67,7 @@ export const runWebServerBackup = async (backup: BackupSchedule) => {
await execAsync(cleanupCommand);
await execAsync(
`rsync -a --ignore-errors --no-specials --no-devices --exclude='volume-backups/' ${BASE_PATH}/ ${tempDir}/filesystem/`,
`rsync -a --ignore-errors --no-specials --no-devices ${BASE_PATH}/ ${tempDir}/filesystem/`,
);
writeStream.write("Copied filesystem to temp directory\n");

View File

@@ -182,11 +182,7 @@ export const mechanizeDockerContainer = async (
});
} catch (error) {
console.log(error);
if (authConfig) {
await docker.createService(authConfig, settings);
} else {
await docker.createService(settings);
}
await docker.createService(settings);
}
};

View File

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

View File

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

View File

@@ -39,7 +39,7 @@ export const backupVolume = async (
const rcloneCommand = `rclone copyto ${rcloneFlags.join(" ")} "${volumeBackupPath}/${backupFileName}" "${rcloneDestination}"`;
const backupCommand = `
const baseCommand = `
set -e
echo "Volume name: ${volumeName}"
echo "Backup file name: ${backupFileName}"
@@ -52,9 +52,6 @@ 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 ✅"
@@ -64,10 +61,7 @@ export const backupVolume = async (
`;
if (!turnOff) {
return `
${backupCommand}
${uploadCommand}
`;
return baseCommand;
}
const serviceLockId =
@@ -116,10 +110,9 @@ 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}
${backupCommand}
${baseCommand}
echo "Starting application to $ACTUAL_REPLICAS replicas"
docker service update --replicas=$ACTUAL_REPLICAS --with-registry-auth ${volumeBackup.application?.appName}
${uploadCommand}
`);
}
if (serviceType === "compose") {
@@ -154,9 +147,8 @@ export const backupVolume = async (
}
return lockWrapper(`
${stopCommand}
${backupCommand}
${baseCommand}
${startCommand}
${uploadCommand}
`);
}
};