mirror of
https://github.com/Dokploy/dokploy.git
synced 2026-06-18 13:45:23 +02:00
Compare commits
1 Commits
v0.28.7
...
dosu/doc-u
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d287e2a56d |
@@ -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`.
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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]) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "dokploy",
|
||||
"version": "v0.28.7",
|
||||
"version": "v0.28.6",
|
||||
"private": true,
|
||||
"license": "Apache-2.0",
|
||||
"type": "module",
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -153,7 +153,7 @@ export const sendDatabaseBackupNotifications = async ({
|
||||
? [
|
||||
{
|
||||
name: decorate("`⚠️`", "Error Message"),
|
||||
value: `\`\`\`${errorMessage.length > 1010 ? `${errorMessage.substring(0, 1010)}...` : errorMessage}\`\`\``,
|
||||
value: `\`\`\`${errorMessage}\`\`\``,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
|
||||
@@ -161,7 +161,7 @@ export const sendVolumeBackupNotifications = async ({
|
||||
? [
|
||||
{
|
||||
name: decorate("`⚠️`", "Error Message"),
|
||||
value: `\`\`\`${errorMessage.substring(0, 1010)}\`\`\``,
|
||||
value: `\`\`\`${errorMessage}\`\`\``,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
|
||||
@@ -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}
|
||||
`);
|
||||
}
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user