mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Compare commits
56 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a93f18eb4a | ||
|
|
77e9617770 | ||
|
|
21e97b0175 | ||
|
|
a6618a14d5 | ||
|
|
59308ab013 | ||
|
|
e19c8d7a7a | ||
|
|
421c93795b | ||
|
|
182f908c31 | ||
|
|
20616363e9 | ||
|
|
d85073b26d | ||
|
|
303d1b1b87 | ||
|
|
60d4e1ba63 | ||
|
|
83d52b68f0 | ||
|
|
af3b1a27f4 | ||
|
|
7f94593c07 | ||
|
|
5df7654873 | ||
|
|
054836fd4c | ||
|
|
484ead1f1f | ||
|
|
fbada4c5de | ||
|
|
491113416b | ||
|
|
c42f5cb799 | ||
|
|
47aa223f87 | ||
|
|
cb586c9b74 | ||
|
|
554ac59b97 | ||
|
|
0247898876 | ||
|
|
fa053b4d1f | ||
|
|
bcc7afa3e4 | ||
|
|
647a5d05a6 | ||
|
|
e15d41f80d | ||
|
|
6c7c919d49 | ||
|
|
dfdedf9e48 | ||
|
|
7e76eb4dd1 | ||
|
|
c1f777e23e | ||
|
|
975d13c7e1 | ||
|
|
017bdd2778 | ||
|
|
548df8c0f4 | ||
|
|
1e6dbb5e8e | ||
|
|
431dadb6c2 | ||
|
|
22e42b62ad | ||
|
|
49ee8ce132 | ||
|
|
9bace8e58b | ||
|
|
c0afcaf3f6 | ||
|
|
53edf06476 | ||
|
|
255e9e4095 | ||
|
|
03f923c6e2 | ||
|
|
4685ef7439 | ||
|
|
626cfb80b4 | ||
|
|
9591fbff08 | ||
|
|
fbda00f059 | ||
|
|
1907e7e59c | ||
|
|
ffe7b04bea | ||
|
|
fe0a662afd | ||
|
|
319584d911 | ||
|
|
2d40b2dfe5 | ||
|
|
e32b713742 | ||
|
|
988357fb55 |
@@ -71,10 +71,9 @@ Run the command that will spin up all the required services and files.
|
|||||||
pnpm run dokploy:setup
|
pnpm run dokploy:setup
|
||||||
```
|
```
|
||||||
|
|
||||||
Build the server package (If you make any changes after in the packages/server folder, you need to rebuild and run this command)
|
Run this script
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm run server:build
|
pnpm run server:script
|
||||||
```
|
```
|
||||||
|
|
||||||
Now run the development server.
|
Now run the development server.
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --frozen-lockfile
|
|||||||
# Deploy only the dokploy app
|
# Deploy only the dokploy app
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
RUN pnpm --filter=@dokploy/server switch:prod
|
||||||
RUN pnpm --filter=@dokploy/server build
|
RUN pnpm --filter=@dokploy/server build
|
||||||
RUN pnpm --filter=./apps/dokploy run build
|
RUN pnpm --filter=./apps/dokploy run build
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --filter=@dokploy/server
|
|||||||
# Deploy only the dokploy app
|
# Deploy only the dokploy app
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
RUN pnpm --filter=@dokploy/server switch:prod
|
||||||
RUN pnpm --filter=@dokploy/server build
|
RUN pnpm --filter=@dokploy/server build
|
||||||
RUN pnpm --filter=./apps/dokploy run build
|
RUN pnpm --filter=./apps/dokploy run build
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --filter=@dokploy/server
|
|||||||
# Deploy only the dokploy app
|
# Deploy only the dokploy app
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
RUN pnpm --filter=@dokploy/server switch:prod
|
||||||
RUN pnpm --filter=@dokploy/server build
|
RUN pnpm --filter=@dokploy/server build
|
||||||
RUN pnpm --filter=./apps/schedules run build
|
RUN pnpm --filter=./apps/schedules run build
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --filter=@dokploy/server
|
|||||||
# Deploy only the dokploy app
|
# Deploy only the dokploy app
|
||||||
|
|
||||||
ENV NODE_ENV=production
|
ENV NODE_ENV=production
|
||||||
|
RUN pnpm --filter=@dokploy/server switch:prod
|
||||||
RUN pnpm --filter=@dokploy/server build
|
RUN pnpm --filter=@dokploy/server build
|
||||||
RUN pnpm --filter=./apps/api run build
|
RUN pnpm --filter=./apps/api run build
|
||||||
|
|
||||||
|
|||||||
22
README.md
22
README.md
@@ -15,29 +15,29 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
Dokploy is a free self-hostable Platform as a Service (PaaS) that simplifies the deployment and management of applications and databases.
|
Dokploy is a free, self-hostable Platform as a Service (PaaS) that simplifies the deployment and management of applications and databases.
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
Dokploy include multiples features to make your life easier.
|
Dokploy includes multiple features to make your life easier.
|
||||||
|
|
||||||
- **Applications**: Deploy any type of application (Node.js, PHP, Python, Go, Ruby, etc.).
|
- **Applications**: Deploy any type of application (Node.js, PHP, Python, Go, Ruby, etc.).
|
||||||
- **Databases**: Create and manage databases with support for MySQL, PostgreSQL, MongoDB, MariaDB, Redis.
|
- **Databases**: Create and manage databases with support for MySQL, PostgreSQL, MongoDB, MariaDB, and Redis.
|
||||||
- **Backups**: Automate backups for databases to a external storage destination.
|
- **Backups**: Automate backups for databases to an external storage destination.
|
||||||
- **Docker Compose**: Native support for Docker Compose to manage complex applications.
|
- **Docker Compose**: Native support for Docker Compose to manage complex applications.
|
||||||
- **Multi Node**: Scale applications to multiples nodes using docker swarm to manage the cluster.
|
- **Multi Node**: Scale applications to multiple nodes using Docker Swarm to manage the cluster.
|
||||||
- **Templates**: Deploy in a single click open source templates (Plausible, Pocketbase, Calcom, etc.).
|
- **Templates**: Deploy open-source templates (Plausible, Pocketbase, Calcom, etc.) with a single click.
|
||||||
- **Traefik Integration**: Automatically integrates with Traefik for routing and load balancing.
|
- **Traefik Integration**: Automatically integrates with Traefik for routing and load balancing.
|
||||||
- **Real-time Monitoring**: Monitor CPU, memory, storage, and network usage, for every resource.
|
- **Real-time Monitoring**: Monitor CPU, memory, storage, and network usage for every resource.
|
||||||
- **Docker Management**: Easily deploy and manage Docker containers.
|
- **Docker Management**: Easily deploy and manage Docker containers.
|
||||||
- **CLI/API**: Manage your applications and databases using the command line or through the API.
|
- **CLI/API**: Manage your applications and databases using the command line or through the API.
|
||||||
- **Notifications**: Get notified when your deployments are successful or failed (Slack, Discord, Telegram, Email, etc.)
|
- **Notifications**: Get notified when your deployments succeed or fail (via Slack, Discord, Telegram, Email, etc.).
|
||||||
- **Multi Server**: Deploy and manager your applications remotely to external servers.
|
- **Multi Server**: Deploy and manage your applications remotely to external servers.
|
||||||
- **Self-Hosted**: Self-host Dokploy on your VPS.
|
- **Self-Hosted**: Self-host Dokploy on your VPS.
|
||||||
|
|
||||||
## 🚀 Getting Started
|
## 🚀 Getting Started
|
||||||
|
|
||||||
To get started run the following command in a VPS:
|
To get started, run the following command on a VPS:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -sSL https://dokploy.com/install.sh | sh
|
curl -sSL https://dokploy.com/install.sh | sh
|
||||||
@@ -90,6 +90,8 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
|
|||||||
<div style="display: flex; gap: 30px; flex-wrap: wrap;">
|
<div style="display: flex; gap: 30px; flex-wrap: wrap;">
|
||||||
<a href="https://steamsets.com/?ref=dokploy"><img src="https://avatars.githubusercontent.com/u/111978405?s=200&v=4" width="60px" alt="Steamsets.com"/></a>
|
<a href="https://steamsets.com/?ref=dokploy"><img src="https://avatars.githubusercontent.com/u/111978405?s=200&v=4" width="60px" alt="Steamsets.com"/></a>
|
||||||
<a href="https://rivo.gg/?ref=dokploy"><img src="https://avatars.githubusercontent.com/u/126797452?s=200&v=4" width="60px" alt="Rivo.gg"/></a>
|
<a href="https://rivo.gg/?ref=dokploy"><img src="https://avatars.githubusercontent.com/u/126797452?s=200&v=4" width="60px" alt="Rivo.gg"/></a>
|
||||||
|
<a href="https://photoquest.wedding/?ref=dokploy"><img src="https://photoquest.wedding/favicon/android-chrome-512x512.png" width="60px" alt="Rivo.gg"/></a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
#### Organizations:
|
#### Organizations:
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const deployJobSchema = z.discriminatedUnion("applicationType", [
|
|||||||
server: z.boolean().optional(),
|
server: z.boolean().optional(),
|
||||||
type: z.enum(["deploy", "redeploy"]),
|
type: z.enum(["deploy", "redeploy"]),
|
||||||
applicationType: z.literal("application"),
|
applicationType: z.literal("application"),
|
||||||
serverId: z.string(),
|
serverId: z.string().min(1),
|
||||||
}),
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
composeId: z.string(),
|
composeId: z.string(),
|
||||||
@@ -17,7 +17,7 @@ export const deployJobSchema = z.discriminatedUnion("applicationType", [
|
|||||||
server: z.boolean().optional(),
|
server: z.boolean().optional(),
|
||||||
type: z.enum(["deploy", "redeploy"]),
|
type: z.enum(["deploy", "redeploy"]),
|
||||||
applicationType: z.literal("compose"),
|
applicationType: z.literal("compose"),
|
||||||
serverId: z.string(),
|
serverId: z.string().min(1),
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
rebuildRemoteCompose,
|
rebuildRemoteCompose,
|
||||||
updateApplicationStatus,
|
updateApplicationStatus,
|
||||||
updateCompose,
|
updateCompose,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server/dist";
|
||||||
import type { DeployJob } from "./schema";
|
import type { DeployJob } from "./schema";
|
||||||
import type { LemonSqueezyLicenseResponse } from "./types";
|
import type { LemonSqueezyLicenseResponse } from "./types";
|
||||||
|
|
||||||
|
|||||||
@@ -15,13 +15,13 @@ description: '学习如何在服务器上手动安装 Dokploy。'
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# 确保以根用户身份运行脚本
|
# Ensure the script is run as root
|
||||||
if [ "$(id -u)" != "0" ]; then
|
if [ "$(id -u)" != "0" ]; then
|
||||||
echo "This script must be run as root" >&2
|
echo "This script must be run as root" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 检查 Linux 操作系统(非 macOS 或 Docker 容器内的操作系统)
|
# Check for Linux OS (not macOS or inside a Docker container)
|
||||||
if [ "$(uname)" = "Darwin" ]; then
|
if [ "$(uname)" = "Darwin" ]; then
|
||||||
echo "This script must be run on Linux" >&2
|
echo "This script must be run on Linux" >&2
|
||||||
exit 1
|
exit 1
|
||||||
@@ -32,7 +32,7 @@ if [ -f /.dockerenv ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 检查端口是否被占用
|
# Check for occupied ports
|
||||||
if ss -tulnp | grep ':80 ' >/dev/null; then
|
if ss -tulnp | grep ':80 ' >/dev/null; then
|
||||||
echo "Error: Port 80 is already in use" >&2
|
echo "Error: Port 80 is already in use" >&2
|
||||||
exit 1
|
exit 1
|
||||||
@@ -43,32 +43,53 @@ if ss -tulnp | grep ':443 ' >/dev/null; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 检查命令是否存在
|
# Function to check if a command exists
|
||||||
command_exists() {
|
command_exists() {
|
||||||
command -v "$@" > /dev/null 2>&1
|
command -v "$@" > /dev/null 2>&1
|
||||||
}
|
}
|
||||||
|
|
||||||
# 如果未安装 Docker 则安装
|
# Install Docker if it is not installed
|
||||||
if command_exists docker; then
|
if command_exists docker; then
|
||||||
echo "Docker already installed"
|
echo "Docker already installed"
|
||||||
else
|
else
|
||||||
curl -sSL https://get.docker.com | sh
|
curl -sSL https://get.docker.com | sh
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 初始化 Docker Swarm
|
# Initialize Docker Swarm
|
||||||
docker swarm leave --force 2>/dev/null
|
docker swarm leave --force 2>/dev/null
|
||||||
advertise_addr=$(curl -s ifconfig.me)
|
|
||||||
docker swarm init --advertise-addr $advertise_addr
|
|
||||||
echo "Swarm initialized"
|
|
||||||
|
|
||||||
# 创建网络
|
get_ip() {
|
||||||
docker network rm -f dokploy-network 2>/dev/null
|
# Try to get IPv4
|
||||||
docker network create --driver overlay --attachable dokploy-network
|
local ipv4=$(curl -4s https://ifconfig.io 2>/dev/null)
|
||||||
echo "Network created"
|
|
||||||
|
|
||||||
# 准备配置目录
|
if [ -n "$ipv4" ]; then
|
||||||
mkdir -p /etc/dokploy
|
echo "$ipv4"
|
||||||
chmod -R 777 /etc/dokploy
|
else
|
||||||
|
# Try to get IPv6
|
||||||
|
local ipv6=$(curl -6s https://ifconfig.io 2>/dev/null)
|
||||||
|
if [ -n "$ipv6" ]; then
|
||||||
|
echo "$ipv6"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
advertise_addr="${ADVERTISE_ADDR:-$(get_ip)}"
|
||||||
|
|
||||||
|
docker swarm init --advertise-addr $advertise_addr
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Error: Failed to initialize Docker Swarm" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker network rm -f dokploy-network 2>/dev/null
|
||||||
|
docker network create --driver overlay --attachable dokploy-network
|
||||||
|
|
||||||
|
echo "Network created"
|
||||||
|
|
||||||
|
mkdir -p /etc/dokploy
|
||||||
|
|
||||||
|
chmod 777 /etc/dokploy
|
||||||
|
|
||||||
# Pull and deploy Dokploy
|
# Pull and deploy Dokploy
|
||||||
docker pull dokploy/dokploy:latest
|
docker pull dokploy/dokploy:latest
|
||||||
@@ -84,9 +105,10 @@ docker service create \
|
|||||||
-e PORT=<Value For PORT eg(3000)> \
|
-e PORT=<Value For PORT eg(3000)> \
|
||||||
-e TRAEFIK_SSL_PORT=<Value For SSL PORT eg(444)> \
|
-e TRAEFIK_SSL_PORT=<Value For SSL PORT eg(444)> \
|
||||||
-e TRAEFIK_PORT=<VALUE FOR TRAEFIK HTTP PORT eg(81)> \
|
-e TRAEFIK_PORT=<VALUE FOR TRAEFIK HTTP PORT eg(81)> \
|
||||||
|
-e ADVERTISE_ADDR=$advertise_addr \
|
||||||
dokploy/dokploy:latest
|
dokploy/dokploy:latest
|
||||||
|
|
||||||
# 输出成功消息
|
# Output success message
|
||||||
GREEN="\033[0;32m"
|
GREEN="\033[0;32m"
|
||||||
YELLOW="\033[1;33m"
|
YELLOW="\033[1;33m"
|
||||||
BLUE="\033[0;34m"
|
BLUE="\033[0;34m"
|
||||||
|
|||||||
@@ -57,18 +57,39 @@ fi
|
|||||||
|
|
||||||
# Initialize Docker Swarm
|
# Initialize Docker Swarm
|
||||||
docker swarm leave --force 2>/dev/null
|
docker swarm leave --force 2>/dev/null
|
||||||
advertise_addr=$(curl -s ifconfig.me)
|
|
||||||
docker swarm init --advertise-addr $advertise_addr
|
|
||||||
echo "Swarm initialized"
|
|
||||||
|
|
||||||
# Create network
|
get_ip() {
|
||||||
docker network rm -f dokploy-network 2>/dev/null
|
# Try to get IPv4
|
||||||
docker network create --driver overlay --attachable dokploy-network
|
local ipv4=$(curl -4s https://ifconfig.io 2>/dev/null)
|
||||||
echo "Network created"
|
|
||||||
|
|
||||||
# Prepare configuration directory
|
if [ -n "$ipv4" ]; then
|
||||||
mkdir -p /etc/dokploy
|
echo "$ipv4"
|
||||||
chmod -R 777 /etc/dokploy
|
else
|
||||||
|
# Try to get IPv6
|
||||||
|
local ipv6=$(curl -6s https://ifconfig.io 2>/dev/null)
|
||||||
|
if [ -n "$ipv6" ]; then
|
||||||
|
echo "$ipv6"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
advertise_addr="${ADVERTISE_ADDR:-$(get_ip)}"
|
||||||
|
|
||||||
|
docker swarm init --advertise-addr $advertise_addr
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Error: Failed to initialize Docker Swarm" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker network rm -f dokploy-network 2>/dev/null
|
||||||
|
docker network create --driver overlay --attachable dokploy-network
|
||||||
|
|
||||||
|
echo "Network created"
|
||||||
|
|
||||||
|
mkdir -p /etc/dokploy
|
||||||
|
|
||||||
|
chmod 777 /etc/dokploy
|
||||||
|
|
||||||
# Pull and deploy Dokploy
|
# Pull and deploy Dokploy
|
||||||
docker pull dokploy/dokploy:latest
|
docker pull dokploy/dokploy:latest
|
||||||
@@ -84,6 +105,7 @@ docker service create \
|
|||||||
-e PORT=<Value For PORT eg(3000)> \
|
-e PORT=<Value For PORT eg(3000)> \
|
||||||
-e TRAEFIK_SSL_PORT=<Value For SSL PORT eg(444)> \
|
-e TRAEFIK_SSL_PORT=<Value For SSL PORT eg(444)> \
|
||||||
-e TRAEFIK_PORT=<VALUE FOR TRAEFIK HTTP PORT eg(81)> \
|
-e TRAEFIK_PORT=<VALUE FOR TRAEFIK HTTP PORT eg(81)> \
|
||||||
|
-e ADVERTISE_ADDR=$advertise_addr \
|
||||||
dokploy/dokploy:latest
|
dokploy/dokploy:latest
|
||||||
|
|
||||||
# Output success message
|
# Output success message
|
||||||
|
|||||||
@@ -1,12 +1,23 @@
|
|||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { paths } from "@dokploy/server/dist/constants";
|
import { paths } from "@dokploy/server/constants";
|
||||||
const { APPLICATIONS_PATH } = paths();
|
const { APPLICATIONS_PATH } = paths();
|
||||||
import type { ApplicationNested } from "@dokploy/server";
|
import type { ApplicationNested } from "@dokploy/server";
|
||||||
import { unzipDrop } from "@dokploy/server";
|
import { unzipDrop } from "@dokploy/server";
|
||||||
import AdmZip from "adm-zip";
|
import AdmZip from "adm-zip";
|
||||||
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
vi.mock("@dokploy/server/constants", async (importOriginal) => {
|
||||||
|
const actual = await importOriginal();
|
||||||
|
return {
|
||||||
|
// @ts-ignore
|
||||||
|
...actual,
|
||||||
|
paths: () => ({
|
||||||
|
APPLICATIONS_PATH: "./__test__/drop/zips/output",
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
if (typeof window === "undefined") {
|
if (typeof window === "undefined") {
|
||||||
const undici = require("undici");
|
const undici = require("undici");
|
||||||
globalThis.File = undici.File as any;
|
globalThis.File = undici.File as any;
|
||||||
@@ -82,16 +93,6 @@ const baseApp: ApplicationNested = {
|
|||||||
dockerContextPath: null,
|
dockerContextPath: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
vi.mock("@dokploy/server/dist/constants", async (importOriginal) => {
|
|
||||||
const actual = await importOriginal();
|
|
||||||
return {
|
|
||||||
// @ts-ignore
|
|
||||||
...actual,
|
|
||||||
paths: () => ({
|
|
||||||
APPLICATIONS_PATH: "./__test__/drop/zips/output",
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
describe("unzipDrop using real zip files", () => {
|
describe("unzipDrop using real zip files", () => {
|
||||||
// const { APPLICATIONS_PATH } = paths();
|
// const { APPLICATIONS_PATH } = paths();
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ const baseAdmin: Admin = {
|
|||||||
sshPrivateKey: null,
|
sshPrivateKey: null,
|
||||||
enableDockerCleanup: false,
|
enableDockerCleanup: false,
|
||||||
enableLogRotation: false,
|
enableLogRotation: false,
|
||||||
|
serversQuantity: 0,
|
||||||
|
stripeCustomerId: "",
|
||||||
|
stripeSubscriptionId: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|||||||
@@ -1,13 +1,8 @@
|
|||||||
|
import path from "node:path";
|
||||||
import tsconfigPaths from "vite-tsconfig-paths";
|
import tsconfigPaths from "vite-tsconfig-paths";
|
||||||
import { defineConfig } from "vitest/config";
|
import { defineConfig } from "vitest/config";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
|
||||||
tsconfigPaths({
|
|
||||||
root: "./",
|
|
||||||
projects: ["tsconfig.json"],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
test: {
|
test: {
|
||||||
include: ["__test__/**/*.test.ts"], // Incluir solo los archivos de test en el directorio __test__
|
include: ["__test__/**/*.test.ts"], // Incluir solo los archivos de test en el directorio __test__
|
||||||
exclude: ["**/node_modules/**", "**/dist/**", "**/.docker/**"],
|
exclude: ["**/node_modules/**", "**/dist/**", "**/.docker/**"],
|
||||||
@@ -18,4 +13,12 @@ export default defineConfig({
|
|||||||
NODE: "test",
|
NODE: "test",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@dokploy/server": path.resolve(
|
||||||
|
__dirname,
|
||||||
|
"../../../packages/server/src",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,241 @@
|
|||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import { NumberInput } from "@/components/ui/input";
|
||||||
|
import { Progress } from "@/components/ui/progress";
|
||||||
|
import { Tabs, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { loadStripe } from "@stripe/stripe-js";
|
||||||
|
import clsx from "clsx";
|
||||||
|
import { AlertTriangle, CheckIcon, MinusIcon, PlusIcon } from "lucide-react";
|
||||||
|
import React, { useState } from "react";
|
||||||
|
|
||||||
|
const stripePromise = loadStripe(
|
||||||
|
"pk_test_51QAm7bF3cxQuHeOz0xg04o9teeyTbbNHQPJ5Tr98MlTEan9MzewT3gwh0jSWBNvrRWZ5vASoBgxUSF4gPWsJwATk00Ir2JZ0S1",
|
||||||
|
);
|
||||||
|
|
||||||
|
export const calculatePrice = (count: number, isAnnual = false) => {
|
||||||
|
if (isAnnual) {
|
||||||
|
if (count <= 1) return 45.9;
|
||||||
|
return 35.7 * count;
|
||||||
|
}
|
||||||
|
if (count <= 1) return 4.5;
|
||||||
|
return count * 3.5;
|
||||||
|
};
|
||||||
|
// 178.156.147.118
|
||||||
|
export const ShowBilling = () => {
|
||||||
|
const { data: servers } = api.server.all.useQuery(undefined);
|
||||||
|
const { data: admin } = api.admin.one.useQuery();
|
||||||
|
const { data } = api.stripe.getProducts.useQuery();
|
||||||
|
const { mutateAsync: createCheckoutSession } =
|
||||||
|
api.stripe.createCheckoutSession.useMutation();
|
||||||
|
|
||||||
|
const { mutateAsync: createCustomerPortalSession } =
|
||||||
|
api.stripe.createCustomerPortalSession.useMutation();
|
||||||
|
|
||||||
|
const [serverQuantity, setServerQuantity] = useState(3);
|
||||||
|
const [isAnnual, setIsAnnual] = useState(false);
|
||||||
|
|
||||||
|
const handleCheckout = async (productId: string) => {
|
||||||
|
const stripe = await stripePromise;
|
||||||
|
if (data && data.subscriptions.length === 0) {
|
||||||
|
createCheckoutSession({
|
||||||
|
productId,
|
||||||
|
serverQuantity: serverQuantity,
|
||||||
|
isAnnual,
|
||||||
|
}).then(async (session) => {
|
||||||
|
await stripe?.redirectToCheckout({
|
||||||
|
sessionId: session.sessionId,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const products = data?.products.filter((product) => {
|
||||||
|
// @ts-ignore
|
||||||
|
const interval = product?.default_price?.recurring?.interval;
|
||||||
|
return isAnnual ? interval === "year" : interval === "month";
|
||||||
|
});
|
||||||
|
|
||||||
|
const maxServers = admin?.serversQuantity ?? 1;
|
||||||
|
const percentage = ((servers?.length ?? 0) / maxServers) * 100;
|
||||||
|
const safePercentage = Math.min(percentage, 100);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col gap-4 w-full">
|
||||||
|
<Tabs
|
||||||
|
defaultValue="monthly"
|
||||||
|
value={isAnnual ? "annual" : "monthly"}
|
||||||
|
className="w-full"
|
||||||
|
onValueChange={(e) => setIsAnnual(e === "annual")}
|
||||||
|
>
|
||||||
|
<TabsList>
|
||||||
|
<TabsTrigger value="monthly">Monthly</TabsTrigger>
|
||||||
|
<TabsTrigger value="annual">Annual</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
</Tabs>
|
||||||
|
{admin?.stripeSubscriptionId && (
|
||||||
|
<div className="space-y-2">
|
||||||
|
<h3 className="text-lg font-medium">Servers Plan</h3>
|
||||||
|
<p className="text-sm text-muted-foreground">
|
||||||
|
You have {servers?.length} server on your plan of{" "}
|
||||||
|
{admin?.serversQuantity} servers
|
||||||
|
</p>
|
||||||
|
<div className="pb-5">
|
||||||
|
<Progress value={safePercentage} className="max-w-lg" />
|
||||||
|
</div>
|
||||||
|
{admin && (
|
||||||
|
<>
|
||||||
|
{admin.serversQuantity! <= servers?.length! && (
|
||||||
|
<div className="flex flex-row gap-4 p-2 bg-yellow-50 dark:bg-yellow-950 rounded-lg items-center">
|
||||||
|
<AlertTriangle className="text-yellow-600 dark:text-yellow-400" />
|
||||||
|
<span className="text-sm text-yellow-600 dark:text-yellow-400">
|
||||||
|
You have reached the maximum number of servers you can
|
||||||
|
create, please upgrade your plan to add more servers.
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{products?.map((product) => {
|
||||||
|
const featured = true;
|
||||||
|
return (
|
||||||
|
<div key={product.id}>
|
||||||
|
<section
|
||||||
|
className={clsx(
|
||||||
|
"flex flex-col rounded-3xl border-dashed border-2 px-4 max-w-sm",
|
||||||
|
featured
|
||||||
|
? "order-first bg-black border py-8 lg:order-none"
|
||||||
|
: "lg:py-8",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{isAnnual ? (
|
||||||
|
<div className="flex flex-row gap-2 items-center">
|
||||||
|
<p className=" text-2xl font-semibold tracking-tight text-primary ">
|
||||||
|
$ {calculatePrice(serverQuantity, isAnnual).toFixed(2)} USD
|
||||||
|
</p>
|
||||||
|
|
|
||||||
|
<p className=" text-base font-semibold tracking-tight text-muted-foreground">
|
||||||
|
${" "}
|
||||||
|
{(calculatePrice(serverQuantity, isAnnual) / 12).toFixed(2)}{" "}
|
||||||
|
/ Month USD
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className=" text-2xl font-semibold tracking-tight text-primary ">
|
||||||
|
$ {calculatePrice(serverQuantity, isAnnual).toFixed(2)} USD
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<h3 className="mt-5 font-medium text-lg text-white">
|
||||||
|
{product.name}
|
||||||
|
</h3>
|
||||||
|
<p
|
||||||
|
className={clsx(
|
||||||
|
"text-sm",
|
||||||
|
featured ? "text-white" : "text-slate-400",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{product.description}
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul
|
||||||
|
role="list"
|
||||||
|
className={clsx(
|
||||||
|
" mt-4 flex flex-col gap-y-2 text-sm",
|
||||||
|
featured ? "text-white" : "text-slate-200",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{[
|
||||||
|
"All the features of Dokploy",
|
||||||
|
"Unlimited deployments",
|
||||||
|
"Self-hosted on your own infrastructure",
|
||||||
|
"Full access to all deployment features",
|
||||||
|
"Dokploy integration",
|
||||||
|
"Backups",
|
||||||
|
"All Incoming features",
|
||||||
|
].map((feature) => (
|
||||||
|
<li key={feature} className="flex text-muted-foreground">
|
||||||
|
<CheckIcon />
|
||||||
|
<span className="ml-4">{feature}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<div className="flex flex-col gap-2 mt-4">
|
||||||
|
<div className="flex items-center gap-2 justify-center">
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{serverQuantity} Servers
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Button
|
||||||
|
disabled={serverQuantity <= 1}
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
if (serverQuantity <= 1) return;
|
||||||
|
|
||||||
|
setServerQuantity(serverQuantity - 1);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MinusIcon className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<NumberInput
|
||||||
|
value={serverQuantity}
|
||||||
|
onChange={(e) => {
|
||||||
|
setServerQuantity(e.target.value as unknown as number);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setServerQuantity(serverQuantity + 1);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PlusIcon className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
data?.subscriptions && data?.subscriptions?.length > 0
|
||||||
|
? "justify-between"
|
||||||
|
: "justify-end",
|
||||||
|
"flex flex-row items-center gap-2 mt-4",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{admin?.stripeCustomerId && (
|
||||||
|
<Button
|
||||||
|
variant="secondary"
|
||||||
|
className="w-full"
|
||||||
|
onClick={async () => {
|
||||||
|
const session = await createCustomerPortalSession();
|
||||||
|
|
||||||
|
window.open(session.url);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Manage Subscription
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{data?.subscriptions?.length === 0 && (
|
||||||
|
<div className="justify-end w-full">
|
||||||
|
<Button
|
||||||
|
className="w-full"
|
||||||
|
onClick={async () => {
|
||||||
|
handleCheckout(product.id);
|
||||||
|
}}
|
||||||
|
disabled={serverQuantity < 1}
|
||||||
|
>
|
||||||
|
Subscribe
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormDescription,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from "@/components/ui/form";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { AlertTriangle, Container } from "lucide-react";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
const AddRegistrySchema = z.object({
|
|
||||||
username: z
|
|
||||||
.string()
|
|
||||||
.min(1, {
|
|
||||||
message: "Username is required",
|
|
||||||
})
|
|
||||||
.regex(/^[a-zA-Z0-9]+$/, {
|
|
||||||
message: "Username can only contain letters and numbers",
|
|
||||||
}),
|
|
||||||
password: z.string().min(1, {
|
|
||||||
message: "Password is required",
|
|
||||||
}),
|
|
||||||
registryUrl: z.string().min(1, {
|
|
||||||
message: "Registry URL is required",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
type AddRegistry = z.infer<typeof AddRegistrySchema>;
|
|
||||||
|
|
||||||
export const AddSelfHostedRegistry = () => {
|
|
||||||
const utils = api.useUtils();
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const { mutateAsync, error, isError, isLoading } =
|
|
||||||
api.registry.enableSelfHostedRegistry.useMutation();
|
|
||||||
const router = useRouter();
|
|
||||||
const form = useForm<AddRegistry>({
|
|
||||||
defaultValues: {
|
|
||||||
username: "",
|
|
||||||
password: "",
|
|
||||||
registryUrl: "",
|
|
||||||
},
|
|
||||||
resolver: zodResolver(AddRegistrySchema),
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
form.reset({
|
|
||||||
registryUrl: "",
|
|
||||||
username: "",
|
|
||||||
password: "",
|
|
||||||
});
|
|
||||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
|
||||||
|
|
||||||
const onSubmit = async (data: AddRegistry) => {
|
|
||||||
await mutateAsync({
|
|
||||||
registryUrl: data.registryUrl,
|
|
||||||
username: data.username,
|
|
||||||
password: data.password,
|
|
||||||
})
|
|
||||||
.then(async (data) => {
|
|
||||||
await utils.registry.all.invalidate();
|
|
||||||
toast.success("Self Hosted Registry Created");
|
|
||||||
setIsOpen(false);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error to create a self hosted registry");
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Button className="max-sm:w-full">
|
|
||||||
<Container className="h-4 w-4" />
|
|
||||||
Enable Self Hosted Registry
|
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent className="sm:m:max-w-lg ">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Add a self hosted registry</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Fill the next fields to add a self hosted registry.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
{isError && (
|
|
||||||
<div className="flex flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
|
|
||||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
|
||||||
<span className="text-sm text-red-600 dark:text-red-400">
|
|
||||||
{error?.message}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<Form {...form}>
|
|
||||||
<form
|
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
|
||||||
className="grid w-full gap-4"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="username"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Username</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="Username" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="password"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Password</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="Password"
|
|
||||||
{...field}
|
|
||||||
type="password"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="registryUrl"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Registry URL</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="registry.dokploy.com" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
Point a DNS record to the VPS IP address.
|
|
||||||
</FormDescription>
|
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button isLoading={isLoading} type="submit">
|
|
||||||
Create
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Server } from "lucide-react";
|
import { Server } from "lucide-react";
|
||||||
import { AddRegistry } from "./add-docker-registry";
|
import { AddRegistry } from "./add-docker-registry";
|
||||||
import { AddSelfHostedRegistry } from "./add-self-docker-registry";
|
|
||||||
import { DeleteRegistry } from "./delete-registry";
|
import { DeleteRegistry } from "./delete-registry";
|
||||||
import { UpdateDockerRegistry } from "./update-docker-registry";
|
import { UpdateDockerRegistry } from "./update-docker-registry";
|
||||||
|
|
||||||
@@ -31,8 +30,6 @@ export const ShowRegistry = () => {
|
|||||||
<div className="flex flex-row gap-2">
|
<div className="flex flex-row gap-2">
|
||||||
{data && data?.length > 0 && (
|
{data && data?.length > 0 && (
|
||||||
<>
|
<>
|
||||||
{!haveSelfHostedRegistry && <AddSelfHostedRegistry />}
|
|
||||||
|
|
||||||
<AddRegistry />
|
<AddRegistry />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -47,7 +44,6 @@ export const ShowRegistry = () => {
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div className="flex flex-row md:flex-row gap-2 flex-wrap w-full justify-center">
|
<div className="flex flex-row md:flex-row gap-2 flex-wrap w-full justify-center">
|
||||||
<AddSelfHostedRegistry />
|
|
||||||
<AddRegistry />
|
<AddRegistry />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,6 +18,16 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
@@ -32,12 +42,15 @@ const addDestination = z.object({
|
|||||||
bucket: z.string(),
|
bucket: z.string(),
|
||||||
region: z.string(),
|
region: z.string(),
|
||||||
endpoint: z.string(),
|
endpoint: z.string(),
|
||||||
|
serverId: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type AddDestination = z.infer<typeof addDestination>;
|
type AddDestination = z.infer<typeof addDestination>;
|
||||||
|
|
||||||
export const AddDestination = () => {
|
export const AddDestination = () => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||||
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
|
|
||||||
const { mutateAsync, isError, error, isLoading } =
|
const { mutateAsync, isError, error, isLoading } =
|
||||||
api.destination.create.useMutation();
|
api.destination.create.useMutation();
|
||||||
@@ -189,30 +202,106 @@ export const AddDestination = () => {
|
|||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<DialogFooter className="flex w-full flex-row !justify-between pt-3">
|
<DialogFooter
|
||||||
<Button
|
className={cn(
|
||||||
isLoading={isLoadingConnection}
|
isCloud ? "!flex-col" : "flex-row",
|
||||||
type="button"
|
"flex w-full !justify-between pt-3 gap-4",
|
||||||
variant="secondary"
|
)}
|
||||||
onClick={async () => {
|
>
|
||||||
await testConnection({
|
{isCloud ? (
|
||||||
accessKey: form.getValues("accessKeyId"),
|
<div className="flex flex-col gap-4 border p-2 rounded-lg">
|
||||||
bucket: form.getValues("bucket"),
|
<span className="text-sm text-muted-foreground">
|
||||||
endpoint: form.getValues("endpoint"),
|
Select a server to test the destination. If you don't have a
|
||||||
name: "Test",
|
server choose the default one.
|
||||||
region: form.getValues("region"),
|
</span>
|
||||||
secretAccessKey: form.getValues("secretAccessKey"),
|
<FormField
|
||||||
})
|
control={form.control}
|
||||||
.then(async () => {
|
name="serverId"
|
||||||
toast.success("Connection Success");
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Server (Optional)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full">
|
||||||
|
<SelectValue placeholder="Select a server" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectLabel>Servers</SelectLabel>
|
||||||
|
{servers?.map((server) => (
|
||||||
|
<SelectItem
|
||||||
|
key={server.serverId}
|
||||||
|
value={server.serverId}
|
||||||
|
>
|
||||||
|
{server.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
<SelectItem value={"none"}>None</SelectItem>
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant={"secondary"}
|
||||||
|
isLoading={isLoading}
|
||||||
|
onClick={async () => {
|
||||||
|
await testConnection({
|
||||||
|
accessKey: form.getValues("accessKeyId"),
|
||||||
|
bucket: form.getValues("bucket"),
|
||||||
|
endpoint: form.getValues("endpoint"),
|
||||||
|
name: "Test",
|
||||||
|
region: form.getValues("region"),
|
||||||
|
secretAccessKey: form.getValues("secretAccessKey"),
|
||||||
|
serverId: form.getValues("serverId"),
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
toast.success("Connection Success");
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
toast.error("Error to connect the provider", {
|
||||||
|
description: e.message,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Test Connection
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
isLoading={isLoadingConnection}
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={async () => {
|
||||||
|
await testConnection({
|
||||||
|
accessKey: form.getValues("accessKeyId"),
|
||||||
|
bucket: form.getValues("bucket"),
|
||||||
|
endpoint: form.getValues("endpoint"),
|
||||||
|
name: "Test",
|
||||||
|
region: form.getValues("region"),
|
||||||
|
secretAccessKey: form.getValues("secretAccessKey"),
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.then(async () => {
|
||||||
toast.error("Error to connect the provider");
|
toast.success("Connection Success");
|
||||||
});
|
})
|
||||||
}}
|
.catch(() => {
|
||||||
>
|
toast.error("Error to connect the provider");
|
||||||
Test connection
|
});
|
||||||
</Button>
|
}}
|
||||||
|
>
|
||||||
|
Test connection
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
form="hook-form-destination-add"
|
form="hook-form-destination-add"
|
||||||
|
|||||||
@@ -18,6 +18,16 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { PenBoxIcon } from "lucide-react";
|
import { PenBoxIcon } from "lucide-react";
|
||||||
@@ -33,6 +43,7 @@ const updateDestination = z.object({
|
|||||||
bucket: z.string(),
|
bucket: z.string(),
|
||||||
region: z.string(),
|
region: z.string(),
|
||||||
endpoint: z.string(),
|
endpoint: z.string(),
|
||||||
|
serverId: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type UpdateDestination = z.infer<typeof updateDestination>;
|
type UpdateDestination = z.infer<typeof updateDestination>;
|
||||||
@@ -43,6 +54,8 @@ interface Props {
|
|||||||
|
|
||||||
export const UpdateDestination = ({ destinationId }: Props) => {
|
export const UpdateDestination = ({ destinationId }: Props) => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||||
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const { data, refetch } = api.destination.one.useQuery(
|
const { data, refetch } = api.destination.one.useQuery(
|
||||||
{
|
{
|
||||||
@@ -220,34 +233,107 @@ export const UpdateDestination = ({ destinationId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<DialogFooter className="flex w-full flex-row !justify-between pt-3">
|
<DialogFooter
|
||||||
<Button
|
className={cn(
|
||||||
isLoading={isLoadingConnection}
|
isCloud ? "!flex-col" : "flex-row",
|
||||||
type="button"
|
"flex w-full !justify-between pt-3 gap-4",
|
||||||
variant="secondary"
|
)}
|
||||||
onClick={async () => {
|
>
|
||||||
await testConnection({
|
{isCloud ? (
|
||||||
accessKey: form.getValues("accessKeyId"),
|
<div className="flex flex-col gap-4 border p-2 rounded-lg">
|
||||||
bucket: form.getValues("bucket"),
|
<span className="text-sm text-muted-foreground">
|
||||||
endpoint: form.getValues("endpoint"),
|
Select a server to test the destination. If you don't have a
|
||||||
name: "Test",
|
server choose the default one.
|
||||||
region: form.getValues("region"),
|
</span>
|
||||||
secretAccessKey: form.getValues("secretAccessKey"),
|
<FormField
|
||||||
})
|
control={form.control}
|
||||||
.then(async () => {
|
name="serverId"
|
||||||
toast.success("Connection Success");
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Server (Optional)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full">
|
||||||
|
<SelectValue placeholder="Select a server" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectLabel>Servers</SelectLabel>
|
||||||
|
{servers?.map((server) => (
|
||||||
|
<SelectItem
|
||||||
|
key={server.serverId}
|
||||||
|
value={server.serverId}
|
||||||
|
>
|
||||||
|
{server.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
<SelectItem value={"none"}>None</SelectItem>
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant={"secondary"}
|
||||||
|
onClick={async () => {
|
||||||
|
await testConnection({
|
||||||
|
accessKey: form.getValues("accessKeyId"),
|
||||||
|
bucket: form.getValues("bucket"),
|
||||||
|
endpoint: form.getValues("endpoint"),
|
||||||
|
name: "Test",
|
||||||
|
region: form.getValues("region"),
|
||||||
|
secretAccessKey: form.getValues("secretAccessKey"),
|
||||||
|
serverId: form.getValues("serverId"),
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
toast.success("Connection Success");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to connect the provider");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Test Connection
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
isLoading={isLoadingConnection}
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={async () => {
|
||||||
|
await testConnection({
|
||||||
|
accessKey: form.getValues("accessKeyId"),
|
||||||
|
bucket: form.getValues("bucket"),
|
||||||
|
endpoint: form.getValues("endpoint"),
|
||||||
|
name: "Test",
|
||||||
|
region: form.getValues("region"),
|
||||||
|
secretAccessKey: form.getValues("secretAccessKey"),
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.then(async () => {
|
||||||
toast.error("Error to connect the provider");
|
toast.success("Connection Success");
|
||||||
});
|
})
|
||||||
}}
|
.catch(() => {
|
||||||
>
|
toast.error("Error to connect the provider");
|
||||||
Test connection
|
});
|
||||||
</Button>
|
}}
|
||||||
|
>
|
||||||
|
Test connection
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
isLoading={form.formState.isSubmitting}
|
|
||||||
form="hook-form"
|
form="hook-form"
|
||||||
type="submit"
|
type="submit"
|
||||||
|
isLoading={form.formState.isSubmitting}
|
||||||
>
|
>
|
||||||
Update
|
Update
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ export type NotificationSchema = z.infer<typeof notificationSchema>;
|
|||||||
export const AddNotification = () => {
|
export const AddNotification = () => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { mutateAsync: testSlackConnection, isLoading: isLoadingSlack } =
|
const { mutateAsync: testSlackConnection, isLoading: isLoadingSlack } =
|
||||||
api.notification.testSlackConnection.useMutation();
|
api.notification.testSlackConnection.useMutation();
|
||||||
|
|
||||||
@@ -660,26 +660,28 @@ export const AddNotification = () => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
{!isCloud && (
|
||||||
control={form.control}
|
<FormField
|
||||||
name="dokployRestart"
|
control={form.control}
|
||||||
render={({ field }) => (
|
name="dokployRestart"
|
||||||
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
render={({ field }) => (
|
||||||
<div className="space-y-0.5">
|
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
||||||
<FormLabel>Dokploy Restart</FormLabel>
|
<div className="space-y-0.5">
|
||||||
<FormDescription>
|
<FormLabel>Dokploy Restart</FormLabel>
|
||||||
Trigger the action when a dokploy is restarted.
|
<FormDescription>
|
||||||
</FormDescription>
|
Trigger the action when a dokploy is restarted.
|
||||||
</div>
|
</FormDescription>
|
||||||
<FormControl>
|
</div>
|
||||||
<Switch
|
<FormControl>
|
||||||
checked={field.value}
|
<Switch
|
||||||
onCheckedChange={field.onChange}
|
checked={field.value}
|
||||||
/>
|
onCheckedChange={field.onChange}
|
||||||
</FormControl>
|
/>
|
||||||
</FormItem>
|
</FormControl>
|
||||||
)}
|
</FormItem>
|
||||||
/>
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export const UpdateNotification = ({ notificationId }: Props) => {
|
|||||||
const telegramMutation = api.notification.updateTelegram.useMutation();
|
const telegramMutation = api.notification.updateTelegram.useMutation();
|
||||||
const discordMutation = api.notification.updateDiscord.useMutation();
|
const discordMutation = api.notification.updateDiscord.useMutation();
|
||||||
const emailMutation = api.notification.updateEmail.useMutation();
|
const emailMutation = api.notification.updateEmail.useMutation();
|
||||||
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const form = useForm<NotificationSchema>({
|
const form = useForm<NotificationSchema>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
type: "slack",
|
type: "slack",
|
||||||
@@ -618,27 +618,29 @@ export const UpdateNotification = ({ notificationId }: Props) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<FormField
|
{!isCloud && (
|
||||||
control={form.control}
|
<FormField
|
||||||
defaultValue={form.control._defaultValues.dokployRestart}
|
control={form.control}
|
||||||
name="dokployRestart"
|
defaultValue={form.control._defaultValues.dokployRestart}
|
||||||
render={({ field }) => (
|
name="dokployRestart"
|
||||||
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
render={({ field }) => (
|
||||||
<div className="space-y-0.5">
|
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
||||||
<FormLabel>Dokploy Restart</FormLabel>
|
<div className="space-y-0.5">
|
||||||
<FormDescription>
|
<FormLabel>Dokploy Restart</FormLabel>
|
||||||
Trigger the action when a dokploy is restarted.
|
<FormDescription>
|
||||||
</FormDescription>
|
Trigger the action when a dokploy is restarted.
|
||||||
</div>
|
</FormDescription>
|
||||||
<FormControl>
|
</div>
|
||||||
<Switch
|
<FormControl>
|
||||||
checked={field.value}
|
<Switch
|
||||||
onCheckedChange={field.onChange}
|
checked={field.value}
|
||||||
/>
|
onCheckedChange={field.onChange}
|
||||||
</FormControl>
|
/>
|
||||||
</FormItem>
|
</FormControl>
|
||||||
)}
|
</FormItem>
|
||||||
/>
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import { Textarea } from "@/components/ui/textarea";
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { PlusIcon } from "lucide-react";
|
import { PlusIcon } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
@@ -57,6 +58,9 @@ type Schema = z.infer<typeof Schema>;
|
|||||||
export const AddServer = () => {
|
export const AddServer = () => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const { data: canCreateMoreServers, refetch } =
|
||||||
|
api.stripe.canCreateMoreServers.useQuery();
|
||||||
|
|
||||||
const { data: sshKeys } = api.sshKey.all.useQuery();
|
const { data: sshKeys } = api.sshKey.all.useQuery();
|
||||||
const { mutateAsync, error, isError } = api.server.create.useMutation();
|
const { mutateAsync, error, isError } = api.server.create.useMutation();
|
||||||
const form = useForm<Schema>({
|
const form = useForm<Schema>({
|
||||||
@@ -82,6 +86,10 @@ export const AddServer = () => {
|
|||||||
});
|
});
|
||||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
refetch();
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
const onSubmit = async (data: Schema) => {
|
const onSubmit = async (data: Schema) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
name: data.name,
|
name: data.name,
|
||||||
@@ -116,6 +124,14 @@ export const AddServer = () => {
|
|||||||
Add a server to deploy your applications remotely.
|
Add a server to deploy your applications remotely.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
{!canCreateMoreServers && (
|
||||||
|
<AlertBlock type="warning">
|
||||||
|
You cannot create more servers,{" "}
|
||||||
|
<Link href="/dashboard/settings/billing" className="text-primary">
|
||||||
|
Please upgrade your plan
|
||||||
|
</Link>
|
||||||
|
</AlertBlock>
|
||||||
|
)}
|
||||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
@@ -254,6 +270,7 @@ export const AddServer = () => {
|
|||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<Button
|
||||||
isLoading={form.formState.isSubmitting}
|
isLoading={form.formState.isSubmitting}
|
||||||
|
disabled={!canCreateMoreServers}
|
||||||
form="hook-form-add-server"
|
form="hook-form-add-server"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ export const ShowServers = () => {
|
|||||||
const { data, refetch } = api.server.all.useQuery();
|
const { data, refetch } = api.server.all.useQuery();
|
||||||
const { mutateAsync } = api.server.remove.useMutation();
|
const { mutateAsync } = api.server.remove.useMutation();
|
||||||
const { data: sshKeys } = api.sshKey.all.useQuery();
|
const { data: sshKeys } = api.sshKey.all.useQuery();
|
||||||
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
|
const { data: canCreateMoreServers } =
|
||||||
|
api.stripe.canCreateMoreServers.useQuery();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 space-y-6">
|
<div className="p-6 space-y-6">
|
||||||
@@ -74,8 +77,22 @@ export const ShowServers = () => {
|
|||||||
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
|
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
|
||||||
<ServerIcon className="size-8" />
|
<ServerIcon className="size-8" />
|
||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
No Servers found. Add a server to deploy your applications
|
{!canCreateMoreServers ? (
|
||||||
remotely.
|
<div>
|
||||||
|
You cannot create more servers,{" "}
|
||||||
|
<Link
|
||||||
|
href="/dashboard/settings/billing"
|
||||||
|
className="text-primary"
|
||||||
|
>
|
||||||
|
Please upgrade your plan
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span>
|
||||||
|
No Servers found. Add a server to deploy your applications
|
||||||
|
remotely.
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -87,6 +104,9 @@ export const ShowServers = () => {
|
|||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="w-[100px]">Name</TableHead>
|
<TableHead className="w-[100px]">Name</TableHead>
|
||||||
|
{isCloud && (
|
||||||
|
<TableHead className="text-center">Status</TableHead>
|
||||||
|
)}
|
||||||
<TableHead className="text-center">IP Address</TableHead>
|
<TableHead className="text-center">IP Address</TableHead>
|
||||||
<TableHead className="text-center">Port</TableHead>
|
<TableHead className="text-center">Port</TableHead>
|
||||||
<TableHead className="text-center">Username</TableHead>
|
<TableHead className="text-center">Username</TableHead>
|
||||||
@@ -98,9 +118,23 @@ export const ShowServers = () => {
|
|||||||
<TableBody>
|
<TableBody>
|
||||||
{data?.map((server) => {
|
{data?.map((server) => {
|
||||||
const canDelete = server.totalSum === 0;
|
const canDelete = server.totalSum === 0;
|
||||||
|
const isActive = server.serverStatus === "active";
|
||||||
return (
|
return (
|
||||||
<TableRow key={server.serverId}>
|
<TableRow key={server.serverId}>
|
||||||
<TableCell className="w-[100px]">{server.name}</TableCell>
|
<TableCell className="w-[100px]">{server.name}</TableCell>
|
||||||
|
{isCloud && (
|
||||||
|
<TableHead className="text-center">
|
||||||
|
<Badge
|
||||||
|
variant={
|
||||||
|
server.serverStatus === "active"
|
||||||
|
? "default"
|
||||||
|
: "destructive"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{server.serverStatus}
|
||||||
|
</Badge>
|
||||||
|
</TableHead>
|
||||||
|
)}
|
||||||
<TableCell className="text-center">
|
<TableCell className="text-center">
|
||||||
<Badge>{server.ipAddress}</Badge>
|
<Badge>{server.ipAddress}</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -131,18 +165,25 @@ export const ShowServers = () => {
|
|||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||||
{server.sshKeyId && (
|
|
||||||
<TerminalModal serverId={server.serverId}>
|
{isActive && (
|
||||||
<span>Enter the terminal</span>
|
<>
|
||||||
</TerminalModal>
|
{server.sshKeyId && (
|
||||||
|
<TerminalModal serverId={server.serverId}>
|
||||||
|
<span>Enter the terminal</span>
|
||||||
|
</TerminalModal>
|
||||||
|
)}
|
||||||
|
<SetupServer serverId={server.serverId} />
|
||||||
|
|
||||||
|
<UpdateServer serverId={server.serverId} />
|
||||||
|
{server.sshKeyId && (
|
||||||
|
<ShowServerActions
|
||||||
|
serverId={server.serverId}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<SetupServer serverId={server.serverId} />
|
|
||||||
|
|
||||||
<UpdateServer serverId={server.serverId} />
|
|
||||||
{server.sshKeyId && (
|
|
||||||
<ShowServerActions serverId={server.serverId} />
|
|
||||||
)}
|
|
||||||
<DialogAction
|
<DialogAction
|
||||||
disabled={!canDelete}
|
disabled={!canDelete}
|
||||||
title={
|
title={
|
||||||
@@ -187,17 +228,21 @@ export const ShowServers = () => {
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
|
|
||||||
{server.sshKeyId && (
|
{isActive && (
|
||||||
<>
|
<>
|
||||||
<DropdownMenuSeparator />
|
{server.sshKeyId && (
|
||||||
<DropdownMenuLabel>Extra</DropdownMenuLabel>
|
<>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuLabel>Extra</DropdownMenuLabel>
|
||||||
|
|
||||||
<ShowTraefikFileSystemModal
|
<ShowTraefikFileSystemModal
|
||||||
serverId={server.serverId}
|
serverId={server.serverId}
|
||||||
/>
|
/>
|
||||||
<ShowDockerContainersModal
|
<ShowDockerContainersModal
|
||||||
serverId={server.serverId}
|
serverId={server.serverId}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ export const SettingsLayout = ({ children }: Props) => {
|
|||||||
icon: ListMusic,
|
icon: ListMusic,
|
||||||
href: "/dashboard/settings/registry",
|
href: "/dashboard/settings/registry",
|
||||||
},
|
},
|
||||||
|
|
||||||
...(!isCloud
|
...(!isCloud
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
@@ -102,6 +103,16 @@ export const SettingsLayout = ({ children }: Props) => {
|
|||||||
icon: Server,
|
icon: Server,
|
||||||
href: "/dashboard/settings/servers",
|
href: "/dashboard/settings/servers",
|
||||||
},
|
},
|
||||||
|
...(isCloud
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
title: "Billing",
|
||||||
|
label: "",
|
||||||
|
icon: CreditCardIcon,
|
||||||
|
href: "/dashboard/settings/billing",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
...(user?.canAccessToSSHKeys
|
...(user?.canAccessToSSHKeys
|
||||||
@@ -137,6 +148,7 @@ import {
|
|||||||
Activity,
|
Activity,
|
||||||
Bell,
|
Bell,
|
||||||
BoxesIcon,
|
BoxesIcon,
|
||||||
|
CreditCardIcon,
|
||||||
Database,
|
Database,
|
||||||
GitBranch,
|
GitBranch,
|
||||||
KeyIcon,
|
KeyIcon,
|
||||||
|
|||||||
12
apps/dokploy/drizzle/0041_huge_bruce_banner.sql
Normal file
12
apps/dokploy/drizzle/0041_huge_bruce_banner.sql
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
DO $$ BEGIN
|
||||||
|
CREATE TYPE "public"."serverStatus" AS ENUM('active', 'inactive');
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "admin" ADD COLUMN "stripeCustomerId" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "admin" ADD COLUMN "stripeSubscriptionId" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "admin" ADD COLUMN "serversQuantity" integer DEFAULT 0 NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "auth" ADD COLUMN "resetPasswordToken" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "auth" ADD COLUMN "resetPasswordExpiresAt" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "server" ADD COLUMN "serverStatus" "serverStatus" DEFAULT 'active' NOT NULL;
|
||||||
3956
apps/dokploy/drizzle/meta/0041_snapshot.json
Normal file
3956
apps/dokploy/drizzle/meta/0041_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -288,6 +288,13 @@
|
|||||||
"when": 1728780577084,
|
"when": 1728780577084,
|
||||||
"tag": "0040_graceful_wolfsbane",
|
"tag": "0040_graceful_wolfsbane",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 41,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1729667438853,
|
||||||
|
"tag": "0041_huge_bruce_banner",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dokploy",
|
"name": "dokploy",
|
||||||
"version": "v0.10.3",
|
"version": "v0.10.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
"build-next": "next build",
|
"build-next": "next build",
|
||||||
"setup": "tsx -r dotenv/config setup.ts && sleep 5 && pnpm run migration:run",
|
"setup": "tsx -r dotenv/config setup.ts && sleep 5 && pnpm run migration:run",
|
||||||
"reset-password": "node -r dotenv/config dist/reset-password.mjs",
|
"reset-password": "node -r dotenv/config dist/reset-password.mjs",
|
||||||
"dev": "tsx -r dotenv/config ./server/server.ts --project tsconfig.server.json ",
|
"dev": "TURBOPACK=1 tsx -r dotenv/config ./server/server.ts --project tsconfig.server.json ",
|
||||||
"studio": "drizzle-kit studio --config ./server/db/drizzle.config.ts",
|
"studio": "drizzle-kit studio --config ./server/db/drizzle.config.ts",
|
||||||
"migration:generate": "drizzle-kit generate --config ./server/db/drizzle.config.ts",
|
"migration:generate": "drizzle-kit generate --config ./server/db/drizzle.config.ts",
|
||||||
"migration:run": "tsx -r dotenv/config migration.ts",
|
"migration:run": "tsx -r dotenv/config migration.ts",
|
||||||
@@ -34,12 +34,12 @@
|
|||||||
"test": "vitest --config __test__/vitest.config.ts"
|
"test": "vitest --config __test__/vitest.config.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dokploy/server": "workspace:*",
|
|
||||||
"@codemirror/lang-json": "^6.0.1",
|
"@codemirror/lang-json": "^6.0.1",
|
||||||
"@codemirror/lang-yaml": "^6.1.1",
|
"@codemirror/lang-yaml": "^6.1.1",
|
||||||
"@codemirror/language": "^6.10.1",
|
"@codemirror/language": "^6.10.1",
|
||||||
"@codemirror/legacy-modes": "6.4.0",
|
"@codemirror/legacy-modes": "6.4.0",
|
||||||
"@codemirror/view": "6.29.0",
|
"@codemirror/view": "6.29.0",
|
||||||
|
"@dokploy/server": "workspace:*",
|
||||||
"@dokploy/trpc-openapi": "0.0.4",
|
"@dokploy/trpc-openapi": "0.0.4",
|
||||||
"@hookform/resolvers": "^3.3.4",
|
"@hookform/resolvers": "^3.3.4",
|
||||||
"@octokit/webhooks": "^13.2.7",
|
"@octokit/webhooks": "^13.2.7",
|
||||||
@@ -61,6 +61,7 @@
|
|||||||
"@radix-ui/react-tabs": "^1.0.4",
|
"@radix-ui/react-tabs": "^1.0.4",
|
||||||
"@radix-ui/react-toggle": "^1.0.3",
|
"@radix-ui/react-toggle": "^1.0.3",
|
||||||
"@radix-ui/react-tooltip": "^1.0.7",
|
"@radix-ui/react-tooltip": "^1.0.7",
|
||||||
|
"@stripe/stripe-js": "4.8.0",
|
||||||
"@tanstack/react-query": "^4.36.1",
|
"@tanstack/react-query": "^4.36.1",
|
||||||
"@tanstack/react-table": "^8.16.0",
|
"@tanstack/react-table": "^8.16.0",
|
||||||
"@trpc/client": "^10.43.6",
|
"@trpc/client": "^10.43.6",
|
||||||
@@ -89,7 +90,7 @@
|
|||||||
"lucia": "^3.0.1",
|
"lucia": "^3.0.1",
|
||||||
"lucide-react": "^0.312.0",
|
"lucide-react": "^0.312.0",
|
||||||
"nanoid": "3",
|
"nanoid": "3",
|
||||||
"next": "^14.1.3",
|
"next": "^15.0.1",
|
||||||
"next-themes": "^0.2.1",
|
"next-themes": "^0.2.1",
|
||||||
"node-pty": "1.0.0",
|
"node-pty": "1.0.0",
|
||||||
"node-schedule": "2.1.1",
|
"node-schedule": "2.1.1",
|
||||||
@@ -102,6 +103,8 @@
|
|||||||
"recharts": "^2.12.7",
|
"recharts": "^2.12.7",
|
||||||
"slugify": "^1.6.6",
|
"slugify": "^1.6.6",
|
||||||
"sonner": "^1.4.0",
|
"sonner": "^1.4.0",
|
||||||
|
"ssh2": "1.15.0",
|
||||||
|
"stripe": "17.2.0",
|
||||||
"superjson": "^2.2.1",
|
"superjson": "^2.2.1",
|
||||||
"swagger-ui-react": "^5.17.14",
|
"swagger-ui-react": "^5.17.14",
|
||||||
"tailwind-merge": "^2.2.0",
|
"tailwind-merge": "^2.2.0",
|
||||||
@@ -111,11 +114,9 @@
|
|||||||
"ws": "8.16.0",
|
"ws": "8.16.0",
|
||||||
"xterm-addon-fit": "^0.8.0",
|
"xterm-addon-fit": "^0.8.0",
|
||||||
"zod": "^3.23.4",
|
"zod": "^3.23.4",
|
||||||
"zod-form-data": "^2.0.2",
|
"zod-form-data": "^2.0.2"
|
||||||
"ssh2": "1.15.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"autoprefixer": "10.4.12",
|
|
||||||
"@types/adm-zip": "^0.5.5",
|
"@types/adm-zip": "^0.5.5",
|
||||||
"@types/bcrypt": "5.0.2",
|
"@types/bcrypt": "5.0.2",
|
||||||
"@types/js-yaml": "4.0.9",
|
"@types/js-yaml": "4.0.9",
|
||||||
@@ -124,8 +125,10 @@
|
|||||||
"@types/node-schedule": "2.1.6",
|
"@types/node-schedule": "2.1.6",
|
||||||
"@types/react": "^18.2.37",
|
"@types/react": "^18.2.37",
|
||||||
"@types/react-dom": "^18.2.15",
|
"@types/react-dom": "^18.2.15",
|
||||||
|
"@types/ssh2": "1.15.1",
|
||||||
"@types/swagger-ui-react": "^4.18.3",
|
"@types/swagger-ui-react": "^4.18.3",
|
||||||
"@types/ws": "8.5.10",
|
"@types/ws": "8.5.10",
|
||||||
|
"autoprefixer": "10.4.12",
|
||||||
"drizzle-kit": "^0.21.1",
|
"drizzle-kit": "^0.21.1",
|
||||||
"esbuild": "0.20.2",
|
"esbuild": "0.20.2",
|
||||||
"lint-staged": "^15.2.7",
|
"lint-staged": "^15.2.7",
|
||||||
@@ -134,8 +137,7 @@
|
|||||||
"tsx": "^4.7.0",
|
"tsx": "^4.7.0",
|
||||||
"typescript": "^5.4.2",
|
"typescript": "^5.4.2",
|
||||||
"vite-tsconfig-paths": "4.3.2",
|
"vite-tsconfig-paths": "4.3.2",
|
||||||
"vitest": "^1.6.0",
|
"vitest": "^1.6.0"
|
||||||
"@types/ssh2": "1.15.1"
|
|
||||||
},
|
},
|
||||||
"ct3aMetadata": {
|
"ct3aMetadata": {
|
||||||
"initVersion": "7.25.2"
|
"initVersion": "7.25.2"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { db } from "@/server/db";
|
import { db } from "@/server/db";
|
||||||
import { applications } from "@/server/db/schema";
|
import { applications } from "@/server/db/schema";
|
||||||
import type { DeploymentJob } from "@/server/queues/deployments-queue";
|
import type { DeploymentJob } from "@/server/queues/queue-types";
|
||||||
import { myQueue } from "@/server/queues/queueSetup";
|
import { myQueue } from "@/server/queues/queueSetup";
|
||||||
import { deploy } from "@/server/utils/deploy";
|
import { deploy } from "@/server/utils/deploy";
|
||||||
import { IS_CLOUD } from "@dokploy/server";
|
import { IS_CLOUD } from "@dokploy/server";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { db } from "@/server/db";
|
import { db } from "@/server/db";
|
||||||
import { compose } from "@/server/db/schema";
|
import { compose } from "@/server/db/schema";
|
||||||
import type { DeploymentJob } from "@/server/queues/deployments-queue";
|
import type { DeploymentJob } from "@/server/queues/queue-types";
|
||||||
import { myQueue } from "@/server/queues/queueSetup";
|
import { myQueue } from "@/server/queues/queueSetup";
|
||||||
import { deploy } from "@/server/utils/deploy";
|
import { deploy } from "@/server/utils/deploy";
|
||||||
import { IS_CLOUD } from "@dokploy/server";
|
import { IS_CLOUD } from "@dokploy/server";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { db } from "@/server/db";
|
import { db } from "@/server/db";
|
||||||
import { applications, compose, github } from "@/server/db/schema";
|
import { applications, compose, github } from "@/server/db/schema";
|
||||||
import type { DeploymentJob } from "@/server/queues/deployments-queue";
|
import type { DeploymentJob } from "@/server/queues/queue-types";
|
||||||
import { myQueue } from "@/server/queues/queueSetup";
|
import { myQueue } from "@/server/queues/queueSetup";
|
||||||
import { deploy } from "@/server/utils/deploy";
|
import { deploy } from "@/server/utils/deploy";
|
||||||
import { IS_CLOUD } from "@dokploy/server";
|
import { IS_CLOUD } from "@dokploy/server";
|
||||||
|
|||||||
274
apps/dokploy/pages/api/stripe/webhook.ts
Normal file
274
apps/dokploy/pages/api/stripe/webhook.ts
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
import { buffer } from "node:stream/consumers";
|
||||||
|
import { db } from "@/server/db";
|
||||||
|
import { admins, server } from "@/server/db/schema";
|
||||||
|
import { findAdminById } from "@dokploy/server";
|
||||||
|
import { asc, eq } from "drizzle-orm";
|
||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import Stripe from "stripe";
|
||||||
|
|
||||||
|
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET || "";
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
api: {
|
||||||
|
bodyParser: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function handler(
|
||||||
|
req: NextApiRequest,
|
||||||
|
res: NextApiResponse,
|
||||||
|
) {
|
||||||
|
if (!endpointSecret) {
|
||||||
|
return res.status(400).send("Webhook Error: Missing Stripe Secret Key");
|
||||||
|
}
|
||||||
|
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || "", {
|
||||||
|
apiVersion: "2024-09-30.acacia",
|
||||||
|
maxNetworkRetries: 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
const buf = await buffer(req);
|
||||||
|
const sig = req.headers["stripe-signature"] as string;
|
||||||
|
|
||||||
|
let event: Stripe.Event;
|
||||||
|
|
||||||
|
try {
|
||||||
|
event = stripe.webhooks.constructEvent(buf, sig, endpointSecret);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(
|
||||||
|
"Webhook signature verification failed.",
|
||||||
|
err instanceof Error ? err.message : err,
|
||||||
|
);
|
||||||
|
return res.status(400).send("Webhook Error: ");
|
||||||
|
}
|
||||||
|
|
||||||
|
const webhooksAllowed = [
|
||||||
|
"customer.subscription.created",
|
||||||
|
"customer.subscription.deleted",
|
||||||
|
"customer.subscription.updated",
|
||||||
|
"invoice.payment_succeeded",
|
||||||
|
"invoice.payment_failed",
|
||||||
|
"customer.deleted",
|
||||||
|
"checkout.session.completed",
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!webhooksAllowed.includes(event.type)) {
|
||||||
|
return res.status(400).send("Webhook Error: Invalid Event Type");
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (event.type) {
|
||||||
|
case "checkout.session.completed": {
|
||||||
|
const session = event.data.object as Stripe.Checkout.Session;
|
||||||
|
const adminId = session?.metadata?.adminId as string;
|
||||||
|
|
||||||
|
const subscription = await stripe.subscriptions.retrieve(
|
||||||
|
session.subscription as string,
|
||||||
|
);
|
||||||
|
await db
|
||||||
|
.update(admins)
|
||||||
|
.set({
|
||||||
|
stripeCustomerId: session.customer as string,
|
||||||
|
stripeSubscriptionId: session.subscription as string,
|
||||||
|
serversQuantity: subscription?.items?.data?.[0]?.quantity ?? 0,
|
||||||
|
})
|
||||||
|
.where(eq(admins.adminId, adminId))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
const admin = await findAdminById(adminId);
|
||||||
|
if (!admin) {
|
||||||
|
return res.status(400).send("Webhook Error: Admin not found");
|
||||||
|
}
|
||||||
|
const newServersQuantity = admin.serversQuantity;
|
||||||
|
await updateServersBasedOnQuantity(admin.adminId, newServersQuantity);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "customer.subscription.created": {
|
||||||
|
const newSubscription = event.data.object as Stripe.Subscription;
|
||||||
|
|
||||||
|
await db
|
||||||
|
.update(admins)
|
||||||
|
.set({
|
||||||
|
stripeSubscriptionId: newSubscription.id,
|
||||||
|
serversQuantity: 0,
|
||||||
|
stripeCustomerId: newSubscription.customer as string,
|
||||||
|
})
|
||||||
|
.where(eq(admins.stripeCustomerId, newSubscription.customer as string))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "customer.subscription.deleted": {
|
||||||
|
const newSubscription = event.data.object as Stripe.Subscription;
|
||||||
|
|
||||||
|
await db
|
||||||
|
.update(admins)
|
||||||
|
.set({
|
||||||
|
stripeSubscriptionId: null,
|
||||||
|
serversQuantity: 0,
|
||||||
|
})
|
||||||
|
.where(eq(admins.stripeCustomerId, newSubscription.customer as string));
|
||||||
|
|
||||||
|
const admin = await findAdminByStripeCustomerId(
|
||||||
|
newSubscription.customer as string,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!admin) {
|
||||||
|
return res.status(400).send("Webhook Error: Admin not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
await disableServers(admin.adminId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "customer.subscription.updated": {
|
||||||
|
const newSubscription = event.data.object as Stripe.Subscription;
|
||||||
|
await db
|
||||||
|
.update(admins)
|
||||||
|
.set({
|
||||||
|
serversQuantity: newSubscription?.items?.data?.[0]?.quantity ?? 0,
|
||||||
|
})
|
||||||
|
.where(eq(admins.stripeCustomerId, newSubscription.customer as string));
|
||||||
|
|
||||||
|
const admin = await findAdminByStripeCustomerId(
|
||||||
|
newSubscription.customer as string,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!admin) {
|
||||||
|
return res.status(400).send("Webhook Error: Admin not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const newServersQuantity = admin.serversQuantity;
|
||||||
|
await updateServersBasedOnQuantity(admin.adminId, newServersQuantity);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "invoice.payment_succeeded": {
|
||||||
|
const newInvoice = event.data.object as Stripe.Invoice;
|
||||||
|
|
||||||
|
const suscription = await stripe.subscriptions.retrieve(
|
||||||
|
newInvoice.subscription as string,
|
||||||
|
);
|
||||||
|
|
||||||
|
await db
|
||||||
|
.update(admins)
|
||||||
|
.set({
|
||||||
|
serversQuantity: suscription?.items?.data?.[0]?.quantity ?? 0,
|
||||||
|
})
|
||||||
|
.where(eq(admins.stripeCustomerId, suscription.customer as string));
|
||||||
|
|
||||||
|
const admin = await findAdminByStripeCustomerId(
|
||||||
|
suscription.customer as string,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!admin) {
|
||||||
|
return res.status(400).send("Webhook Error: Admin not found");
|
||||||
|
}
|
||||||
|
const newServersQuantity = admin.serversQuantity;
|
||||||
|
await updateServersBasedOnQuantity(admin.adminId, newServersQuantity);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "invoice.payment_failed": {
|
||||||
|
const newInvoice = event.data.object as Stripe.Invoice;
|
||||||
|
await db
|
||||||
|
.update(admins)
|
||||||
|
.set({
|
||||||
|
serversQuantity: 0,
|
||||||
|
})
|
||||||
|
.where(eq(admins.stripeCustomerId, newInvoice.customer as string));
|
||||||
|
|
||||||
|
const admin = await findAdminByStripeCustomerId(
|
||||||
|
newInvoice.customer as string,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!admin) {
|
||||||
|
return res.status(400).send("Webhook Error: Admin not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
await disableServers(admin.adminId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "customer.deleted": {
|
||||||
|
const customer = event.data.object as Stripe.Customer;
|
||||||
|
|
||||||
|
const admin = await findAdminByStripeCustomerId(customer.id);
|
||||||
|
if (!admin) {
|
||||||
|
return res.status(400).send("Webhook Error: Admin not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
await disableServers(admin.adminId);
|
||||||
|
await db
|
||||||
|
.update(admins)
|
||||||
|
.set({
|
||||||
|
stripeCustomerId: null,
|
||||||
|
stripeSubscriptionId: null,
|
||||||
|
serversQuantity: 0,
|
||||||
|
})
|
||||||
|
.where(eq(admins.stripeCustomerId, customer.id));
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
console.log(`Unhandled event type: ${event.type}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).json({ received: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const disableServers = async (adminId: string) => {
|
||||||
|
await db
|
||||||
|
.update(server)
|
||||||
|
.set({
|
||||||
|
serverStatus: "inactive",
|
||||||
|
})
|
||||||
|
.where(eq(server.adminId, adminId));
|
||||||
|
};
|
||||||
|
|
||||||
|
const findAdminByStripeCustomerId = async (stripeCustomerId: string) => {
|
||||||
|
const admin = db.query.admins.findFirst({
|
||||||
|
where: eq(admins.stripeCustomerId, stripeCustomerId),
|
||||||
|
});
|
||||||
|
return admin;
|
||||||
|
};
|
||||||
|
|
||||||
|
const activateServer = async (serverId: string) => {
|
||||||
|
await db
|
||||||
|
.update(server)
|
||||||
|
.set({ serverStatus: "active" })
|
||||||
|
.where(eq(server.serverId, serverId));
|
||||||
|
};
|
||||||
|
|
||||||
|
const deactivateServer = async (serverId: string) => {
|
||||||
|
await db
|
||||||
|
.update(server)
|
||||||
|
.set({ serverStatus: "inactive" })
|
||||||
|
.where(eq(server.serverId, serverId));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const findServersByAdminIdSorted = async (adminId: string) => {
|
||||||
|
const servers = await db.query.server.findMany({
|
||||||
|
where: eq(server.adminId, adminId),
|
||||||
|
orderBy: asc(server.createdAt),
|
||||||
|
});
|
||||||
|
|
||||||
|
return servers;
|
||||||
|
};
|
||||||
|
export const updateServersBasedOnQuantity = async (
|
||||||
|
adminId: string,
|
||||||
|
newServersQuantity: number,
|
||||||
|
) => {
|
||||||
|
const servers = await findServersByAdminIdSorted(adminId);
|
||||||
|
|
||||||
|
if (servers.length > newServersQuantity) {
|
||||||
|
for (const [index, server] of servers.entries()) {
|
||||||
|
if (index < newServersQuantity) {
|
||||||
|
await activateServer(server.serverId);
|
||||||
|
} else {
|
||||||
|
await deactivateServer(server.serverId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const server of servers) {
|
||||||
|
await activateServer(server.serverId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -22,13 +22,20 @@ import {
|
|||||||
BreadcrumbItem,
|
BreadcrumbItem,
|
||||||
BreadcrumbLink,
|
BreadcrumbLink,
|
||||||
} from "@/components/ui/breadcrumb";
|
} from "@/components/ui/breadcrumb";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { validateRequest } from "@dokploy/server";
|
import { validateRequest } from "@dokploy/server";
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import { GlobeIcon } from "lucide-react";
|
import { GlobeIcon, HelpCircle, ServerOff } from "lucide-react";
|
||||||
import type {
|
import type {
|
||||||
GetServerSidePropsContext,
|
GetServerSidePropsContext,
|
||||||
InferGetServerSidePropsType,
|
InferGetServerSidePropsType,
|
||||||
@@ -100,8 +107,40 @@ const Service = (
|
|||||||
</h1>
|
</h1>
|
||||||
<span className="text-sm">{data?.appName}</span>
|
<span className="text-sm">{data?.appName}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="flex flex-row h-fit w-fit gap-2">
|
||||||
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
|
<Badge
|
||||||
|
variant={
|
||||||
|
!data?.serverId
|
||||||
|
? "default"
|
||||||
|
: data?.server?.serverStatus === "active"
|
||||||
|
? "default"
|
||||||
|
: "destructive"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{data?.server?.name || "Dokploy Server"}
|
||||||
|
</Badge>
|
||||||
|
{data?.server?.serverStatus === "inactive" && (
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Label className="break-all w-fit flex flex-row gap-1 items-center">
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground" />
|
||||||
|
</Label>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
className="z-[999] w-[300px]"
|
||||||
|
align="start"
|
||||||
|
side="top"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
You cannot, deploy this application because the server
|
||||||
|
is inactive, please upgrade your plan to add more
|
||||||
|
servers.
|
||||||
|
</span>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{data?.description && (
|
{data?.description && (
|
||||||
@@ -119,90 +158,111 @@ const Service = (
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
</div>
|
</div>
|
||||||
<Tabs
|
|
||||||
value={tab}
|
{data?.server?.serverStatus === "inactive" ? (
|
||||||
defaultValue="general"
|
<div className="flex h-[55vh] border-2 rounded-xl border-dashed p-4">
|
||||||
className="w-full"
|
<div className="max-w-3xl mx-auto flex flex-col items-center justify-center self-center gap-3">
|
||||||
onValueChange={(e) => {
|
<ServerOff className="size-10 text-muted-foreground self-center" />
|
||||||
setSab(e as TabState);
|
<span className="text-center text-base text-muted-foreground">
|
||||||
const newPath = `/dashboard/project/${projectId}/services/application/${applicationId}?tab=${e}`;
|
This service is hosted on the server {data.server.name}, but this
|
||||||
router.push(newPath, undefined, { shallow: true });
|
server has been disabled because your current plan doesn't include
|
||||||
}}
|
enough servers. Please purchase more servers to regain access to
|
||||||
>
|
this application.
|
||||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
</span>
|
||||||
<TabsList
|
<span className="text-center text-base text-muted-foreground">
|
||||||
className={cn(
|
Go to{" "}
|
||||||
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
<Link href="/dashboard/settings/billing" className="text-primary">
|
||||||
data?.serverId ? "md:grid-cols-6" : "md:grid-cols-7",
|
Billing
|
||||||
)}
|
</Link>
|
||||||
>
|
</span>
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
|
||||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
|
||||||
{!data?.serverId && (
|
|
||||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
|
||||||
)}
|
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
|
||||||
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
|
||||||
<TabsTrigger value="domains">Domains</TabsTrigger>
|
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
<div className="flex flex-row gap-2">
|
|
||||||
<UpdateApplication applicationId={applicationId} />
|
|
||||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
|
||||||
<DeleteApplication applicationId={applicationId} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<Tabs
|
||||||
|
value={tab}
|
||||||
|
defaultValue="general"
|
||||||
|
className="w-full"
|
||||||
|
onValueChange={(e) => {
|
||||||
|
setSab(e as TabState);
|
||||||
|
const newPath = `/dashboard/project/${projectId}/services/application/${applicationId}?tab=${e}`;
|
||||||
|
router.push(newPath, undefined, { shallow: true });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||||
|
<TabsList
|
||||||
|
className={cn(
|
||||||
|
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
||||||
|
data?.serverId ? "md:grid-cols-6" : "md:grid-cols-7",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
|
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||||
|
{!data?.serverId && (
|
||||||
|
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||||
|
)}
|
||||||
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
|
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
||||||
|
<TabsTrigger value="domains">Domains</TabsTrigger>
|
||||||
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<div className="flex flex-row gap-2">
|
||||||
|
<UpdateApplication applicationId={applicationId} />
|
||||||
|
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
||||||
|
<DeleteApplication applicationId={applicationId} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<TabsContent value="general">
|
<TabsContent value="general">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
|
||||||
<ShowGeneralApplication applicationId={applicationId} />
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value="environment">
|
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
|
||||||
<ShowEnvironment applicationId={applicationId} />
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
{!data?.serverId && (
|
|
||||||
<TabsContent value="monitoring">
|
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<DockerMonitoring appName={data?.appName || ""} />
|
<ShowGeneralApplication applicationId={applicationId} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
)}
|
<TabsContent value="environment">
|
||||||
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
|
<ShowEnvironment applicationId={applicationId} />
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
{!data?.serverId && (
|
||||||
|
<TabsContent value="monitoring">
|
||||||
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
|
<DockerMonitoring appName={data?.appName || ""} />
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
)}
|
||||||
|
|
||||||
<TabsContent value="logs">
|
<TabsContent value="logs">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowDockerLogs
|
<ShowDockerLogs
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
serverId={data?.serverId || ""}
|
serverId={data?.serverId || ""}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="deployments" className="w-full">
|
<TabsContent value="deployments" className="w-full">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowDeployments applicationId={applicationId} />
|
<ShowDeployments applicationId={applicationId} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="domains" className="w-full">
|
<TabsContent value="domains" className="w-full">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowDomains applicationId={applicationId} />
|
<ShowDomains applicationId={applicationId} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="advanced">
|
<TabsContent value="advanced">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<AddCommand applicationId={applicationId} />
|
<AddCommand applicationId={applicationId} />
|
||||||
<ShowClusterSettings applicationId={applicationId} />
|
<ShowClusterSettings applicationId={applicationId} />
|
||||||
<ShowApplicationResources applicationId={applicationId} />
|
<ShowApplicationResources applicationId={applicationId} />
|
||||||
<ShowVolumes applicationId={applicationId} />
|
<ShowVolumes applicationId={applicationId} />
|
||||||
<ShowRedirects applicationId={applicationId} />
|
<ShowRedirects applicationId={applicationId} />
|
||||||
<ShowSecurity applicationId={applicationId} />
|
<ShowSecurity applicationId={applicationId} />
|
||||||
<ShowPorts applicationId={applicationId} />
|
<ShowPorts applicationId={applicationId} />
|
||||||
<ShowTraefikConfig applicationId={applicationId} />
|
<ShowTraefikConfig applicationId={applicationId} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,13 +16,21 @@ import {
|
|||||||
BreadcrumbItem,
|
BreadcrumbItem,
|
||||||
BreadcrumbLink,
|
BreadcrumbLink,
|
||||||
} from "@/components/ui/breadcrumb";
|
} from "@/components/ui/breadcrumb";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { validateRequest } from "@dokploy/server";
|
import { validateRequest } from "@dokploy/server";
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import { CircuitBoard } from "lucide-react";
|
import { CircuitBoard, ServerOff } from "lucide-react";
|
||||||
|
import { HelpCircle } from "lucide-react";
|
||||||
import type {
|
import type {
|
||||||
GetServerSidePropsContext,
|
GetServerSidePropsContext,
|
||||||
InferGetServerSidePropsType,
|
InferGetServerSidePropsType,
|
||||||
@@ -94,8 +102,40 @@ const Service = (
|
|||||||
</h1>
|
</h1>
|
||||||
<span className="text-sm">{data?.appName}</span>
|
<span className="text-sm">{data?.appName}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="flex flex-row h-fit w-fit gap-2">
|
||||||
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
|
<Badge
|
||||||
|
variant={
|
||||||
|
!data?.serverId
|
||||||
|
? "default"
|
||||||
|
: data?.server?.serverStatus === "active"
|
||||||
|
? "default"
|
||||||
|
: "destructive"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{data?.server?.name || "Dokploy Server"}
|
||||||
|
</Badge>
|
||||||
|
{data?.server?.serverStatus === "inactive" && (
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Label className="break-all w-fit flex flex-row gap-1 items-center">
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground" />
|
||||||
|
</Label>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
className="z-[999] w-[300px]"
|
||||||
|
align="start"
|
||||||
|
side="top"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
You cannot, deploy this application because the server
|
||||||
|
is inactive, please upgrade your plan to add more
|
||||||
|
servers.
|
||||||
|
</span>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{data?.description && (
|
{data?.description && (
|
||||||
<p className="text-sm text-muted-foreground max-w-6xl">
|
<p className="text-sm text-muted-foreground max-w-6xl">
|
||||||
@@ -113,98 +153,118 @@ const Service = (
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
</div>
|
</div>
|
||||||
<Tabs
|
{data?.server?.serverStatus === "inactive" ? (
|
||||||
value={tab}
|
<div className="flex h-[55vh] border-2 rounded-xl border-dashed p-4">
|
||||||
defaultValue="general"
|
<div className="max-w-3xl mx-auto flex flex-col items-center justify-center self-center gap-3">
|
||||||
className="w-full"
|
<ServerOff className="size-10 text-muted-foreground self-center" />
|
||||||
onValueChange={(e) => {
|
<span className="text-center text-base text-muted-foreground">
|
||||||
setSab(e as TabState);
|
This service is hosted on the server {data.server.name}, but this
|
||||||
const newPath = `/dashboard/project/${projectId}/services/compose/${composeId}?tab=${e}`;
|
server has been disabled because your current plan doesn't include
|
||||||
router.push(newPath, undefined, { shallow: true });
|
enough servers. Please purchase more servers to regain access to
|
||||||
}}
|
this application.
|
||||||
>
|
</span>
|
||||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
<span className="text-center text-base text-muted-foreground">
|
||||||
<TabsList
|
Go to{" "}
|
||||||
className={cn(
|
<Link href="/dashboard/settings/billing" className="text-primary">
|
||||||
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
Billing
|
||||||
data?.serverId ? "md:grid-cols-6" : "md:grid-cols-7",
|
</Link>
|
||||||
data?.composeType === "docker-compose" ? "" : "md:grid-cols-6",
|
</span>
|
||||||
data?.serverId && data?.composeType === "stack"
|
|
||||||
? "md:grid-cols-5"
|
|
||||||
: "",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
|
||||||
{data?.composeType === "docker-compose" && (
|
|
||||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
|
||||||
)}
|
|
||||||
{!data?.serverId && (
|
|
||||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
|
||||||
)}
|
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
|
||||||
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
|
||||||
<TabsTrigger value="domains">Domains</TabsTrigger>
|
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
<div className="flex flex-row gap-2">
|
|
||||||
<UpdateCompose composeId={composeId} />
|
|
||||||
|
|
||||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
|
||||||
<DeleteCompose composeId={composeId} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<Tabs
|
||||||
|
value={tab}
|
||||||
|
defaultValue="general"
|
||||||
|
className="w-full"
|
||||||
|
onValueChange={(e) => {
|
||||||
|
setSab(e as TabState);
|
||||||
|
const newPath = `/dashboard/project/${projectId}/services/compose/${composeId}?tab=${e}`;
|
||||||
|
router.push(newPath, undefined, { shallow: true });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||||
|
<TabsList
|
||||||
|
className={cn(
|
||||||
|
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
||||||
|
data?.serverId ? "md:grid-cols-6" : "md:grid-cols-7",
|
||||||
|
data?.composeType === "docker-compose" ? "" : "md:grid-cols-6",
|
||||||
|
data?.serverId && data?.composeType === "stack"
|
||||||
|
? "md:grid-cols-5"
|
||||||
|
: "",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
|
{data?.composeType === "docker-compose" && (
|
||||||
|
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||||
|
)}
|
||||||
|
{!data?.serverId && (
|
||||||
|
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||||
|
)}
|
||||||
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
|
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
||||||
|
<TabsTrigger value="domains">Domains</TabsTrigger>
|
||||||
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<div className="flex flex-row gap-2">
|
||||||
|
<UpdateCompose composeId={composeId} />
|
||||||
|
|
||||||
<TabsContent value="general">
|
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<DeleteCompose composeId={composeId} />
|
||||||
<ShowGeneralCompose composeId={composeId} />
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value="environment">
|
<TabsContent value="general">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
|
||||||
<ShowEnvironmentCompose composeId={composeId} />
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
{!data?.serverId && (
|
|
||||||
<TabsContent value="monitoring">
|
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowMonitoringCompose
|
<ShowGeneralCompose composeId={composeId} />
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="environment">
|
||||||
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
|
<ShowEnvironmentCompose composeId={composeId} />
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
{!data?.serverId && (
|
||||||
|
<TabsContent value="monitoring">
|
||||||
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
|
<ShowMonitoringCompose
|
||||||
|
serverId={data?.serverId || ""}
|
||||||
|
appName={data?.appName || ""}
|
||||||
|
appType={data?.composeType || "docker-compose"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<TabsContent value="logs">
|
||||||
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
|
<ShowDockerLogsCompose
|
||||||
serverId={data?.serverId || ""}
|
serverId={data?.serverId || ""}
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
appType={data?.composeType || "docker-compose"}
|
appType={data?.composeType || "docker-compose"}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
)}
|
|
||||||
|
|
||||||
<TabsContent value="logs">
|
<TabsContent value="deployments">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowDockerLogsCompose
|
<ShowDeploymentsCompose composeId={composeId} />
|
||||||
serverId={data?.serverId || ""}
|
</div>
|
||||||
appName={data?.appName || ""}
|
</TabsContent>
|
||||||
appType={data?.composeType || "docker-compose"}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="deployments">
|
<TabsContent value="domains">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowDeploymentsCompose composeId={composeId} />
|
<ShowDomainsCompose composeId={composeId} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
<TabsContent value="advanced">
|
||||||
<TabsContent value="domains">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<AddCommandCompose composeId={composeId} />
|
||||||
<ShowDomainsCompose composeId={composeId} />
|
<ShowVolumesCompose composeId={composeId} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="advanced">
|
</Tabs>
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
)}
|
||||||
<AddCommandCompose composeId={composeId} />
|
|
||||||
<ShowVolumesCompose composeId={composeId} />
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,12 +17,20 @@ import {
|
|||||||
BreadcrumbItem,
|
BreadcrumbItem,
|
||||||
BreadcrumbLink,
|
BreadcrumbLink,
|
||||||
} from "@/components/ui/breadcrumb";
|
} from "@/components/ui/breadcrumb";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { validateRequest } from "@dokploy/server";
|
import { validateRequest } from "@dokploy/server";
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
|
import { HelpCircle, ServerOff } from "lucide-react";
|
||||||
import type {
|
import type {
|
||||||
GetServerSidePropsContext,
|
GetServerSidePropsContext,
|
||||||
InferGetServerSidePropsType,
|
InferGetServerSidePropsType,
|
||||||
@@ -82,8 +90,40 @@ const Mariadb = (
|
|||||||
</h1>
|
</h1>
|
||||||
<span className="text-sm">{data?.appName}</span>
|
<span className="text-sm">{data?.appName}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="flex flex-row h-fit w-fit gap-2">
|
||||||
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
|
<Badge
|
||||||
|
variant={
|
||||||
|
!data?.serverId
|
||||||
|
? "default"
|
||||||
|
: data?.server?.serverStatus === "active"
|
||||||
|
? "default"
|
||||||
|
: "destructive"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{data?.server?.name || "Dokploy Server"}
|
||||||
|
</Badge>
|
||||||
|
{data?.server?.serverStatus === "inactive" && (
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Label className="break-all w-fit flex flex-row gap-1 items-center">
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground" />
|
||||||
|
</Label>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
className="z-[999] w-[300px]"
|
||||||
|
align="start"
|
||||||
|
side="top"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
You cannot, deploy this application because the server
|
||||||
|
is inactive, please upgrade your plan to add more
|
||||||
|
servers.
|
||||||
|
</span>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{data?.description && (
|
{data?.description && (
|
||||||
<p className="text-sm text-muted-foreground max-w-6xl">
|
<p className="text-sm text-muted-foreground max-w-6xl">
|
||||||
@@ -99,79 +139,99 @@ const Mariadb = (
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
</div>
|
</div>
|
||||||
<Tabs
|
{data?.server?.serverStatus === "inactive" ? (
|
||||||
value={tab}
|
<div className="flex h-[55vh] border-2 rounded-xl border-dashed p-4">
|
||||||
defaultValue="general"
|
<div className="max-w-3xl mx-auto flex flex-col items-center justify-center self-center gap-3">
|
||||||
className="w-full"
|
<ServerOff className="size-10 text-muted-foreground self-center" />
|
||||||
onValueChange={(e) => {
|
<span className="text-center text-base text-muted-foreground">
|
||||||
setSab(e as TabState);
|
This service is hosted on the server {data.server.name}, but this
|
||||||
const newPath = `/dashboard/project/${projectId}/services/mariadb/${mariadbId}?tab=${e}`;
|
server has been disabled because your current plan doesn't include
|
||||||
|
enough servers. Please purchase more servers to regain access to
|
||||||
router.push(newPath, undefined, { shallow: true });
|
this application.
|
||||||
}}
|
</span>
|
||||||
>
|
<span className="text-center text-base text-muted-foreground">
|
||||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
Go to{" "}
|
||||||
<TabsList
|
<Link href="/dashboard/settings/billing" className="text-primary">
|
||||||
className={cn(
|
Billing
|
||||||
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
</Link>
|
||||||
data?.serverId ? "md:grid-cols-5" : "md:grid-cols-6",
|
</span>
|
||||||
)}
|
|
||||||
>
|
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
|
||||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
|
||||||
{!data?.serverId && (
|
|
||||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
|
||||||
)}
|
|
||||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
<div className="flex flex-row gap-2">
|
|
||||||
<UpdateMariadb mariadbId={mariadbId} />
|
|
||||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
|
||||||
<DeleteMariadb mariadbId={mariadbId} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<Tabs
|
||||||
|
value={tab}
|
||||||
|
defaultValue="general"
|
||||||
|
className="w-full"
|
||||||
|
onValueChange={(e) => {
|
||||||
|
setSab(e as TabState);
|
||||||
|
const newPath = `/dashboard/project/${projectId}/services/mariadb/${mariadbId}?tab=${e}`;
|
||||||
|
|
||||||
<TabsContent value="general">
|
router.push(newPath, undefined, { shallow: true });
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
}}
|
||||||
<ShowGeneralMariadb mariadbId={mariadbId} />
|
>
|
||||||
<ShowInternalMariadbCredentials mariadbId={mariadbId} />
|
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||||
<ShowExternalMariadbCredentials mariadbId={mariadbId} />
|
<TabsList
|
||||||
|
className={cn(
|
||||||
|
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
||||||
|
data?.serverId ? "md:grid-cols-5" : "md:grid-cols-6",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
|
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||||
|
{!data?.serverId && (
|
||||||
|
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||||
|
)}
|
||||||
|
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||||
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<div className="flex flex-row gap-2">
|
||||||
|
<UpdateMariadb mariadbId={mariadbId} />
|
||||||
|
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
||||||
|
<DeleteMariadb mariadbId={mariadbId} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value="environment">
|
<TabsContent value="general">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
|
||||||
<ShowMariadbEnvironment mariadbId={mariadbId} />
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
{!data?.serverId && (
|
|
||||||
<TabsContent value="monitoring">
|
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<DockerMonitoring appName={data?.appName || ""} />
|
<ShowGeneralMariadb mariadbId={mariadbId} />
|
||||||
|
<ShowInternalMariadbCredentials mariadbId={mariadbId} />
|
||||||
|
<ShowExternalMariadbCredentials mariadbId={mariadbId} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
)}
|
<TabsContent value="environment">
|
||||||
<TabsContent value="logs">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<ShowMariadbEnvironment mariadbId={mariadbId} />
|
||||||
<ShowDockerLogs
|
</div>
|
||||||
serverId={data?.serverId || ""}
|
</TabsContent>
|
||||||
appName={data?.appName || ""}
|
{!data?.serverId && (
|
||||||
/>
|
<TabsContent value="monitoring">
|
||||||
</div>
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
</TabsContent>
|
<DockerMonitoring appName={data?.appName || ""} />
|
||||||
<TabsContent value="backups">
|
</div>
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
</TabsContent>
|
||||||
<ShowBackupMariadb mariadbId={mariadbId} />
|
)}
|
||||||
</div>
|
<TabsContent value="logs">
|
||||||
</TabsContent>
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<TabsContent value="advanced">
|
<ShowDockerLogs
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
serverId={data?.serverId || ""}
|
||||||
<ShowAdvancedMariadb mariadbId={mariadbId} />
|
appName={data?.appName || ""}
|
||||||
</div>
|
/>
|
||||||
</TabsContent>
|
</div>
|
||||||
</Tabs>
|
</TabsContent>
|
||||||
|
<TabsContent value="backups">
|
||||||
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
|
<ShowBackupMariadb mariadbId={mariadbId} />
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="advanced">
|
||||||
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
|
<ShowAdvancedMariadb mariadbId={mariadbId} />
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,12 +17,20 @@ import {
|
|||||||
BreadcrumbItem,
|
BreadcrumbItem,
|
||||||
BreadcrumbLink,
|
BreadcrumbLink,
|
||||||
} from "@/components/ui/breadcrumb";
|
} from "@/components/ui/breadcrumb";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { validateRequest } from "@dokploy/server";
|
import { validateRequest } from "@dokploy/server";
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
|
import { HelpCircle, ServerOff } from "lucide-react";
|
||||||
import type {
|
import type {
|
||||||
GetServerSidePropsContext,
|
GetServerSidePropsContext,
|
||||||
InferGetServerSidePropsType,
|
InferGetServerSidePropsType,
|
||||||
@@ -83,8 +91,40 @@ const Mongo = (
|
|||||||
</h1>
|
</h1>
|
||||||
<span className="text-sm">{data?.appName}</span>
|
<span className="text-sm">{data?.appName}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="flex flex-row h-fit w-fit gap-2">
|
||||||
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
|
<Badge
|
||||||
|
variant={
|
||||||
|
!data?.serverId
|
||||||
|
? "default"
|
||||||
|
: data?.server?.serverStatus === "active"
|
||||||
|
? "default"
|
||||||
|
: "destructive"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{data?.server?.name || "Dokploy Server"}
|
||||||
|
</Badge>
|
||||||
|
{data?.server?.serverStatus === "inactive" && (
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Label className="break-all w-fit flex flex-row gap-1 items-center">
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground" />
|
||||||
|
</Label>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
className="z-[999] w-[300px]"
|
||||||
|
align="start"
|
||||||
|
side="top"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
You cannot, deploy this application because the server
|
||||||
|
is inactive, please upgrade your plan to add more
|
||||||
|
servers.
|
||||||
|
</span>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{data?.description && (
|
{data?.description && (
|
||||||
<p className="text-sm text-muted-foreground max-w-6xl">
|
<p className="text-sm text-muted-foreground max-w-6xl">
|
||||||
@@ -100,80 +140,100 @@ const Mongo = (
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
</div>
|
</div>
|
||||||
<Tabs
|
{data?.server?.serverStatus === "inactive" ? (
|
||||||
value={tab}
|
<div className="flex h-[55vh] border-2 rounded-xl border-dashed p-4">
|
||||||
defaultValue="general"
|
<div className="max-w-3xl mx-auto flex flex-col items-center justify-center self-center gap-3">
|
||||||
className="w-full"
|
<ServerOff className="size-10 text-muted-foreground self-center" />
|
||||||
onValueChange={(e) => {
|
<span className="text-center text-base text-muted-foreground">
|
||||||
setSab(e as TabState);
|
This service is hosted on the server {data.server.name}, but this
|
||||||
const newPath = `/dashboard/project/${projectId}/services/mongo/${mongoId}?tab=${e}`;
|
server has been disabled because your current plan doesn't include
|
||||||
|
enough servers. Please purchase more servers to regain access to
|
||||||
router.push(newPath, undefined, { shallow: true });
|
this application.
|
||||||
}}
|
</span>
|
||||||
>
|
<span className="text-center text-base text-muted-foreground">
|
||||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
Go to{" "}
|
||||||
<TabsList
|
<Link href="/dashboard/settings/billing" className="text-primary">
|
||||||
className={cn(
|
Billing
|
||||||
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
</Link>
|
||||||
data?.serverId ? "md:grid-cols-5" : "md:grid-cols-6",
|
</span>
|
||||||
)}
|
|
||||||
>
|
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
|
||||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
|
||||||
{!data?.serverId && (
|
|
||||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
|
||||||
)}
|
|
||||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
<div className="flex flex-row gap-2">
|
|
||||||
<UpdateMongo mongoId={mongoId} />
|
|
||||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
|
||||||
<DeleteMongo mongoId={mongoId} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<Tabs
|
||||||
|
value={tab}
|
||||||
|
defaultValue="general"
|
||||||
|
className="w-full"
|
||||||
|
onValueChange={(e) => {
|
||||||
|
setSab(e as TabState);
|
||||||
|
const newPath = `/dashboard/project/${projectId}/services/mongo/${mongoId}?tab=${e}`;
|
||||||
|
|
||||||
<TabsContent value="general">
|
router.push(newPath, undefined, { shallow: true });
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
}}
|
||||||
<ShowGeneralMongo mongoId={mongoId} />
|
>
|
||||||
<ShowInternalMongoCredentials mongoId={mongoId} />
|
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||||
<ShowExternalMongoCredentials mongoId={mongoId} />
|
<TabsList
|
||||||
|
className={cn(
|
||||||
|
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
||||||
|
data?.serverId ? "md:grid-cols-5" : "md:grid-cols-6",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
|
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||||
|
{!data?.serverId && (
|
||||||
|
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||||
|
)}
|
||||||
|
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||||
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<div className="flex flex-row gap-2">
|
||||||
|
<UpdateMongo mongoId={mongoId} />
|
||||||
|
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
||||||
|
<DeleteMongo mongoId={mongoId} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value="environment">
|
<TabsContent value="general">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
|
||||||
<ShowMongoEnvironment mongoId={mongoId} />
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
{!data?.serverId && (
|
|
||||||
<TabsContent value="monitoring">
|
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<DockerMonitoring appName={data?.appName || ""} />
|
<ShowGeneralMongo mongoId={mongoId} />
|
||||||
|
<ShowInternalMongoCredentials mongoId={mongoId} />
|
||||||
|
<ShowExternalMongoCredentials mongoId={mongoId} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
)}
|
<TabsContent value="environment">
|
||||||
<TabsContent value="logs">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<ShowMongoEnvironment mongoId={mongoId} />
|
||||||
<ShowDockerLogs
|
</div>
|
||||||
serverId={data?.serverId || ""}
|
</TabsContent>
|
||||||
appName={data?.appName || ""}
|
{!data?.serverId && (
|
||||||
/>
|
<TabsContent value="monitoring">
|
||||||
</div>
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
</TabsContent>
|
<DockerMonitoring appName={data?.appName || ""} />
|
||||||
<TabsContent value="backups">
|
</div>
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
</TabsContent>
|
||||||
<ShowBackupMongo mongoId={mongoId} />
|
)}
|
||||||
</div>
|
<TabsContent value="logs">
|
||||||
</TabsContent>
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<TabsContent value="advanced">
|
<ShowDockerLogs
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
serverId={data?.serverId || ""}
|
||||||
<ShowAdvancedMongo mongoId={mongoId} />
|
appName={data?.appName || ""}
|
||||||
</div>
|
/>
|
||||||
</TabsContent>
|
</div>
|
||||||
</Tabs>
|
</TabsContent>
|
||||||
|
<TabsContent value="backups">
|
||||||
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
|
<ShowBackupMongo mongoId={mongoId} />
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="advanced">
|
||||||
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
|
<ShowAdvancedMongo mongoId={mongoId} />
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,12 +17,20 @@ import {
|
|||||||
BreadcrumbItem,
|
BreadcrumbItem,
|
||||||
BreadcrumbLink,
|
BreadcrumbLink,
|
||||||
} from "@/components/ui/breadcrumb";
|
} from "@/components/ui/breadcrumb";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { validateRequest } from "@dokploy/server";
|
import { validateRequest } from "@dokploy/server";
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
|
import { HelpCircle, ServerOff } from "lucide-react";
|
||||||
import type {
|
import type {
|
||||||
GetServerSidePropsContext,
|
GetServerSidePropsContext,
|
||||||
InferGetServerSidePropsType,
|
InferGetServerSidePropsType,
|
||||||
@@ -81,8 +89,40 @@ const MySql = (
|
|||||||
</h1>
|
</h1>
|
||||||
<span className="text-sm">{data?.appName}</span>
|
<span className="text-sm">{data?.appName}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="flex flex-row h-fit w-fit gap-2">
|
||||||
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
|
<Badge
|
||||||
|
variant={
|
||||||
|
!data?.serverId
|
||||||
|
? "default"
|
||||||
|
: data?.server?.serverStatus === "active"
|
||||||
|
? "default"
|
||||||
|
: "destructive"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{data?.server?.name || "Dokploy Server"}
|
||||||
|
</Badge>
|
||||||
|
{data?.server?.serverStatus === "inactive" && (
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Label className="break-all w-fit flex flex-row gap-1 items-center">
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground" />
|
||||||
|
</Label>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
className="z-[999] w-[300px]"
|
||||||
|
align="start"
|
||||||
|
side="top"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
You cannot, deploy this application because the server
|
||||||
|
is inactive, please upgrade your plan to add more
|
||||||
|
servers.
|
||||||
|
</span>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{data?.description && (
|
{data?.description && (
|
||||||
<p className="text-sm text-muted-foreground max-w-6xl">
|
<p className="text-sm text-muted-foreground max-w-6xl">
|
||||||
@@ -99,80 +139,100 @@ const MySql = (
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
</div>
|
</div>
|
||||||
<Tabs
|
{data?.server?.serverStatus === "inactive" ? (
|
||||||
value={tab}
|
<div className="flex h-[55vh] border-2 rounded-xl border-dashed p-4">
|
||||||
defaultValue="general"
|
<div className="max-w-3xl mx-auto flex flex-col items-center justify-center self-center gap-3">
|
||||||
className="w-full"
|
<ServerOff className="size-10 text-muted-foreground self-center" />
|
||||||
onValueChange={(e) => {
|
<span className="text-center text-base text-muted-foreground">
|
||||||
setSab(e as TabState);
|
This service is hosted on the server {data.server.name}, but this
|
||||||
const newPath = `/dashboard/project/${projectId}/services/mysql/${mysqlId}?tab=${e}`;
|
server has been disabled because your current plan doesn't include
|
||||||
|
enough servers. Please purchase more servers to regain access to
|
||||||
router.push(newPath, undefined, { shallow: true });
|
this application.
|
||||||
}}
|
</span>
|
||||||
>
|
<span className="text-center text-base text-muted-foreground">
|
||||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
Go to{" "}
|
||||||
<TabsList
|
<Link href="/dashboard/settings/billing" className="text-primary">
|
||||||
className={cn(
|
Billing
|
||||||
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
</Link>
|
||||||
data?.serverId ? "md:grid-cols-5" : "md:grid-cols-6",
|
</span>
|
||||||
)}
|
|
||||||
>
|
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
|
||||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
|
||||||
{!data?.serverId && (
|
|
||||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
|
||||||
)}
|
|
||||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
<div className="flex flex-row gap-2">
|
|
||||||
<UpdateMysql mysqlId={mysqlId} />
|
|
||||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
|
||||||
<DeleteMysql mysqlId={mysqlId} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<Tabs
|
||||||
|
value={tab}
|
||||||
|
defaultValue="general"
|
||||||
|
className="w-full"
|
||||||
|
onValueChange={(e) => {
|
||||||
|
setSab(e as TabState);
|
||||||
|
const newPath = `/dashboard/project/${projectId}/services/mysql/${mysqlId}?tab=${e}`;
|
||||||
|
|
||||||
<TabsContent value="general">
|
router.push(newPath, undefined, { shallow: true });
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
}}
|
||||||
<ShowGeneralMysql mysqlId={mysqlId} />
|
>
|
||||||
<ShowInternalMysqlCredentials mysqlId={mysqlId} />
|
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||||
<ShowExternalMysqlCredentials mysqlId={mysqlId} />
|
<TabsList
|
||||||
|
className={cn(
|
||||||
|
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
||||||
|
data?.serverId ? "md:grid-cols-5" : "md:grid-cols-6",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
|
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||||
|
{!data?.serverId && (
|
||||||
|
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||||
|
)}
|
||||||
|
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||||
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<div className="flex flex-row gap-2">
|
||||||
|
<UpdateMysql mysqlId={mysqlId} />
|
||||||
|
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
||||||
|
<DeleteMysql mysqlId={mysqlId} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value="environment" className="w-full">
|
<TabsContent value="general">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
|
||||||
<ShowMysqlEnvironment mysqlId={mysqlId} />
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
{!data?.serverId && (
|
|
||||||
<TabsContent value="monitoring">
|
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<DockerMonitoring appName={data?.appName || ""} />
|
<ShowGeneralMysql mysqlId={mysqlId} />
|
||||||
|
<ShowInternalMysqlCredentials mysqlId={mysqlId} />
|
||||||
|
<ShowExternalMysqlCredentials mysqlId={mysqlId} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
)}
|
<TabsContent value="environment" className="w-full">
|
||||||
<TabsContent value="logs">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<ShowMysqlEnvironment mysqlId={mysqlId} />
|
||||||
<ShowDockerLogs
|
</div>
|
||||||
serverId={data?.serverId || ""}
|
</TabsContent>
|
||||||
appName={data?.appName || ""}
|
{!data?.serverId && (
|
||||||
/>
|
<TabsContent value="monitoring">
|
||||||
</div>
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
</TabsContent>
|
<DockerMonitoring appName={data?.appName || ""} />
|
||||||
<TabsContent value="backups">
|
</div>
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
</TabsContent>
|
||||||
<ShowBackupMySql mysqlId={mysqlId} />
|
)}
|
||||||
</div>
|
<TabsContent value="logs">
|
||||||
</TabsContent>
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<TabsContent value="advanced">
|
<ShowDockerLogs
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
serverId={data?.serverId || ""}
|
||||||
<ShowAdvancedMysql mysqlId={mysqlId} />
|
appName={data?.appName || ""}
|
||||||
</div>
|
/>
|
||||||
</TabsContent>
|
</div>
|
||||||
</Tabs>
|
</TabsContent>
|
||||||
|
<TabsContent value="backups">
|
||||||
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
|
<ShowBackupMySql mysqlId={mysqlId} />
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="advanced">
|
||||||
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
|
<ShowAdvancedMysql mysqlId={mysqlId} />
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,12 +17,20 @@ import {
|
|||||||
BreadcrumbItem,
|
BreadcrumbItem,
|
||||||
BreadcrumbLink,
|
BreadcrumbLink,
|
||||||
} from "@/components/ui/breadcrumb";
|
} from "@/components/ui/breadcrumb";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { validateRequest } from "@dokploy/server";
|
import { validateRequest } from "@dokploy/server";
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
|
import { HelpCircle, ServerOff } from "lucide-react";
|
||||||
import type {
|
import type {
|
||||||
GetServerSidePropsContext,
|
GetServerSidePropsContext,
|
||||||
InferGetServerSidePropsType,
|
InferGetServerSidePropsType,
|
||||||
@@ -82,8 +90,40 @@ const Postgresql = (
|
|||||||
</h1>
|
</h1>
|
||||||
<span className="text-sm">{data?.appName}</span>
|
<span className="text-sm">{data?.appName}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="flex flex-row h-fit w-fit gap-2">
|
||||||
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
|
<Badge
|
||||||
|
variant={
|
||||||
|
!data?.serverId
|
||||||
|
? "default"
|
||||||
|
: data?.server?.serverStatus === "active"
|
||||||
|
? "default"
|
||||||
|
: "destructive"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{data?.server?.name || "Dokploy Server"}
|
||||||
|
</Badge>
|
||||||
|
{data?.server?.serverStatus === "inactive" && (
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Label className="break-all w-fit flex flex-row gap-1 items-center">
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground" />
|
||||||
|
</Label>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
className="z-[999] w-[300px]"
|
||||||
|
align="start"
|
||||||
|
side="top"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
You cannot, deploy this application because the server
|
||||||
|
is inactive, please upgrade your plan to add more
|
||||||
|
servers.
|
||||||
|
</span>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{data?.description && (
|
{data?.description && (
|
||||||
<p className="text-sm text-muted-foreground max-w-6xl">
|
<p className="text-sm text-muted-foreground max-w-6xl">
|
||||||
@@ -100,80 +140,100 @@ const Postgresql = (
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
</div>
|
</div>
|
||||||
<Tabs
|
{data?.server?.serverStatus === "inactive" ? (
|
||||||
value={tab}
|
<div className="flex h-[55vh] border-2 rounded-xl border-dashed p-4">
|
||||||
defaultValue="general"
|
<div className="max-w-3xl mx-auto flex flex-col items-center justify-center self-center gap-3">
|
||||||
className="w-full"
|
<ServerOff className="size-10 text-muted-foreground self-center" />
|
||||||
onValueChange={(e) => {
|
<span className="text-center text-base text-muted-foreground">
|
||||||
setSab(e as TabState);
|
This service is hosted on the server {data.server.name}, but this
|
||||||
const newPath = `/dashboard/project/${projectId}/services/postgres/${postgresId}?tab=${e}`;
|
server has been disabled because your current plan doesn't include
|
||||||
|
enough servers. Please purchase more servers to regain access to
|
||||||
router.push(newPath, undefined, { shallow: true });
|
this application.
|
||||||
}}
|
</span>
|
||||||
>
|
<span className="text-center text-base text-muted-foreground">
|
||||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
Go to{" "}
|
||||||
<TabsList
|
<Link href="/dashboard/settings/billing" className="text-primary">
|
||||||
className={cn(
|
Billing
|
||||||
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
</Link>
|
||||||
data?.serverId ? "md:grid-cols-5" : "md:grid-cols-6",
|
</span>
|
||||||
)}
|
|
||||||
>
|
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
|
||||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
|
||||||
{!data?.serverId && (
|
|
||||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
|
||||||
)}
|
|
||||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
<div className="flex flex-row gap-2">
|
|
||||||
<UpdatePostgres postgresId={postgresId} />
|
|
||||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
|
||||||
<DeletePostgres postgresId={postgresId} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<Tabs
|
||||||
|
value={tab}
|
||||||
|
defaultValue="general"
|
||||||
|
className="w-full"
|
||||||
|
onValueChange={(e) => {
|
||||||
|
setSab(e as TabState);
|
||||||
|
const newPath = `/dashboard/project/${projectId}/services/postgres/${postgresId}?tab=${e}`;
|
||||||
|
|
||||||
<TabsContent value="general">
|
router.push(newPath, undefined, { shallow: true });
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
}}
|
||||||
<ShowGeneralPostgres postgresId={postgresId} />
|
>
|
||||||
<ShowInternalPostgresCredentials postgresId={postgresId} />
|
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||||
<ShowExternalPostgresCredentials postgresId={postgresId} />
|
<TabsList
|
||||||
|
className={cn(
|
||||||
|
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
||||||
|
data?.serverId ? "md:grid-cols-5" : "md:grid-cols-6",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
|
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||||
|
{!data?.serverId && (
|
||||||
|
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||||
|
)}
|
||||||
|
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||||
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<div className="flex flex-row gap-2">
|
||||||
|
<UpdatePostgres postgresId={postgresId} />
|
||||||
|
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
||||||
|
<DeletePostgres postgresId={postgresId} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value="environment">
|
<TabsContent value="general">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
|
||||||
<ShowPostgresEnvironment postgresId={postgresId} />
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
{!data?.serverId && (
|
|
||||||
<TabsContent value="monitoring">
|
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<DockerMonitoring appName={data?.appName || ""} />
|
<ShowGeneralPostgres postgresId={postgresId} />
|
||||||
|
<ShowInternalPostgresCredentials postgresId={postgresId} />
|
||||||
|
<ShowExternalPostgresCredentials postgresId={postgresId} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
)}
|
<TabsContent value="environment">
|
||||||
<TabsContent value="logs">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<ShowPostgresEnvironment postgresId={postgresId} />
|
||||||
<ShowDockerLogs
|
</div>
|
||||||
serverId={data?.serverId || ""}
|
</TabsContent>
|
||||||
appName={data?.appName || ""}
|
{!data?.serverId && (
|
||||||
/>
|
<TabsContent value="monitoring">
|
||||||
</div>
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
</TabsContent>
|
<DockerMonitoring appName={data?.appName || ""} />
|
||||||
<TabsContent value="backups">
|
</div>
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
</TabsContent>
|
||||||
<ShowBackupPostgres postgresId={postgresId} />
|
)}
|
||||||
</div>
|
<TabsContent value="logs">
|
||||||
</TabsContent>
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<TabsContent value="advanced">
|
<ShowDockerLogs
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
serverId={data?.serverId || ""}
|
||||||
<ShowAdvancedPostgres postgresId={postgresId} />
|
appName={data?.appName || ""}
|
||||||
</div>
|
/>
|
||||||
</TabsContent>
|
</div>
|
||||||
</Tabs>
|
</TabsContent>
|
||||||
|
<TabsContent value="backups">
|
||||||
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
|
<ShowBackupPostgres postgresId={postgresId} />
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="advanced">
|
||||||
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
|
<ShowAdvancedPostgres postgresId={postgresId} />
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,12 +16,20 @@ import {
|
|||||||
BreadcrumbItem,
|
BreadcrumbItem,
|
||||||
BreadcrumbLink,
|
BreadcrumbLink,
|
||||||
} from "@/components/ui/breadcrumb";
|
} from "@/components/ui/breadcrumb";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { validateRequest } from "@dokploy/server";
|
import { validateRequest } from "@dokploy/server";
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
|
import { HelpCircle, ServerOff } from "lucide-react";
|
||||||
import type {
|
import type {
|
||||||
GetServerSidePropsContext,
|
GetServerSidePropsContext,
|
||||||
InferGetServerSidePropsType,
|
InferGetServerSidePropsType,
|
||||||
@@ -81,8 +89,40 @@ const Redis = (
|
|||||||
</h1>
|
</h1>
|
||||||
<span className="text-sm">{data?.appName}</span>
|
<span className="text-sm">{data?.appName}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="flex flex-row h-fit w-fit gap-2">
|
||||||
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
|
<Badge
|
||||||
|
variant={
|
||||||
|
!data?.serverId
|
||||||
|
? "default"
|
||||||
|
: data?.server?.serverStatus === "active"
|
||||||
|
? "default"
|
||||||
|
: "destructive"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{data?.server?.name || "Dokploy Server"}
|
||||||
|
</Badge>
|
||||||
|
{data?.server?.serverStatus === "inactive" && (
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Label className="break-all w-fit flex flex-row gap-1 items-center">
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground" />
|
||||||
|
</Label>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
className="z-[999] w-[300px]"
|
||||||
|
align="start"
|
||||||
|
side="top"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
You cannot, deploy this application because the server
|
||||||
|
is inactive, please upgrade your plan to add more
|
||||||
|
servers.
|
||||||
|
</span>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{data?.description && (
|
{data?.description && (
|
||||||
<p className="text-sm text-muted-foreground max-w-6xl">
|
<p className="text-sm text-muted-foreground max-w-6xl">
|
||||||
@@ -99,74 +139,94 @@ const Redis = (
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
</div>
|
</div>
|
||||||
<Tabs
|
{data?.server?.serverStatus === "inactive" ? (
|
||||||
value={tab}
|
<div className="flex h-[55vh] border-2 rounded-xl border-dashed p-4">
|
||||||
defaultValue="general"
|
<div className="max-w-3xl mx-auto flex flex-col items-center justify-center self-center gap-3">
|
||||||
className="w-full"
|
<ServerOff className="size-10 text-muted-foreground self-center" />
|
||||||
onValueChange={(e) => {
|
<span className="text-center text-base text-muted-foreground">
|
||||||
setSab(e as TabState);
|
This service is hosted on the server {data.server.name}, but this
|
||||||
const newPath = `/dashboard/project/${projectId}/services/redis/${redisId}?tab=${e}`;
|
server has been disabled because your current plan doesn't include
|
||||||
|
enough servers. Please purchase more servers to regain access to
|
||||||
router.push(newPath, undefined, { shallow: true });
|
this application.
|
||||||
}}
|
</span>
|
||||||
>
|
<span className="text-center text-base text-muted-foreground">
|
||||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
Go to{" "}
|
||||||
<TabsList
|
<Link href="/dashboard/settings/billing" className="text-primary">
|
||||||
className={cn(
|
Billing
|
||||||
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
</Link>
|
||||||
data?.serverId ? "md:grid-cols-4" : "md:grid-cols-5",
|
</span>
|
||||||
)}
|
|
||||||
>
|
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
|
||||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
|
||||||
{!data?.serverId && (
|
|
||||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
|
||||||
)}
|
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
<div className="flex flex-row gap-2">
|
|
||||||
<UpdateRedis redisId={redisId} />
|
|
||||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
|
||||||
<DeleteRedis redisId={redisId} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<Tabs
|
||||||
|
value={tab}
|
||||||
|
defaultValue="general"
|
||||||
|
className="w-full"
|
||||||
|
onValueChange={(e) => {
|
||||||
|
setSab(e as TabState);
|
||||||
|
const newPath = `/dashboard/project/${projectId}/services/redis/${redisId}?tab=${e}`;
|
||||||
|
|
||||||
<TabsContent value="general">
|
router.push(newPath, undefined, { shallow: true });
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
}}
|
||||||
<ShowGeneralRedis redisId={redisId} />
|
>
|
||||||
<ShowInternalRedisCredentials redisId={redisId} />
|
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||||
<ShowExternalRedisCredentials redisId={redisId} />
|
<TabsList
|
||||||
|
className={cn(
|
||||||
|
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
||||||
|
data?.serverId ? "md:grid-cols-4" : "md:grid-cols-5",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
|
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||||
|
{!data?.serverId && (
|
||||||
|
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||||
|
)}
|
||||||
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<div className="flex flex-row gap-2">
|
||||||
|
<UpdateRedis redisId={redisId} />
|
||||||
|
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
||||||
|
<DeleteRedis redisId={redisId} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value="environment">
|
<TabsContent value="general">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
|
||||||
<ShowRedisEnvironment redisId={redisId} />
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
{!data?.serverId && (
|
|
||||||
<TabsContent value="monitoring">
|
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<DockerMonitoring appName={data?.appName || ""} />
|
<ShowGeneralRedis redisId={redisId} />
|
||||||
|
<ShowInternalRedisCredentials redisId={redisId} />
|
||||||
|
<ShowExternalRedisCredentials redisId={redisId} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
)}
|
<TabsContent value="environment">
|
||||||
<TabsContent value="logs">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<ShowRedisEnvironment redisId={redisId} />
|
||||||
<ShowDockerLogs
|
</div>
|
||||||
serverId={data?.serverId || ""}
|
</TabsContent>
|
||||||
appName={data?.appName || ""}
|
{!data?.serverId && (
|
||||||
/>
|
<TabsContent value="monitoring">
|
||||||
</div>
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
</TabsContent>
|
<DockerMonitoring appName={data?.appName || ""} />
|
||||||
<TabsContent value="advanced">
|
</div>
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
</TabsContent>
|
||||||
<ShowAdvancedRedis redisId={redisId} />
|
)}
|
||||||
</div>
|
<TabsContent value="logs">
|
||||||
</TabsContent>
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
</Tabs>
|
<ShowDockerLogs
|
||||||
|
serverId={data?.serverId || ""}
|
||||||
|
appName={data?.appName || ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="advanced">
|
||||||
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
|
<ShowAdvancedRedis redisId={redisId} />
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import { ShowProjects } from "@/components/dashboard/projects/show";
|
import { ShowProjects } from "@/components/dashboard/projects/show";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
|
import { appRouter } from "@/server/api/root";
|
||||||
import { validateRequest } from "@dokploy/server";
|
import { validateRequest } from "@dokploy/server";
|
||||||
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import React, { type ReactElement } from "react";
|
import React, { type ReactElement } from "react";
|
||||||
|
import superjson from "superjson";
|
||||||
|
|
||||||
const Dashboard = () => {
|
const Dashboard = () => {
|
||||||
return <ShowProjects />;
|
return <ShowProjects />;
|
||||||
@@ -16,7 +19,22 @@ Dashboard.getLayout = (page: ReactElement) => {
|
|||||||
export async function getServerSideProps(
|
export async function getServerSideProps(
|
||||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||||
) {
|
) {
|
||||||
const { user } = await validateRequest(ctx.req, ctx.res);
|
const { req, res } = ctx;
|
||||||
|
const { user, session } = await validateRequest(req, res);
|
||||||
|
|
||||||
|
const helpers = createServerSideHelpers({
|
||||||
|
router: appRouter,
|
||||||
|
ctx: {
|
||||||
|
req: req as any,
|
||||||
|
res: res as any,
|
||||||
|
db: null as any,
|
||||||
|
session: session,
|
||||||
|
user: user,
|
||||||
|
},
|
||||||
|
transformer: superjson,
|
||||||
|
});
|
||||||
|
|
||||||
|
await helpers.settings.isCloud.prefetch();
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
@@ -27,6 +45,8 @@ export async function getServerSideProps(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
props: {},
|
props: {
|
||||||
|
trpcState: helpers.dehydrate(),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { AppearanceForm } from "@/components/dashboard/settings/appearance-form";
|
import { AppearanceForm } from "@/components/dashboard/settings/appearance-form";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||||
|
import { appRouter } from "@/server/api/root";
|
||||||
import { validateRequest } from "@dokploy/server";
|
import { validateRequest } from "@dokploy/server";
|
||||||
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import React, { type ReactElement } from "react";
|
import React, { type ReactElement } from "react";
|
||||||
|
import superjson from "superjson";
|
||||||
|
|
||||||
const Page = () => {
|
const Page = () => {
|
||||||
return (
|
return (
|
||||||
@@ -25,7 +28,23 @@ Page.getLayout = (page: ReactElement) => {
|
|||||||
export async function getServerSideProps(
|
export async function getServerSideProps(
|
||||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||||
) {
|
) {
|
||||||
const { user } = await validateRequest(ctx.req, ctx.res);
|
const { req, res } = ctx;
|
||||||
|
const { user, session } = await validateRequest(req, res);
|
||||||
|
|
||||||
|
const helpers = createServerSideHelpers({
|
||||||
|
router: appRouter,
|
||||||
|
ctx: {
|
||||||
|
req: req as any,
|
||||||
|
res: res as any,
|
||||||
|
db: null as any,
|
||||||
|
session: session,
|
||||||
|
user: user,
|
||||||
|
},
|
||||||
|
transformer: superjson,
|
||||||
|
});
|
||||||
|
|
||||||
|
await helpers.settings.isCloud.prefetch();
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -34,8 +53,9 @@ export async function getServerSideProps(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {},
|
props: {
|
||||||
|
trpcState: helpers.dehydrate(),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
65
apps/dokploy/pages/dashboard/settings/billing.tsx
Normal file
65
apps/dokploy/pages/dashboard/settings/billing.tsx
Normal file
@@ -0,0 +1,65 @@
|
|||||||
|
import { ShowBilling } from "@/components/dashboard/settings/billing/show-billing";
|
||||||
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
|
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||||
|
import { appRouter } from "@/server/api/root";
|
||||||
|
import { IS_CLOUD, validateRequest } from "@dokploy/server";
|
||||||
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
|
import type { GetServerSidePropsContext } from "next";
|
||||||
|
import React, { type ReactElement } from "react";
|
||||||
|
import superjson from "superjson";
|
||||||
|
|
||||||
|
const Page = () => {
|
||||||
|
return <ShowBilling />;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default Page;
|
||||||
|
|
||||||
|
Page.getLayout = (page: ReactElement) => {
|
||||||
|
return (
|
||||||
|
<DashboardLayout tab={"settings"}>
|
||||||
|
<SettingsLayout>{page}</SettingsLayout>
|
||||||
|
</DashboardLayout>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
export async function getServerSideProps(
|
||||||
|
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||||
|
) {
|
||||||
|
if (!IS_CLOUD) {
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
permanent: true,
|
||||||
|
destination: "/dashboard/projects",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const { req, res } = ctx;
|
||||||
|
const { user, session } = await validateRequest(req, res);
|
||||||
|
if (!user || user.rol === "user") {
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
permanent: true,
|
||||||
|
destination: "/",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const helpers = createServerSideHelpers({
|
||||||
|
router: appRouter,
|
||||||
|
ctx: {
|
||||||
|
req: req as any,
|
||||||
|
res: res as any,
|
||||||
|
db: null as any,
|
||||||
|
session: session,
|
||||||
|
user: user,
|
||||||
|
},
|
||||||
|
transformer: superjson,
|
||||||
|
});
|
||||||
|
|
||||||
|
await helpers.settings.isCloud.prefetch();
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
trpcState: helpers.dehydrate(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
import { ShowCertificates } from "@/components/dashboard/settings/certificates/show-certificates";
|
import { ShowCertificates } from "@/components/dashboard/settings/certificates/show-certificates";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||||
|
import { appRouter } from "@/server/api/root";
|
||||||
import { validateRequest } from "@dokploy/server";
|
import { validateRequest } from "@dokploy/server";
|
||||||
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import React, { type ReactElement } from "react";
|
import React, { type ReactElement } from "react";
|
||||||
|
import superjson from "superjson";
|
||||||
const Page = () => {
|
const Page = () => {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4 w-full">
|
<div className="flex flex-col gap-4 w-full">
|
||||||
@@ -25,7 +27,8 @@ Page.getLayout = (page: ReactElement) => {
|
|||||||
export async function getServerSideProps(
|
export async function getServerSideProps(
|
||||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||||
) {
|
) {
|
||||||
const { user, session } = await validateRequest(ctx.req, ctx.res);
|
const { req, res } = ctx;
|
||||||
|
const { user, session } = await validateRequest(req, res);
|
||||||
if (!user || user.rol === "user") {
|
if (!user || user.rol === "user") {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -35,7 +38,23 @@ export async function getServerSideProps(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const helpers = createServerSideHelpers({
|
||||||
|
router: appRouter,
|
||||||
|
ctx: {
|
||||||
|
req: req as any,
|
||||||
|
res: res as any,
|
||||||
|
db: null as any,
|
||||||
|
session: session,
|
||||||
|
user: user,
|
||||||
|
},
|
||||||
|
transformer: superjson,
|
||||||
|
});
|
||||||
|
|
||||||
|
await helpers.settings.isCloud.prefetch();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {},
|
props: {
|
||||||
|
trpcState: helpers.dehydrate(),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import { ShowNodes } from "@/components/dashboard/settings/cluster/nodes/show-nodes";
|
import { ShowNodes } from "@/components/dashboard/settings/cluster/nodes/show-nodes";
|
||||||
import { ShowRegistry } from "@/components/dashboard/settings/cluster/registry/show-registry";
|
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||||
import { IS_CLOUD, validateRequest } from "@dokploy/server";
|
import { IS_CLOUD, validateRequest } from "@dokploy/server";
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { ShowDestinations } from "@/components/dashboard/settings/destination/show-destinations";
|
import { ShowDestinations } from "@/components/dashboard/settings/destination/show-destinations";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||||
|
import { appRouter } from "@/server/api/root";
|
||||||
import { validateRequest } from "@dokploy/server";
|
import { validateRequest } from "@dokploy/server";
|
||||||
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import React, { type ReactElement } from "react";
|
import React, { type ReactElement } from "react";
|
||||||
|
import superjson from "superjson";
|
||||||
|
|
||||||
const Page = () => {
|
const Page = () => {
|
||||||
return (
|
return (
|
||||||
@@ -25,7 +28,8 @@ Page.getLayout = (page: ReactElement) => {
|
|||||||
export async function getServerSideProps(
|
export async function getServerSideProps(
|
||||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||||
) {
|
) {
|
||||||
const { user, session } = await validateRequest(ctx.req, ctx.res);
|
const { req, res } = ctx;
|
||||||
|
const { user, session } = await validateRequest(req, res);
|
||||||
if (!user || user.rol === "user") {
|
if (!user || user.rol === "user") {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -35,7 +39,23 @@ export async function getServerSideProps(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const helpers = createServerSideHelpers({
|
||||||
|
router: appRouter,
|
||||||
|
ctx: {
|
||||||
|
req: req as any,
|
||||||
|
res: res as any,
|
||||||
|
db: null as any,
|
||||||
|
session: session,
|
||||||
|
user: user,
|
||||||
|
},
|
||||||
|
transformer: superjson,
|
||||||
|
});
|
||||||
|
|
||||||
|
await helpers.settings.isCloud.prefetch();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {},
|
props: {
|
||||||
|
trpcState: helpers.dehydrate(),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,6 +52,7 @@ export async function getServerSideProps(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await helpers.project.all.prefetch();
|
await helpers.project.all.prefetch();
|
||||||
|
await helpers.settings.isCloud.prefetch();
|
||||||
const auth = await helpers.auth.get.fetch();
|
const auth = await helpers.auth.get.fetch();
|
||||||
|
|
||||||
if (auth.rol === "user") {
|
if (auth.rol === "user") {
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { ShowDestinations } from "@/components/dashboard/settings/destination/show-destinations";
|
|
||||||
import { ShowNotifications } from "@/components/dashboard/settings/notifications/show-notifications";
|
import { ShowNotifications } from "@/components/dashboard/settings/notifications/show-notifications";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||||
|
import { appRouter } from "@/server/api/root";
|
||||||
import { validateRequest } from "@dokploy/server";
|
import { validateRequest } from "@dokploy/server";
|
||||||
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import React, { type ReactElement } from "react";
|
import React, { type ReactElement } from "react";
|
||||||
|
import superjson from "superjson";
|
||||||
|
|
||||||
const Page = () => {
|
const Page = () => {
|
||||||
return (
|
return (
|
||||||
@@ -26,7 +28,8 @@ Page.getLayout = (page: ReactElement) => {
|
|||||||
export async function getServerSideProps(
|
export async function getServerSideProps(
|
||||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||||
) {
|
) {
|
||||||
const { user, session } = await validateRequest(ctx.req, ctx.res);
|
const { req, res } = ctx;
|
||||||
|
const { user, session } = await validateRequest(req, res);
|
||||||
if (!user || user.rol === "user") {
|
if (!user || user.rol === "user") {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -36,7 +39,23 @@ export async function getServerSideProps(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const helpers = createServerSideHelpers({
|
||||||
|
router: appRouter,
|
||||||
|
ctx: {
|
||||||
|
req: req as any,
|
||||||
|
res: res as any,
|
||||||
|
db: null as any,
|
||||||
|
session: session,
|
||||||
|
user: user,
|
||||||
|
},
|
||||||
|
transformer: superjson,
|
||||||
|
});
|
||||||
|
|
||||||
|
await helpers.settings.isCloud.prefetch();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {},
|
props: {
|
||||||
|
trpcState: helpers.dehydrate(),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,13 @@ import { GenerateToken } from "@/components/dashboard/settings/profile/generate-
|
|||||||
import { ProfileForm } from "@/components/dashboard/settings/profile/profile-form";
|
import { ProfileForm } from "@/components/dashboard/settings/profile/profile-form";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||||
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { validateRequest } from "@dokploy/server";
|
import { validateRequest } from "@dokploy/server";
|
||||||
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import React, { type ReactElement } from "react";
|
import React, { type ReactElement } from "react";
|
||||||
|
import superjson from "superjson";
|
||||||
|
|
||||||
const Page = () => {
|
const Page = () => {
|
||||||
const { data } = api.auth.get.useQuery();
|
const { data } = api.auth.get.useQuery();
|
||||||
@@ -37,7 +40,22 @@ Page.getLayout = (page: ReactElement) => {
|
|||||||
export async function getServerSideProps(
|
export async function getServerSideProps(
|
||||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||||
) {
|
) {
|
||||||
const { user } = await validateRequest(ctx.req, ctx.res);
|
const { req, res } = ctx;
|
||||||
|
const { user, session } = await validateRequest(req, res);
|
||||||
|
|
||||||
|
const helpers = createServerSideHelpers({
|
||||||
|
router: appRouter,
|
||||||
|
ctx: {
|
||||||
|
req: req as any,
|
||||||
|
res: res as any,
|
||||||
|
db: null as any,
|
||||||
|
session: session,
|
||||||
|
user: user,
|
||||||
|
},
|
||||||
|
transformer: superjson,
|
||||||
|
});
|
||||||
|
|
||||||
|
await helpers.settings.isCloud.prefetch();
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -48,6 +66,8 @@ export async function getServerSideProps(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {},
|
props: {
|
||||||
|
trpcState: helpers.dehydrate(),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { ShowRegistry } from "@/components/dashboard/settings/cluster/registry/show-registry";
|
import { ShowRegistry } from "@/components/dashboard/settings/cluster/registry/show-registry";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||||
|
import { appRouter } from "@/server/api/root";
|
||||||
import { validateRequest } from "@dokploy/server";
|
import { validateRequest } from "@dokploy/server";
|
||||||
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import React, { type ReactElement } from "react";
|
import React, { type ReactElement } from "react";
|
||||||
|
import superjson from "superjson";
|
||||||
|
|
||||||
const Page = () => {
|
const Page = () => {
|
||||||
return (
|
return (
|
||||||
@@ -25,7 +28,8 @@ Page.getLayout = (page: ReactElement) => {
|
|||||||
export async function getServerSideProps(
|
export async function getServerSideProps(
|
||||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||||
) {
|
) {
|
||||||
const { user, session } = await validateRequest(ctx.req, ctx.res);
|
const { req, res } = ctx;
|
||||||
|
const { user, session } = await validateRequest(req, res);
|
||||||
if (!user || user.rol === "user") {
|
if (!user || user.rol === "user") {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -34,8 +38,23 @@ export async function getServerSideProps(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
const helpers = createServerSideHelpers({
|
||||||
|
router: appRouter,
|
||||||
|
ctx: {
|
||||||
|
req: req as any,
|
||||||
|
res: res as any,
|
||||||
|
db: null as any,
|
||||||
|
session: session,
|
||||||
|
user: user,
|
||||||
|
},
|
||||||
|
transformer: superjson,
|
||||||
|
});
|
||||||
|
|
||||||
|
await helpers.settings.isCloud.prefetch();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {},
|
props: {
|
||||||
|
trpcState: helpers.dehydrate(),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { ShowServers } from "@/components/dashboard/settings/servers/show-servers";
|
import { ShowServers } from "@/components/dashboard/settings/servers/show-servers";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||||
|
import { appRouter } from "@/server/api/root";
|
||||||
import { validateRequest } from "@dokploy/server";
|
import { validateRequest } from "@dokploy/server";
|
||||||
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import React, { type ReactElement } from "react";
|
import React, { type ReactElement } from "react";
|
||||||
|
import superjson from "superjson";
|
||||||
|
|
||||||
const Page = () => {
|
const Page = () => {
|
||||||
return (
|
return (
|
||||||
@@ -25,7 +28,8 @@ Page.getLayout = (page: ReactElement) => {
|
|||||||
export async function getServerSideProps(
|
export async function getServerSideProps(
|
||||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||||
) {
|
) {
|
||||||
const { user } = await validateRequest(ctx.req, ctx.res);
|
const { req, res } = ctx;
|
||||||
|
const { user, session } = await validateRequest(req, res);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -43,7 +47,23 @@ export async function getServerSideProps(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const helpers = createServerSideHelpers({
|
||||||
|
router: appRouter,
|
||||||
|
ctx: {
|
||||||
|
req: req as any,
|
||||||
|
res: res as any,
|
||||||
|
db: null as any,
|
||||||
|
session: session,
|
||||||
|
user: user,
|
||||||
|
},
|
||||||
|
transformer: superjson,
|
||||||
|
});
|
||||||
|
|
||||||
|
await helpers.settings.isCloud.prefetch();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {},
|
props: {
|
||||||
|
trpcState: helpers.dehydrate(),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -52,7 +52,9 @@ export async function getServerSideProps(
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
await helpers.project.all.prefetch();
|
await helpers.project.all.prefetch();
|
||||||
|
|
||||||
const auth = await helpers.auth.get.fetch();
|
const auth = await helpers.auth.get.fetch();
|
||||||
|
await helpers.settings.isCloud.prefetch();
|
||||||
|
|
||||||
if (auth.rol === "user") {
|
if (auth.rol === "user") {
|
||||||
const user = await helpers.user.byAuthId.fetch({
|
const user = await helpers.user.byAuthId.fetch({
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { ShowUsers } from "@/components/dashboard/settings/users/show-users";
|
import { ShowUsers } from "@/components/dashboard/settings/users/show-users";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||||
|
import { appRouter } from "@/server/api/root";
|
||||||
import { validateRequest } from "@dokploy/server";
|
import { validateRequest } from "@dokploy/server";
|
||||||
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import React, { type ReactElement } from "react";
|
import React, { type ReactElement } from "react";
|
||||||
|
import superjson from "superjson";
|
||||||
|
|
||||||
const Page = () => {
|
const Page = () => {
|
||||||
return (
|
return (
|
||||||
@@ -25,7 +28,8 @@ Page.getLayout = (page: ReactElement) => {
|
|||||||
export async function getServerSideProps(
|
export async function getServerSideProps(
|
||||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||||
) {
|
) {
|
||||||
const { user } = await validateRequest(ctx.req, ctx.res);
|
const { req, res } = ctx;
|
||||||
|
const { user, session } = await validateRequest(req, res);
|
||||||
if (!user || user.rol === "user") {
|
if (!user || user.rol === "user") {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -35,7 +39,23 @@ export async function getServerSideProps(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const helpers = createServerSideHelpers({
|
||||||
|
router: appRouter,
|
||||||
|
ctx: {
|
||||||
|
req: req as any,
|
||||||
|
res: res as any,
|
||||||
|
db: null as any,
|
||||||
|
session: session,
|
||||||
|
user: user,
|
||||||
|
},
|
||||||
|
transformer: superjson,
|
||||||
|
});
|
||||||
|
|
||||||
|
await helpers.settings.isCloud.prefetch();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {},
|
props: {
|
||||||
|
trpcState: helpers.dehydrate(),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -55,7 +55,10 @@ type AuthResponse = {
|
|||||||
authId: string;
|
authId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Home() {
|
interface Props {
|
||||||
|
IS_CLOUD: boolean;
|
||||||
|
}
|
||||||
|
export default function Home({ IS_CLOUD }: Props) {
|
||||||
const [temp, setTemp] = useState<AuthResponse>({
|
const [temp, setTemp] = useState<AuthResponse>({
|
||||||
is2FAEnabled: false,
|
is2FAEnabled: false,
|
||||||
authId: "",
|
authId: "",
|
||||||
@@ -176,13 +179,22 @@ export default function Home() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4 text-sm flex flex-row justify-center gap-2">
|
<div className="mt-4 text-sm flex flex-row justify-center gap-2">
|
||||||
<Link
|
{IS_CLOUD ? (
|
||||||
className="hover:underline text-muted-foreground"
|
<Link
|
||||||
href="https://docs.dokploy.com/docs/core/get-started/reset-password"
|
className="hover:underline text-muted-foreground"
|
||||||
target="_blank"
|
href="/send-reset-password"
|
||||||
>
|
>
|
||||||
Lost your password?
|
Lost your password?
|
||||||
</Link>
|
</Link>
|
||||||
|
) : (
|
||||||
|
<Link
|
||||||
|
className="hover:underline text-muted-foreground"
|
||||||
|
href="https://docs.dokploy.com/docs/core/get-started/reset-password"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
Lost your password?
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-2" />
|
<div className="p-2" />
|
||||||
@@ -212,7 +224,9 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
} catch (error) {}
|
} catch (error) {}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {},
|
props: {
|
||||||
|
IS_CLOUD: IS_CLOUD,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const hasAdmin = await isAdminPresent();
|
const hasAdmin = await isAdminPresent();
|
||||||
|
|||||||
228
apps/dokploy/pages/reset-password.tsx
Normal file
228
apps/dokploy/pages/reset-password.tsx
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
import { OnboardingLayout } from "@/components/layouts/onboarding-layout";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { Logo } from "@/components/shared/logo";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { auth } from "@/server/db/schema";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { IS_CLOUD } from "@dokploy/server";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { isBefore } from "date-fns";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import type { GetServerSidePropsContext } from "next";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { type ReactElement, useEffect } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const loginSchema = z
|
||||||
|
.object({
|
||||||
|
password: z
|
||||||
|
.string()
|
||||||
|
.min(1, {
|
||||||
|
message: "Password is required",
|
||||||
|
})
|
||||||
|
.min(8, {
|
||||||
|
message: "Password must be at least 8 characters",
|
||||||
|
}),
|
||||||
|
confirmPassword: z
|
||||||
|
.string()
|
||||||
|
.min(1, {
|
||||||
|
message: "Password is required",
|
||||||
|
})
|
||||||
|
.min(8, {
|
||||||
|
message: "Password must be at least 8 characters",
|
||||||
|
}),
|
||||||
|
})
|
||||||
|
.refine((data) => data.password === data.confirmPassword, {
|
||||||
|
message: "Passwords do not match",
|
||||||
|
path: ["confirmPassword"],
|
||||||
|
});
|
||||||
|
|
||||||
|
type Login = z.infer<typeof loginSchema>;
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
token: string;
|
||||||
|
}
|
||||||
|
export default function Home({ token }: Props) {
|
||||||
|
const { mutateAsync, isLoading, isError, error } =
|
||||||
|
api.auth.resetPassword.useMutation();
|
||||||
|
const router = useRouter();
|
||||||
|
const form = useForm<Login>({
|
||||||
|
defaultValues: {
|
||||||
|
password: "",
|
||||||
|
confirmPassword: "",
|
||||||
|
},
|
||||||
|
resolver: zodResolver(loginSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
form.reset();
|
||||||
|
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
||||||
|
|
||||||
|
const onSubmit = async (values: Login) => {
|
||||||
|
await mutateAsync({
|
||||||
|
resetPasswordToken: token,
|
||||||
|
password: values.password,
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
toast.success("Password reset succesfully", {
|
||||||
|
duration: 2000,
|
||||||
|
});
|
||||||
|
router.push("/");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to reset password", {
|
||||||
|
duration: 2000,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className="flex h-screen w-full items-center justify-center ">
|
||||||
|
<div className="flex flex-col items-center gap-4 w-full">
|
||||||
|
<Link href="/" className="flex flex-row items-center gap-2">
|
||||||
|
<Logo />
|
||||||
|
<span className="font-medium text-sm">Dokploy</span>
|
||||||
|
</Link>
|
||||||
|
<CardTitle className="text-2xl font-bold">Reset Password</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Enter your email to reset your password
|
||||||
|
</CardDescription>
|
||||||
|
|
||||||
|
<Card className="mx-auto w-full max-w-lg bg-transparent ">
|
||||||
|
<div className="p-3.5" />
|
||||||
|
<CardContent>
|
||||||
|
{isError && (
|
||||||
|
<AlertBlock type="error" className="my-2">
|
||||||
|
{error?.message}
|
||||||
|
</AlertBlock>
|
||||||
|
)}
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="grid gap-4"
|
||||||
|
>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="password"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Password</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholder="Password"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="confirmPassword"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Confirm Password</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input
|
||||||
|
type="password"
|
||||||
|
placeholder="Password"
|
||||||
|
{...field}
|
||||||
|
/>
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
isLoading={isLoading}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
Confirm
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Home.getLayout = (page: ReactElement) => {
|
||||||
|
return <OnboardingLayout>{page}</OnboardingLayout>;
|
||||||
|
};
|
||||||
|
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||||
|
if (!IS_CLOUD) {
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
permanent: true,
|
||||||
|
destination: "/",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const { token } = context.query;
|
||||||
|
|
||||||
|
if (typeof token !== "string") {
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
permanent: true,
|
||||||
|
destination: "/",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const authR = await db?.query.auth.findFirst({
|
||||||
|
where: eq(auth.resetPasswordToken, token),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!authR || authR?.resetPasswordExpiresAt === null) {
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
permanent: true,
|
||||||
|
destination: "/",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
const isExpired = isBefore(
|
||||||
|
new Date(authR.resetPasswordExpiresAt),
|
||||||
|
new Date(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isExpired) {
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
permanent: true,
|
||||||
|
destination: "/",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
token: authR.resetPasswordToken,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
172
apps/dokploy/pages/send-reset-password.tsx
Normal file
172
apps/dokploy/pages/send-reset-password.tsx
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
import { Login2FA } from "@/components/auth/login-2fa";
|
||||||
|
import { OnboardingLayout } from "@/components/layouts/onboarding-layout";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { Logo } from "@/components/shared/logo";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { IS_CLOUD } from "@dokploy/server";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import type { GetServerSidePropsContext } from "next";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { type ReactElement, useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const loginSchema = z.object({
|
||||||
|
email: z
|
||||||
|
.string()
|
||||||
|
.min(1, {
|
||||||
|
message: "Email is required",
|
||||||
|
})
|
||||||
|
.email({
|
||||||
|
message: "Email must be a valid email",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
type Login = z.infer<typeof loginSchema>;
|
||||||
|
|
||||||
|
type AuthResponse = {
|
||||||
|
is2FAEnabled: boolean;
|
||||||
|
authId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
const [temp, setTemp] = useState<AuthResponse>({
|
||||||
|
is2FAEnabled: false,
|
||||||
|
authId: "",
|
||||||
|
});
|
||||||
|
const { mutateAsync, isLoading, isError, error } =
|
||||||
|
api.auth.sendResetPasswordEmail.useMutation();
|
||||||
|
const router = useRouter();
|
||||||
|
const form = useForm<Login>({
|
||||||
|
defaultValues: {
|
||||||
|
email: "",
|
||||||
|
},
|
||||||
|
resolver: zodResolver(loginSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
form.reset();
|
||||||
|
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
||||||
|
|
||||||
|
const onSubmit = async (values: Login) => {
|
||||||
|
await mutateAsync({
|
||||||
|
email: values.email,
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
toast.success("Email sent", {
|
||||||
|
duration: 2000,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to send email", {
|
||||||
|
duration: 2000,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className="flex h-screen w-full items-center justify-center ">
|
||||||
|
<div className="flex flex-col items-center gap-4 w-full">
|
||||||
|
<Link href="/" className="flex flex-row items-center gap-2">
|
||||||
|
<Logo />
|
||||||
|
<span className="font-medium text-sm">Dokploy</span>
|
||||||
|
</Link>
|
||||||
|
<CardTitle className="text-2xl font-bold">Reset Password</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Enter your email to reset your password
|
||||||
|
</CardDescription>
|
||||||
|
|
||||||
|
<Card className="mx-auto w-full max-w-lg bg-transparent ">
|
||||||
|
<div className="p-3.5" />
|
||||||
|
<CardContent>
|
||||||
|
{isError && (
|
||||||
|
<AlertBlock type="error" className="my-2">
|
||||||
|
{error?.message}
|
||||||
|
</AlertBlock>
|
||||||
|
)}
|
||||||
|
{!temp.is2FAEnabled ? (
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="grid gap-4"
|
||||||
|
>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="email"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Email</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Email" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
isLoading={isLoading}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
Send Reset Link
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
) : (
|
||||||
|
<Login2FA authId={temp.authId} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex flex-row justify-between flex-wrap">
|
||||||
|
<div className="mt-4 text-center text-sm flex flex-row justify-center gap-2">
|
||||||
|
<Link
|
||||||
|
className="hover:underline text-muted-foreground"
|
||||||
|
href="/"
|
||||||
|
>
|
||||||
|
Login
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Home.getLayout = (page: ReactElement) => {
|
||||||
|
return <OnboardingLayout>{page}</OnboardingLayout>;
|
||||||
|
};
|
||||||
|
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||||
|
if (!IS_CLOUD) {
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
permanent: true,
|
||||||
|
destination: "/",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -29,6 +29,7 @@ import { securityRouter } from "./routers/security";
|
|||||||
import { serverRouter } from "./routers/server";
|
import { serverRouter } from "./routers/server";
|
||||||
import { settingsRouter } from "./routers/settings";
|
import { settingsRouter } from "./routers/settings";
|
||||||
import { sshRouter } from "./routers/ssh-key";
|
import { sshRouter } from "./routers/ssh-key";
|
||||||
|
import { stripeRouter } from "./routers/stripe";
|
||||||
import { userRouter } from "./routers/user";
|
import { userRouter } from "./routers/user";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -69,6 +70,7 @@ export const appRouter = createTRPCRouter({
|
|||||||
gitlab: gitlabRouter,
|
gitlab: gitlabRouter,
|
||||||
github: githubRouter,
|
github: githubRouter,
|
||||||
server: serverRouter,
|
server: serverRouter,
|
||||||
|
stripe: stripeRouter,
|
||||||
});
|
});
|
||||||
|
|
||||||
// export type definition of API
|
// export type definition of API
|
||||||
|
|||||||
@@ -19,11 +19,8 @@ import {
|
|||||||
apiUpdateApplication,
|
apiUpdateApplication,
|
||||||
applications,
|
applications,
|
||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
import {
|
import type { DeploymentJob } from "@/server/queues/queue-types";
|
||||||
type DeploymentJob,
|
import { cleanQueuesByApplication, myQueue } from "@/server/queues/queueSetup";
|
||||||
cleanQueuesByApplication,
|
|
||||||
} from "@/server/queues/deployments-queue";
|
|
||||||
import { myQueue } from "@/server/queues/queueSetup";
|
|
||||||
import { deploy } from "@/server/utils/deploy";
|
import { deploy } from "@/server/utils/deploy";
|
||||||
import { uploadFileSchema } from "@/utils/schema";
|
import { uploadFileSchema } from "@/utils/schema";
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
apiUpdateAuthByAdmin,
|
apiUpdateAuthByAdmin,
|
||||||
apiVerify2FA,
|
apiVerify2FA,
|
||||||
apiVerifyLogin2FA,
|
apiVerifyLogin2FA,
|
||||||
|
auth,
|
||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
import {
|
import {
|
||||||
IS_CLOUD,
|
IS_CLOUD,
|
||||||
@@ -18,12 +19,17 @@ import {
|
|||||||
getUserByToken,
|
getUserByToken,
|
||||||
lucia,
|
lucia,
|
||||||
luciaToken,
|
luciaToken,
|
||||||
|
sendEmailNotification,
|
||||||
updateAuthById,
|
updateAuthById,
|
||||||
validateRequest,
|
validateRequest,
|
||||||
verify2FA,
|
verify2FA,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import * as bcrypt from "bcrypt";
|
import * as bcrypt from "bcrypt";
|
||||||
|
import { isBefore } from "date-fns";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
import { z } from "zod";
|
||||||
import { db } from "../../db";
|
import { db } from "../../db";
|
||||||
import {
|
import {
|
||||||
adminProcedure,
|
adminProcedure,
|
||||||
@@ -233,4 +239,101 @@ export const authRouter = createTRPCRouter({
|
|||||||
verifyToken: protectedProcedure.mutation(async () => {
|
verifyToken: protectedProcedure.mutation(async () => {
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
|
sendResetPasswordEmail: publicProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
email: z.string().min(1).email(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
if (!IS_CLOUD) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: "This feature is only available in the cloud version",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const authR = await db.query.auth.findFirst({
|
||||||
|
where: eq(auth.email, input.email),
|
||||||
|
});
|
||||||
|
if (!authR) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: "User not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const token = nanoid();
|
||||||
|
await updateAuthById(authR.id, {
|
||||||
|
resetPasswordToken: token,
|
||||||
|
// Make resetPassword in 24 hours
|
||||||
|
resetPasswordExpiresAt: new Date(
|
||||||
|
new Date().getTime() + 24 * 60 * 60 * 1000,
|
||||||
|
).toISOString(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const email = await sendEmailNotification(
|
||||||
|
{
|
||||||
|
fromAddress: process.env.SMTP_FROM_ADDRESS || "",
|
||||||
|
toAddresses: [authR.email],
|
||||||
|
smtpServer: process.env.SMTP_SERVER || "",
|
||||||
|
smtpPort: Number(process.env.SMTP_PORT),
|
||||||
|
username: process.env.SMTP_USERNAME || "",
|
||||||
|
password: process.env.SMTP_PASSWORD || "",
|
||||||
|
},
|
||||||
|
"Reset Password",
|
||||||
|
`
|
||||||
|
Reset your password by clicking the link below:
|
||||||
|
The link will expire in 24 hours.
|
||||||
|
<a href="http://localhost:3000/reset-password?token=${token}">
|
||||||
|
Reset Password
|
||||||
|
</a>
|
||||||
|
|
||||||
|
`,
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
|
||||||
|
resetPassword: publicProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
resetPasswordToken: z.string().min(1),
|
||||||
|
password: z.string().min(1),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
if (!IS_CLOUD) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: "This feature is only available in the cloud version",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const authR = await db.query.auth.findFirst({
|
||||||
|
where: eq(auth.resetPasswordToken, input.resetPasswordToken),
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!authR || authR.resetPasswordExpiresAt === null) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: "Token not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const isExpired = isBefore(
|
||||||
|
new Date(authR.resetPasswordExpiresAt),
|
||||||
|
new Date(),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isExpired) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: "Token expired",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await updateAuthById(authR.id, {
|
||||||
|
resetPasswordExpiresAt: null,
|
||||||
|
resetPasswordToken: null,
|
||||||
|
password: bcrypt.hashSync(input.password, 10),
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}),
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
findMongoByBackupId,
|
findMongoByBackupId,
|
||||||
findMySqlByBackupId,
|
findMySqlByBackupId,
|
||||||
findPostgresByBackupId,
|
findPostgresByBackupId,
|
||||||
|
findServerById,
|
||||||
removeBackupById,
|
removeBackupById,
|
||||||
removeScheduleBackup,
|
removeScheduleBackup,
|
||||||
runMariadbBackup,
|
runMariadbBackup,
|
||||||
@@ -36,6 +37,25 @@ export const backupRouter = createTRPCRouter({
|
|||||||
const backup = await findBackupById(newBackup.backupId);
|
const backup = await findBackupById(newBackup.backupId);
|
||||||
|
|
||||||
if (IS_CLOUD && backup.enabled) {
|
if (IS_CLOUD && backup.enabled) {
|
||||||
|
const databaseType = backup.databaseType;
|
||||||
|
let serverId = "";
|
||||||
|
if (databaseType === "postgres" && backup.postgres?.serverId) {
|
||||||
|
serverId = backup.postgres.serverId;
|
||||||
|
} else if (databaseType === "mysql" && backup.mysql?.serverId) {
|
||||||
|
serverId = backup.mysql.serverId;
|
||||||
|
} else if (databaseType === "mongo" && backup.mongo?.serverId) {
|
||||||
|
serverId = backup.mongo.serverId;
|
||||||
|
} else if (databaseType === "mariadb" && backup.mariadb?.serverId) {
|
||||||
|
serverId = backup.mariadb.serverId;
|
||||||
|
}
|
||||||
|
const server = await findServerById(serverId);
|
||||||
|
|
||||||
|
if (server.serverStatus === "inactive") {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: "Server is inactive",
|
||||||
|
});
|
||||||
|
}
|
||||||
await schedule({
|
await schedule({
|
||||||
cronSchedule: backup.schedule,
|
cronSchedule: backup.schedule,
|
||||||
backupId: backup.backupId,
|
backupId: backup.backupId,
|
||||||
|
|||||||
@@ -9,11 +9,7 @@ import {
|
|||||||
apiUpdateCompose,
|
apiUpdateCompose,
|
||||||
compose,
|
compose,
|
||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
import {
|
import { cleanQueuesByCompose, myQueue } from "@/server/queues/queueSetup";
|
||||||
type DeploymentJob,
|
|
||||||
cleanQueuesByCompose,
|
|
||||||
} from "@/server/queues/deployments-queue";
|
|
||||||
import { myQueue } from "@/server/queues/queueSetup";
|
|
||||||
import { templates } from "@/templates/templates";
|
import { templates } from "@/templates/templates";
|
||||||
import type { TemplatesKeys } from "@/templates/types/templates-data.type";
|
import type { TemplatesKeys } from "@/templates/types/templates-data.type";
|
||||||
import {
|
import {
|
||||||
@@ -28,6 +24,7 @@ import _ from "lodash";
|
|||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||||
|
|
||||||
|
import type { DeploymentJob } from "@/server/queues/queue-types";
|
||||||
import { deploy } from "@/server/utils/deploy";
|
import { deploy } from "@/server/utils/deploy";
|
||||||
import {
|
import {
|
||||||
IS_CLOUD,
|
IS_CLOUD,
|
||||||
@@ -41,7 +38,6 @@ import {
|
|||||||
createComposeByTemplate,
|
createComposeByTemplate,
|
||||||
createDomain,
|
createDomain,
|
||||||
createMount,
|
createMount,
|
||||||
findAdmin,
|
|
||||||
findAdminById,
|
findAdminById,
|
||||||
findComposeById,
|
findComposeById,
|
||||||
findDomainsByComposeId,
|
findDomainsByComposeId,
|
||||||
@@ -252,7 +248,6 @@ export const composeRouter = createTRPCRouter({
|
|||||||
descriptionLog: "",
|
descriptionLog: "",
|
||||||
server: !!compose.serverId,
|
server: !!compose.serverId,
|
||||||
};
|
};
|
||||||
console.log(jobData);
|
|
||||||
|
|
||||||
if (IS_CLOUD && compose.serverId) {
|
if (IS_CLOUD && compose.serverId) {
|
||||||
jobData.serverId = compose.serverId;
|
jobData.serverId = compose.serverId;
|
||||||
|
|||||||
@@ -12,9 +12,10 @@ import {
|
|||||||
destinations,
|
destinations,
|
||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
import {
|
import {
|
||||||
|
IS_CLOUD,
|
||||||
createDestintation,
|
createDestintation,
|
||||||
execAsync,
|
execAsync,
|
||||||
findAdmin,
|
execAsyncRemote,
|
||||||
findDestinationById,
|
findDestinationById,
|
||||||
removeDestinationById,
|
removeDestinationById,
|
||||||
updateDestinationById,
|
updateDestinationById,
|
||||||
@@ -53,11 +54,26 @@ export const destinationRouter = createTRPCRouter({
|
|||||||
];
|
];
|
||||||
const rcloneDestination = `:s3:${bucket}`;
|
const rcloneDestination = `:s3:${bucket}`;
|
||||||
const rcloneCommand = `rclone ls ${rcloneFlags.join(" ")} "${rcloneDestination}"`;
|
const rcloneCommand = `rclone ls ${rcloneFlags.join(" ")} "${rcloneDestination}"`;
|
||||||
await execAsync(rcloneCommand);
|
|
||||||
|
if (IS_CLOUD && !input.serverId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: "Server not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
await execAsyncRemote(input.serverId || "", rcloneCommand);
|
||||||
|
} else {
|
||||||
|
await execAsync(rcloneCommand);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
message: "Error to connect to bucket",
|
message:
|
||||||
|
error instanceof Error
|
||||||
|
? error?.message
|
||||||
|
: "Error to connect to bucket",
|
||||||
cause: error,
|
cause: error,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
deployMariadb,
|
deployMariadb,
|
||||||
findMariadbById,
|
findMariadbById,
|
||||||
findProjectById,
|
findProjectById,
|
||||||
|
findServerById,
|
||||||
removeMariadbById,
|
removeMariadbById,
|
||||||
removeService,
|
removeService,
|
||||||
startService,
|
startService,
|
||||||
@@ -151,6 +152,7 @@ export const mariadbRouter = createTRPCRouter({
|
|||||||
message: "You are not authorized to deploy this mariadb",
|
message: "You are not authorized to deploy this mariadb",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return deployMariadb(input.mariadbId);
|
return deployMariadb(input.mariadbId);
|
||||||
}),
|
}),
|
||||||
changeStatus: protectedProcedure
|
changeStatus: protectedProcedure
|
||||||
|
|||||||
@@ -148,12 +148,6 @@ export const notificationRouter = createTRPCRouter({
|
|||||||
.input(apiCreateDiscord)
|
.input(apiCreateDiscord)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
// go to your discord server
|
|
||||||
// go to settings
|
|
||||||
// go to integrations
|
|
||||||
// add a new integration
|
|
||||||
// select webhook
|
|
||||||
// copy the webhook url
|
|
||||||
return await createDiscordNotification(input, ctx.user.adminId);
|
return await createDiscordNotification(input, ctx.user.adminId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
apiCreateRegistry,
|
apiCreateRegistry,
|
||||||
apiEnableSelfHostedRegistry,
|
|
||||||
apiFindOneRegistry,
|
apiFindOneRegistry,
|
||||||
apiRemoveRegistry,
|
apiRemoveRegistry,
|
||||||
apiTestRegistry,
|
apiTestRegistry,
|
||||||
@@ -13,8 +12,6 @@ import {
|
|||||||
execAsyncRemote,
|
execAsyncRemote,
|
||||||
findAllRegistryByAdminId,
|
findAllRegistryByAdminId,
|
||||||
findRegistryById,
|
findRegistryById,
|
||||||
initializeRegistry,
|
|
||||||
manageRegistry,
|
|
||||||
removeRegistry,
|
removeRegistry,
|
||||||
updateRegistry,
|
updateRegistry,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
@@ -84,6 +81,13 @@ export const registryRouter = createTRPCRouter({
|
|||||||
try {
|
try {
|
||||||
const loginCommand = `echo ${input.password} | docker login ${input.registryUrl} --username ${input.username} --password-stdin`;
|
const loginCommand = `echo ${input.password} | docker login ${input.registryUrl} --username ${input.username} --password-stdin`;
|
||||||
|
|
||||||
|
if (IS_CLOUD && !input.serverId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: "Select a server to test the registry",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (input.serverId && input.serverId !== "none") {
|
if (input.serverId && input.serverId !== "none") {
|
||||||
await execAsyncRemote(input.serverId, loginCommand);
|
await execAsyncRemote(input.serverId, loginCommand);
|
||||||
} else {
|
} else {
|
||||||
@@ -96,34 +100,4 @@ export const registryRouter = createTRPCRouter({
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
enableSelfHostedRegistry: adminProcedure
|
|
||||||
.input(apiEnableSelfHostedRegistry)
|
|
||||||
.mutation(async ({ input, ctx }) => {
|
|
||||||
if (IS_CLOUD) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "Self Hosted Registry is not available in the cloud version",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const selfHostedRegistry = await createRegistry(
|
|
||||||
{
|
|
||||||
...input,
|
|
||||||
registryName: "Self Hosted Registry",
|
|
||||||
registryType: "selfHosted",
|
|
||||||
registryUrl:
|
|
||||||
process.env.NODE_ENV === "production"
|
|
||||||
? input.registryUrl
|
|
||||||
: "dokploy-registry.docker.localhost",
|
|
||||||
imagePrefix: null,
|
|
||||||
serverId: undefined,
|
|
||||||
},
|
|
||||||
ctx.user.adminId,
|
|
||||||
);
|
|
||||||
|
|
||||||
await manageRegistry(selfHostedRegistry);
|
|
||||||
await initializeRegistry(input.username, input.password);
|
|
||||||
|
|
||||||
return selfHostedRegistry;
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { updateServersBasedOnQuantity } from "@/pages/api/stripe/webhook";
|
||||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||||
import { db } from "@/server/db";
|
import { db } from "@/server/db";
|
||||||
import {
|
import {
|
||||||
@@ -15,15 +16,17 @@ import {
|
|||||||
server,
|
server,
|
||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
import {
|
import {
|
||||||
|
IS_CLOUD,
|
||||||
createServer,
|
createServer,
|
||||||
deleteServer,
|
deleteServer,
|
||||||
|
findAdminById,
|
||||||
findServerById,
|
findServerById,
|
||||||
|
findServersByAdminId,
|
||||||
haveActiveServices,
|
haveActiveServices,
|
||||||
removeDeploymentsByServerId,
|
removeDeploymentsByServerId,
|
||||||
serverSetup,
|
serverSetup,
|
||||||
updateServerById,
|
updateServerById,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
// import { serverSetup } from "@/server/setup/server-setup";
|
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { and, desc, eq, getTableColumns, isNotNull, sql } from "drizzle-orm";
|
import { and, desc, eq, getTableColumns, isNotNull, sql } from "drizzle-orm";
|
||||||
|
|
||||||
@@ -32,6 +35,14 @@ export const serverRouter = createTRPCRouter({
|
|||||||
.input(apiCreateServer)
|
.input(apiCreateServer)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
try {
|
try {
|
||||||
|
const admin = await findAdminById(ctx.user.adminId);
|
||||||
|
const servers = await findServersByAdminId(admin.adminId);
|
||||||
|
if (IS_CLOUD && servers.length >= admin.serversQuantity) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "You cannot create more servers",
|
||||||
|
});
|
||||||
|
}
|
||||||
const project = await createServer(input, ctx.user.adminId);
|
const project = await createServer(input, ctx.user.adminId);
|
||||||
return project;
|
return project;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -77,13 +88,17 @@ export const serverRouter = createTRPCRouter({
|
|||||||
return result;
|
return result;
|
||||||
}),
|
}),
|
||||||
withSSHKey: protectedProcedure.query(async ({ ctx }) => {
|
withSSHKey: protectedProcedure.query(async ({ ctx }) => {
|
||||||
return await db.query.server.findMany({
|
const result = await db.query.server.findMany({
|
||||||
orderBy: desc(server.createdAt),
|
orderBy: desc(server.createdAt),
|
||||||
where: and(
|
where: IS_CLOUD
|
||||||
isNotNull(server.sshKeyId),
|
? and(
|
||||||
eq(server.adminId, ctx.user.adminId),
|
isNotNull(server.sshKeyId),
|
||||||
),
|
eq(server.adminId, ctx.user.adminId),
|
||||||
|
eq(server.serverStatus, "active"),
|
||||||
|
)
|
||||||
|
: and(isNotNull(server.sshKeyId), eq(server.adminId, ctx.user.adminId)),
|
||||||
});
|
});
|
||||||
|
return result;
|
||||||
}),
|
}),
|
||||||
setup: protectedProcedure
|
setup: protectedProcedure
|
||||||
.input(apiFindOneServer)
|
.input(apiFindOneServer)
|
||||||
@@ -125,6 +140,15 @@ export const serverRouter = createTRPCRouter({
|
|||||||
await removeDeploymentsByServerId(currentServer);
|
await removeDeploymentsByServerId(currentServer);
|
||||||
await deleteServer(input.serverId);
|
await deleteServer(input.serverId);
|
||||||
|
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
const admin = await findAdminById(ctx.user.adminId);
|
||||||
|
|
||||||
|
await updateServersBasedOnQuantity(
|
||||||
|
admin.adminId,
|
||||||
|
admin.serversQuantity,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return currentServer;
|
return currentServer;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
@@ -141,6 +165,13 @@ export const serverRouter = createTRPCRouter({
|
|||||||
message: "You are not authorized to update this server",
|
message: "You are not authorized to update this server",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (server.serverStatus === "inactive") {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: "Server is inactive",
|
||||||
|
});
|
||||||
|
}
|
||||||
const currentServer = await updateServerById(input.serverId, {
|
const currentServer = await updateServerById(input.serverId, {
|
||||||
...input,
|
...input,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -221,6 +221,13 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (server.enableDockerCleanup) {
|
if (server.enableDockerCleanup) {
|
||||||
|
const server = await findServerById(input.serverId);
|
||||||
|
if (server.serverStatus === "inactive") {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: "Server is inactive",
|
||||||
|
});
|
||||||
|
}
|
||||||
if (IS_CLOUD) {
|
if (IS_CLOUD) {
|
||||||
await schedule({
|
await schedule({
|
||||||
cronSchedule: "0 0 * * *",
|
cronSchedule: "0 0 * * *",
|
||||||
@@ -503,7 +510,7 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
if (input?.serverId) {
|
if (input?.serverId) {
|
||||||
const result = await execAsyncRemote(input.serverId, command);
|
const result = await execAsyncRemote(input.serverId, command);
|
||||||
stdout = result.stdout;
|
stdout = result.stdout;
|
||||||
} else {
|
} else if (!IS_CLOUD) {
|
||||||
const result = await execAsync(
|
const result = await execAsync(
|
||||||
"docker service inspect --format='{{json .Endpoint.Ports}}' dokploy-traefik",
|
"docker service inspect --format='{{json .Endpoint.Ports}}' dokploy-traefik",
|
||||||
);
|
);
|
||||||
@@ -635,7 +642,7 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
isCloud: adminProcedure.query(async () => {
|
isCloud: protectedProcedure.query(async () => {
|
||||||
return IS_CLOUD;
|
return IS_CLOUD;
|
||||||
}),
|
}),
|
||||||
health: publicProcedure.query(async () => {
|
health: publicProcedure.query(async () => {
|
||||||
|
|||||||
130
apps/dokploy/server/api/routers/stripe.ts
Normal file
130
apps/dokploy/server/api/routers/stripe.ts
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import { WEBSITE_URL, getStripeItems } from "@/server/utils/stripe";
|
||||||
|
import {
|
||||||
|
IS_CLOUD,
|
||||||
|
findAdminById,
|
||||||
|
findServersByAdminId,
|
||||||
|
updateAdmin,
|
||||||
|
} from "@dokploy/server";
|
||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import Stripe from "stripe";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { adminProcedure, createTRPCRouter } from "../trpc";
|
||||||
|
|
||||||
|
export const stripeRouter = createTRPCRouter({
|
||||||
|
getProducts: adminProcedure.query(async ({ ctx }) => {
|
||||||
|
const admin = await findAdminById(ctx.user.adminId);
|
||||||
|
const stripeCustomerId = admin.stripeCustomerId;
|
||||||
|
|
||||||
|
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || "", {
|
||||||
|
apiVersion: "2024-09-30.acacia",
|
||||||
|
});
|
||||||
|
|
||||||
|
const products = await stripe.products.list({
|
||||||
|
expand: ["data.default_price"],
|
||||||
|
active: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!stripeCustomerId) {
|
||||||
|
return {
|
||||||
|
products: products.data,
|
||||||
|
subscriptions: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const subscriptions = await stripe.subscriptions.list({
|
||||||
|
customer: stripeCustomerId,
|
||||||
|
status: "active",
|
||||||
|
expand: ["data.items.data.price"],
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
products: products.data,
|
||||||
|
subscriptions: subscriptions.data,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
createCheckoutSession: adminProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
productId: z.string(),
|
||||||
|
serverQuantity: z.number().min(1),
|
||||||
|
isAnnual: z.boolean(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || "", {
|
||||||
|
apiVersion: "2024-09-30.acacia",
|
||||||
|
});
|
||||||
|
|
||||||
|
const items = getStripeItems(input.serverQuantity, input.isAnnual);
|
||||||
|
const admin = await findAdminById(ctx.user.adminId);
|
||||||
|
|
||||||
|
let stripeCustomerId = admin.stripeCustomerId;
|
||||||
|
|
||||||
|
if (stripeCustomerId) {
|
||||||
|
const customer = await stripe.customers.retrieve(stripeCustomerId);
|
||||||
|
|
||||||
|
if (customer.deleted) {
|
||||||
|
await updateAdmin(admin.authId, {
|
||||||
|
stripeCustomerId: null,
|
||||||
|
});
|
||||||
|
stripeCustomerId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = await stripe.checkout.sessions.create({
|
||||||
|
mode: "subscription",
|
||||||
|
line_items: items,
|
||||||
|
...(stripeCustomerId && {
|
||||||
|
customer: stripeCustomerId,
|
||||||
|
}),
|
||||||
|
metadata: {
|
||||||
|
adminId: admin.adminId,
|
||||||
|
},
|
||||||
|
success_url: `${WEBSITE_URL}/dashboard/settings/billing`,
|
||||||
|
cancel_url: `${WEBSITE_URL}/dashboard/settings/billing`,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { sessionId: session.id };
|
||||||
|
}),
|
||||||
|
createCustomerPortalSession: adminProcedure.mutation(
|
||||||
|
async ({ ctx, input }) => {
|
||||||
|
const admin = await findAdminById(ctx.user.adminId);
|
||||||
|
|
||||||
|
if (!admin.stripeCustomerId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Stripe Customer ID not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const stripeCustomerId = admin.stripeCustomerId;
|
||||||
|
|
||||||
|
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || "", {
|
||||||
|
apiVersion: "2024-09-30.acacia",
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const session = await stripe.billingPortal.sessions.create({
|
||||||
|
customer: stripeCustomerId,
|
||||||
|
return_url: `${WEBSITE_URL}/dashboard/settings/billing`,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { url: session.url };
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
url: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
canCreateMoreServers: adminProcedure.query(async ({ ctx }) => {
|
||||||
|
const admin = await findAdminById(ctx.user.adminId);
|
||||||
|
const servers = await findServersByAdminId(admin.adminId);
|
||||||
|
|
||||||
|
if (!IS_CLOUD) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return servers.length < admin.serversQuantity;
|
||||||
|
}),
|
||||||
|
});
|
||||||
@@ -1 +1 @@
|
|||||||
export * from "@dokploy/server/dist/db/schema";
|
export * from "@dokploy/server/db/schema";
|
||||||
|
|||||||
@@ -11,29 +11,8 @@ import {
|
|||||||
updateCompose,
|
updateCompose,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { type Job, Worker } from "bullmq";
|
import { type Job, Worker } from "bullmq";
|
||||||
import { myQueue, redisConfig } from "./queueSetup";
|
import type { DeploymentJob } from "./queue-types";
|
||||||
|
import { redisConfig } from "./redis-connection";
|
||||||
type DeployJob =
|
|
||||||
| {
|
|
||||||
applicationId: string;
|
|
||||||
titleLog: string;
|
|
||||||
descriptionLog: string;
|
|
||||||
server?: boolean;
|
|
||||||
type: "deploy" | "redeploy";
|
|
||||||
applicationType: "application";
|
|
||||||
serverId?: string;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
composeId: string;
|
|
||||||
titleLog: string;
|
|
||||||
descriptionLog: string;
|
|
||||||
server?: boolean;
|
|
||||||
type: "deploy" | "redeploy";
|
|
||||||
applicationType: "compose";
|
|
||||||
serverId?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type DeploymentJob = DeployJob;
|
|
||||||
|
|
||||||
export const deploymentWorker = new Worker(
|
export const deploymentWorker = new Worker(
|
||||||
"deployments",
|
"deployments",
|
||||||
@@ -114,25 +93,3 @@ export const deploymentWorker = new Worker(
|
|||||||
connection: redisConfig,
|
connection: redisConfig,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export const cleanQueuesByApplication = async (applicationId: string) => {
|
|
||||||
const jobs = await myQueue.getJobs(["waiting", "delayed"]);
|
|
||||||
|
|
||||||
for (const job of jobs) {
|
|
||||||
if (job?.data?.applicationId === applicationId) {
|
|
||||||
await job.remove();
|
|
||||||
console.log(`Removed job ${job.id} for application ${applicationId}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const cleanQueuesByCompose = async (composeId: string) => {
|
|
||||||
const jobs = await myQueue.getJobs(["waiting", "delayed"]);
|
|
||||||
|
|
||||||
for (const job of jobs) {
|
|
||||||
if (job?.data?.composeId === composeId) {
|
|
||||||
await job.remove();
|
|
||||||
console.log(`Removed job ${job.id} for compose ${composeId}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|||||||
21
apps/dokploy/server/queues/queue-types.ts
Normal file
21
apps/dokploy/server/queues/queue-types.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
type DeployJob =
|
||||||
|
| {
|
||||||
|
applicationId: string;
|
||||||
|
titleLog: string;
|
||||||
|
descriptionLog: string;
|
||||||
|
server?: boolean;
|
||||||
|
type: "deploy" | "redeploy";
|
||||||
|
applicationType: "application";
|
||||||
|
serverId?: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
composeId: string;
|
||||||
|
titleLog: string;
|
||||||
|
descriptionLog: string;
|
||||||
|
server?: boolean;
|
||||||
|
type: "deploy" | "redeploy";
|
||||||
|
applicationType: "compose";
|
||||||
|
serverId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DeploymentJob = DeployJob;
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
import { type ConnectionOptions, Queue } from "bullmq";
|
import { Queue } from "bullmq";
|
||||||
|
import { redisConfig } from "./redis-connection";
|
||||||
|
|
||||||
export const redisConfig: ConnectionOptions = {
|
|
||||||
host: process.env.NODE_ENV === "production" ? "dokploy-redis" : "127.0.0.1",
|
|
||||||
};
|
|
||||||
const myQueue = new Queue("deployments", {
|
const myQueue = new Queue("deployments", {
|
||||||
connection: redisConfig,
|
connection: redisConfig,
|
||||||
});
|
});
|
||||||
@@ -21,4 +19,26 @@ myQueue.on("error", (error) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const cleanQueuesByApplication = async (applicationId: string) => {
|
||||||
|
const jobs = await myQueue.getJobs(["waiting", "delayed"]);
|
||||||
|
|
||||||
|
for (const job of jobs) {
|
||||||
|
if (job?.data?.applicationId === applicationId) {
|
||||||
|
await job.remove();
|
||||||
|
console.log(`Removed job ${job.id} for application ${applicationId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const cleanQueuesByCompose = async (composeId: string) => {
|
||||||
|
const jobs = await myQueue.getJobs(["waiting", "delayed"]);
|
||||||
|
|
||||||
|
for (const job of jobs) {
|
||||||
|
if (job?.data?.composeId === composeId) {
|
||||||
|
await job.remove();
|
||||||
|
console.log(`Removed job ${job.id} for compose ${composeId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export { myQueue };
|
export { myQueue };
|
||||||
|
|||||||
5
apps/dokploy/server/queues/redis-connection.ts
Normal file
5
apps/dokploy/server/queues/redis-connection.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import type { ConnectionOptions } from "bullmq";
|
||||||
|
|
||||||
|
export const redisConfig: ConnectionOptions = {
|
||||||
|
host: process.env.NODE_ENV === "production" ? "dokploy-redis" : "127.0.0.1",
|
||||||
|
};
|
||||||
@@ -19,15 +19,12 @@ import { setupDockerContainerLogsWebSocketServer } from "./wss/docker-container-
|
|||||||
import { setupDockerContainerTerminalWebSocketServer } from "./wss/docker-container-terminal";
|
import { setupDockerContainerTerminalWebSocketServer } from "./wss/docker-container-terminal";
|
||||||
import { setupDockerStatsMonitoringSocketServer } from "./wss/docker-stats";
|
import { setupDockerStatsMonitoringSocketServer } from "./wss/docker-stats";
|
||||||
import { setupDeploymentLogsWebSocketServer } from "./wss/listen-deployment";
|
import { setupDeploymentLogsWebSocketServer } from "./wss/listen-deployment";
|
||||||
import {
|
import { setupTerminalWebSocketServer } from "./wss/terminal";
|
||||||
getPublicIpWithFallback,
|
|
||||||
setupTerminalWebSocketServer,
|
|
||||||
} from "./wss/terminal";
|
|
||||||
|
|
||||||
config({ path: ".env" });
|
config({ path: ".env" });
|
||||||
const PORT = Number.parseInt(process.env.PORT || "3000", 10);
|
const PORT = Number.parseInt(process.env.PORT || "3000", 10);
|
||||||
const dev = process.env.NODE_ENV !== "production";
|
const dev = process.env.NODE_ENV !== "production";
|
||||||
const app = next({ dev });
|
const app = next({ dev, turbopack: dev });
|
||||||
const handle = app.getRequestHandler();
|
const handle = app.getRequestHandler();
|
||||||
void app.prepare().then(async () => {
|
void app.prepare().then(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -40,7 +37,9 @@ void app.prepare().then(async () => {
|
|||||||
setupDockerContainerLogsWebSocketServer(server);
|
setupDockerContainerLogsWebSocketServer(server);
|
||||||
setupDockerContainerTerminalWebSocketServer(server);
|
setupDockerContainerTerminalWebSocketServer(server);
|
||||||
setupTerminalWebSocketServer(server);
|
setupTerminalWebSocketServer(server);
|
||||||
setupDockerStatsMonitoringSocketServer(server);
|
if (!IS_CLOUD) {
|
||||||
|
setupDockerStatsMonitoringSocketServer(server);
|
||||||
|
}
|
||||||
|
|
||||||
if (process.env.NODE_ENV === "production" && !IS_CLOUD) {
|
if (process.env.NODE_ENV === "production" && !IS_CLOUD) {
|
||||||
setupDirectories();
|
setupDirectories();
|
||||||
@@ -53,7 +52,6 @@ void app.prepare().then(async () => {
|
|||||||
await initializeRedis();
|
await initializeRedis();
|
||||||
|
|
||||||
initCronJobs();
|
initCronJobs();
|
||||||
welcomeServer();
|
|
||||||
|
|
||||||
// Timeout to wait for the database to be ready
|
// Timeout to wait for the database to be ready
|
||||||
await new Promise((resolve) => setTimeout(resolve, 7000));
|
await new Promise((resolve) => setTimeout(resolve, 7000));
|
||||||
@@ -76,18 +74,3 @@ void app.prepare().then(async () => {
|
|||||||
console.error("Main Server Error", e);
|
console.error("Main Server Error", e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
async function welcomeServer() {
|
|
||||||
const ip = await getPublicIpWithFallback();
|
|
||||||
console.log(
|
|
||||||
[
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
"Dokploy server is up and running!",
|
|
||||||
"Please wait for 15 seconds before opening the browser.",
|
|
||||||
` http://${ip}:${PORT}`,
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
].join("\n"),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import type { DeploymentJob } from "../queues/deployments-queue";
|
import { findServerById } from "@dokploy/server";
|
||||||
|
import type { DeploymentJob } from "../queues/queue-types";
|
||||||
|
|
||||||
export const deploy = async (jobData: DeploymentJob) => {
|
export const deploy = async (jobData: DeploymentJob) => {
|
||||||
try {
|
try {
|
||||||
|
const server = await findServerById(jobData.serverId as string);
|
||||||
|
if (server.serverStatus === "inactive") {
|
||||||
|
throw new Error("Server is inactive");
|
||||||
|
}
|
||||||
const result = await fetch(`${process.env.SERVER_URL}/deploy`, {
|
const result = await fetch(`${process.env.SERVER_URL}/deploy`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
27
apps/dokploy/server/utils/stripe.ts
Normal file
27
apps/dokploy/server/utils/stripe.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
export const WEBSITE_URL =
|
||||||
|
process.env.NODE_ENV === "development"
|
||||||
|
? "http://localhost:3000"
|
||||||
|
: "https://app.dokploy.com";
|
||||||
|
|
||||||
|
const BASE_PRICE_MONTHLY_ID = process.env.BASE_PRICE_MONTHLY_ID || ""; // $4.00
|
||||||
|
|
||||||
|
const BASE_ANNUAL_MONTHLY_ID = process.env.BASE_ANNUAL_MONTHLY_ID || ""; // $7.99
|
||||||
|
|
||||||
|
export const getStripeItems = (serverQuantity: number, isAnnual: boolean) => {
|
||||||
|
const items = [];
|
||||||
|
|
||||||
|
if (isAnnual) {
|
||||||
|
items.push({
|
||||||
|
price: BASE_ANNUAL_MONTHLY_ID,
|
||||||
|
quantity: serverQuantity,
|
||||||
|
});
|
||||||
|
|
||||||
|
return items;
|
||||||
|
}
|
||||||
|
items.push({
|
||||||
|
price: BASE_PRICE_MONTHLY_ID,
|
||||||
|
quantity: serverQuantity,
|
||||||
|
});
|
||||||
|
|
||||||
|
return items;
|
||||||
|
};
|
||||||
@@ -3,15 +3,15 @@ import {
|
|||||||
createDefaultServerTraefikConfig,
|
createDefaultServerTraefikConfig,
|
||||||
createDefaultTraefikConfig,
|
createDefaultTraefikConfig,
|
||||||
initializeTraefik,
|
initializeTraefik,
|
||||||
} from "@dokploy/server/dist/setup/traefik-setup";
|
} from "@dokploy/server/setup/traefik-setup";
|
||||||
|
|
||||||
import { setupDirectories } from "@dokploy/server/dist/setup/config-paths";
|
import { setupDirectories } from "@dokploy/server/setup/config-paths";
|
||||||
import { initializePostgres } from "@dokploy/server/dist/setup/postgres-setup";
|
import { initializePostgres } from "@dokploy/server/setup/postgres-setup";
|
||||||
import { initializeRedis } from "@dokploy/server/dist/setup/redis-setup";
|
import { initializeRedis } from "@dokploy/server/setup/redis-setup";
|
||||||
import {
|
import {
|
||||||
initializeNetwork,
|
initializeNetwork,
|
||||||
initializeSwarm,
|
initializeSwarm,
|
||||||
} from "@dokploy/server/dist/setup/setup";
|
} from "@dokploy/server/setup/setup";
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
setupDirectories();
|
setupDirectories();
|
||||||
|
|||||||
@@ -26,7 +26,8 @@
|
|||||||
/* Path Aliases */
|
/* Path Aliases */
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./*"]
|
"@/*": ["./*"],
|
||||||
|
"@dokploy/server/*": ["../../packages/server/src/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,8 @@
|
|||||||
"moduleResolution": "Node",
|
"moduleResolution": "Node",
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./*"]
|
"@/*": ["./*"],
|
||||||
|
"@dokploy/server/*": ["../../packages/server/src/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "./server/**/*"]
|
"include": ["next-env.d.ts", "./server/**/*"]
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ import {
|
|||||||
cleanUpSystemPrune,
|
cleanUpSystemPrune,
|
||||||
cleanUpUnusedImages,
|
cleanUpUnusedImages,
|
||||||
findBackupById,
|
findBackupById,
|
||||||
|
findServerById,
|
||||||
runMariadbBackup,
|
runMariadbBackup,
|
||||||
runMongoBackup,
|
runMongoBackup,
|
||||||
runMySqlBackup,
|
runMySqlBackup,
|
||||||
runPostgresBackup,
|
runPostgresBackup,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server/dist";
|
||||||
import { db } from "@dokploy/server/dist/db";
|
import { db } from "@dokploy/server/dist/db";
|
||||||
import { backups, server } from "@dokploy/server/dist/db/schema";
|
import { backups, server } from "@dokploy/server/dist/db/schema";
|
||||||
import { eq } from "drizzle-orm";
|
import { eq } from "drizzle-orm";
|
||||||
@@ -21,22 +22,47 @@ export const runJobs = async (job: QueueJob) => {
|
|||||||
const { backupId } = job;
|
const { backupId } = job;
|
||||||
const backup = await findBackupById(backupId);
|
const backup = await findBackupById(backupId);
|
||||||
const { databaseType, postgres, mysql, mongo, mariadb } = backup;
|
const { databaseType, postgres, mysql, mongo, mariadb } = backup;
|
||||||
|
|
||||||
if (databaseType === "postgres" && postgres) {
|
if (databaseType === "postgres" && postgres) {
|
||||||
|
const server = await findServerById(postgres.serverId as string);
|
||||||
|
if (server.serverStatus === "inactive") {
|
||||||
|
logger.info("Server is inactive");
|
||||||
|
return;
|
||||||
|
}
|
||||||
await runPostgresBackup(postgres, backup);
|
await runPostgresBackup(postgres, backup);
|
||||||
} else if (databaseType === "mysql" && mysql) {
|
} else if (databaseType === "mysql" && mysql) {
|
||||||
|
const server = await findServerById(mysql.serverId as string);
|
||||||
|
if (server.serverStatus === "inactive") {
|
||||||
|
logger.info("Server is inactive");
|
||||||
|
return;
|
||||||
|
}
|
||||||
await runMySqlBackup(mysql, backup);
|
await runMySqlBackup(mysql, backup);
|
||||||
} else if (databaseType === "mongo" && mongo) {
|
} else if (databaseType === "mongo" && mongo) {
|
||||||
|
const server = await findServerById(mongo.serverId as string);
|
||||||
|
if (server.serverStatus === "inactive") {
|
||||||
|
logger.info("Server is inactive");
|
||||||
|
return;
|
||||||
|
}
|
||||||
await runMongoBackup(mongo, backup);
|
await runMongoBackup(mongo, backup);
|
||||||
} else if (databaseType === "mariadb" && mariadb) {
|
} else if (databaseType === "mariadb" && mariadb) {
|
||||||
|
const server = await findServerById(mariadb.serverId as string);
|
||||||
|
if (server.serverStatus === "inactive") {
|
||||||
|
logger.info("Server is inactive");
|
||||||
|
return;
|
||||||
|
}
|
||||||
await runMariadbBackup(mariadb, backup);
|
await runMariadbBackup(mariadb, backup);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (job.type === "server") {
|
if (job.type === "server") {
|
||||||
const { serverId } = job;
|
const { serverId } = job;
|
||||||
|
const server = await findServerById(serverId);
|
||||||
|
if (server.serverStatus === "inactive") {
|
||||||
|
logger.info("Server is inactive");
|
||||||
|
return;
|
||||||
|
}
|
||||||
await cleanUpUnusedImages(serverId);
|
await cleanUpUnusedImages(serverId);
|
||||||
await cleanUpDockerBuilder(serverId);
|
await cleanUpDockerBuilder(serverId);
|
||||||
await cleanUpSystemPrune(serverId);
|
await cleanUpSystemPrune(serverId);
|
||||||
// await sendDockerCleanupNotifications();
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(error);
|
logger.error(error);
|
||||||
|
|||||||
@@ -305,6 +305,19 @@ export const ShowSponsors = () => {
|
|||||||
alt="Rivo.gg"
|
alt="Rivo.gg"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="https://photoquest.wedding/?ref=dokploy"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="https://photoquest.wedding/favicon/android-chrome-512x512.png"
|
||||||
|
className="rounded-xl"
|
||||||
|
width="60px"
|
||||||
|
alt="Rivo.gg"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-4 md:gap-8 justify-start">
|
<div className="flex flex-col gap-4 md:gap-8 justify-start">
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { Link } from "@/i18n/routing";
|
import { Link } from "@/i18n/routing";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { Footer } from "./Footer";
|
|
||||||
import { Header } from "./Header";
|
|
||||||
|
|
||||||
export function SlimLayout() {
|
export function SlimLayout() {
|
||||||
const t = useTranslations("404");
|
const t = useTranslations("404");
|
||||||
@@ -16,6 +14,7 @@ export function SlimLayout() {
|
|||||||
<Link href="/" className="text-primary">
|
<Link href="/" className="text-primary">
|
||||||
{t("action")}
|
{t("action")}
|
||||||
</Link>
|
</Link>
|
||||||
|
p{" "}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import { MinusIcon, PlusIcon } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Container } from "./Container";
|
import { Container } from "./Container";
|
||||||
import { trackGAEvent } from "./analitycs";
|
import { trackGAEvent } from "./analitycs";
|
||||||
import { Button } from "./ui/button";
|
import { Badge } from "./ui/badge";
|
||||||
import { Switch } from "./ui/switch";
|
import { Button, buttonVariants } from "./ui/button";
|
||||||
|
import { NumberInput } from "./ui/input";
|
||||||
|
import { Tabs, TabsList, TabsTrigger } from "./ui/tabs";
|
||||||
|
|
||||||
function SwirlyDoodle(props: React.ComponentPropsWithoutRef<"svg">) {
|
function SwirlyDoodle(props: React.ComponentPropsWithoutRef<"svg">) {
|
||||||
return (
|
return (
|
||||||
@@ -55,7 +59,14 @@ function CheckIcon({
|
|||||||
</svg>
|
</svg>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
export const calculatePrice = (count: number, isAnnual = false) => {
|
||||||
|
if (isAnnual) {
|
||||||
|
if (count <= 1) return 45.9;
|
||||||
|
return 35.7 * count;
|
||||||
|
}
|
||||||
|
if (count <= 1) return 4.5;
|
||||||
|
return count * 3.5;
|
||||||
|
};
|
||||||
function Plan({
|
function Plan({
|
||||||
name,
|
name,
|
||||||
price,
|
price,
|
||||||
@@ -63,6 +74,7 @@ function Plan({
|
|||||||
href,
|
href,
|
||||||
features,
|
features,
|
||||||
featured = false,
|
featured = false,
|
||||||
|
buttonText = "Get Started",
|
||||||
}: {
|
}: {
|
||||||
name: string;
|
name: string;
|
||||||
price: string;
|
price: string;
|
||||||
@@ -70,6 +82,7 @@ function Plan({
|
|||||||
href: string;
|
href: string;
|
||||||
features: Array<string>;
|
features: Array<string>;
|
||||||
featured?: boolean;
|
featured?: boolean;
|
||||||
|
buttonText?: string;
|
||||||
}) {
|
}) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
return (
|
return (
|
||||||
@@ -116,23 +129,17 @@ function Plan({
|
|||||||
}}
|
}}
|
||||||
className="rounded-full mt-8"
|
className="rounded-full mt-8"
|
||||||
>
|
>
|
||||||
Get started
|
{buttonText}
|
||||||
</Button>
|
</Button>
|
||||||
{/* <Button
|
|
||||||
href={href}
|
|
||||||
variant={featured ? "solid" : "outline"}
|
|
||||||
color="white"
|
|
||||||
className="mt-8"
|
|
||||||
aria-label={`Get started with the ${name} plan for ${price}`}
|
|
||||||
>
|
|
||||||
Get started
|
|
||||||
</Button> */}
|
|
||||||
</section>
|
</section>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
export function Pricing() {
|
export function Pricing() {
|
||||||
const [monthly, setMonthly] = useState(false);
|
const router = useRouter();
|
||||||
|
const [isAnnual, setIsAnnual] = useState(true);
|
||||||
|
const [serverQuantity, setServerQuantity] = useState(3);
|
||||||
|
const featured = true;
|
||||||
return (
|
return (
|
||||||
<section
|
<section
|
||||||
id="pricing"
|
id="pricing"
|
||||||
@@ -152,54 +159,200 @@ export function Pricing() {
|
|||||||
Deploy Smarter, Scale Faster – Without Breaking the Bank
|
Deploy Smarter, Scale Faster – Without Breaking the Bank
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-10 flex flex-row gap-x-4 justify-center">
|
|
||||||
<Switch checked={monthly} onCheckedChange={(e) => setMonthly(e)} />
|
|
||||||
{!monthly ? "Monthly" : "Yearly"}
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div className=" mt-10 mx-auto">
|
<div className=" mt-10 mx-auto">
|
||||||
<div className="mt-16 grid md:grid-cols-2 gap-y-10 mx-auto w-full lg:-mx-8 xl:mx-0 xl:gap-x-8">
|
<div className="mt-16 flex flex-col gap-10 mx-auto w-full lg:-mx-8 xl:mx-0 xl:gap-x-8 justify-center items-center">
|
||||||
<Plan
|
<Tabs
|
||||||
name="Free"
|
defaultValue="monthly"
|
||||||
price="$0"
|
value={isAnnual ? "annual" : "monthly"}
|
||||||
description="Perfect for developers who prefer to manage their own servers."
|
// className="w-full"
|
||||||
href="https://docs.dokploy.com/en/docs/core/get-started/installation#docker"
|
onValueChange={(e) => setIsAnnual(e === "annual")}
|
||||||
features={[
|
>
|
||||||
"Unlimited deployments",
|
<TabsList>
|
||||||
"Self-hosted on your own infrastructure",
|
<TabsTrigger value="monthly">Monthly</TabsTrigger>
|
||||||
"Full access to all deployment features",
|
<TabsTrigger value="annual">Annual</TabsTrigger>
|
||||||
"Docker Swarm and Docker Compose support",
|
</TabsList>
|
||||||
"Community support",
|
</Tabs>
|
||||||
"Custom domains and SSL integration",
|
<div className="flex flex-row max-w-4xl gap-4 mx-auto">
|
||||||
"No feature limitations on the core platform",
|
<section
|
||||||
]}
|
className={clsx(
|
||||||
/>
|
"flex flex-col rounded-3xl border-dashed border-muted border-2 px-4 max-w-sm",
|
||||||
<Plan
|
featured
|
||||||
featured
|
? "order-first bg-black border py-8 lg:order-none"
|
||||||
name="General"
|
: "lg:py-8",
|
||||||
price={!monthly ? "$6.99" : "$5.49"}
|
)}
|
||||||
description="Ideal for indie hackers, freelancers, agencies, and businesses looking for a managed solution."
|
>
|
||||||
href="/register"
|
<div className="flex flex-row gap-2 items-center">
|
||||||
features={[
|
<p className=" text-2xl font-semibold tracking-tight text-primary ">
|
||||||
"1 free server included (user-provided)",
|
Free
|
||||||
"All self-hosted features without hosting the UI",
|
</p>
|
||||||
"Dokploy infrastructure managed by us",
|
|
|
||||||
"$3.99 per additional server (user-provided)",
|
<p className=" text-base font-semibold tracking-tight text-muted-foreground">
|
||||||
]}
|
Open Source
|
||||||
/>
|
</p>
|
||||||
{/* <Plan
|
</div>
|
||||||
name="Enterprise"
|
|
||||||
price="$39"
|
<h3 className="mt-5 font-medium text-lg text-white">
|
||||||
description="For even the biggest enterprise companies."
|
Dokploy Open Source
|
||||||
href="/register"
|
</h3>
|
||||||
features={[
|
<p
|
||||||
"Send unlimited quotes and invoices",
|
className={clsx(
|
||||||
"Connect up to 15 bank accounts",
|
"text-sm",
|
||||||
"Track up to 200 expenses per month",
|
featured ? "text-white" : "text-slate-400",
|
||||||
"Automated payroll support",
|
)}
|
||||||
"Export up to 25 reports, including TPS",
|
>
|
||||||
]}
|
Manager your own infrastructure installing dokploy ui in your
|
||||||
/> */}
|
own server.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul
|
||||||
|
role="list"
|
||||||
|
className={clsx(
|
||||||
|
" mt-4 flex flex-col gap-y-2 text-sm",
|
||||||
|
featured ? "text-white" : "text-slate-200",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{[
|
||||||
|
"Complete Flexibility: Install Dokploy UI on your own infrastructure",
|
||||||
|
"Unlimited Deployments",
|
||||||
|
"Self-hosted Infrastructure",
|
||||||
|
"Community Support",
|
||||||
|
"Access to Core Features",
|
||||||
|
"Dokploy Integration",
|
||||||
|
"Basic Backups",
|
||||||
|
"Access to All Updates",
|
||||||
|
].map((feature) => (
|
||||||
|
<li key={feature} className="flex text-muted-foreground">
|
||||||
|
<CheckIcon />
|
||||||
|
<span className="ml-2">{feature}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<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">
|
||||||
|
Unlimited Servers
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section
|
||||||
|
className={clsx(
|
||||||
|
"flex flex-col rounded-3xl border-dashed border-2 px-4 max-w-sm",
|
||||||
|
featured
|
||||||
|
? "order-first bg-black border py-8 lg:order-none"
|
||||||
|
: "lg:py-8",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="flex flex-row gap-2 items-center mb-4">
|
||||||
|
<Badge>Recommended 🚀</Badge>
|
||||||
|
</div>
|
||||||
|
{isAnnual ? (
|
||||||
|
<div className="flex flex-row gap-2 items-center">
|
||||||
|
<p className=" text-2xl font-semibold tracking-tight text-primary ">
|
||||||
|
$ {calculatePrice(serverQuantity, isAnnual).toFixed(2)}{" "}
|
||||||
|
USD
|
||||||
|
</p>
|
||||||
|
|
|
||||||
|
<p className=" text-base font-semibold tracking-tight text-muted-foreground">
|
||||||
|
${" "}
|
||||||
|
{(calculatePrice(serverQuantity, isAnnual) / 12).toFixed(
|
||||||
|
2,
|
||||||
|
)}{" "}
|
||||||
|
/ Month USD
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<p className=" text-2xl font-semibold tracking-tight text-primary ">
|
||||||
|
$ {calculatePrice(serverQuantity, isAnnual).toFixed(2)} USD
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<h3 className="mt-5 font-medium text-lg text-white">
|
||||||
|
Dokploy Plan
|
||||||
|
</h3>
|
||||||
|
<p
|
||||||
|
className={clsx(
|
||||||
|
"text-sm",
|
||||||
|
featured ? "text-white" : "text-slate-400",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
No need to manage Dokploy UI infrastructure, we take care of
|
||||||
|
it for you.
|
||||||
|
</p>
|
||||||
|
|
||||||
|
<ul
|
||||||
|
role="list"
|
||||||
|
className={clsx(
|
||||||
|
" mt-4 flex flex-col gap-y-2 text-sm",
|
||||||
|
featured ? "text-white" : "text-slate-200",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
{[
|
||||||
|
"Managed Hosting: No need to manage your own servers",
|
||||||
|
"Priority Support",
|
||||||
|
"Future-Proof Features",
|
||||||
|
].map((feature) => (
|
||||||
|
<li key={feature} className="flex text-muted-foreground">
|
||||||
|
<CheckIcon />
|
||||||
|
<span className="ml-2">{feature}</span>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
<div className="flex flex-col gap-2 mt-4">
|
||||||
|
<div className="flex items-center gap-2 justify-center">
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
{serverQuantity} Servers (You bring the servers)
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<Button
|
||||||
|
disabled={serverQuantity <= 1}
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
if (serverQuantity <= 1) return;
|
||||||
|
|
||||||
|
setServerQuantity(serverQuantity - 1);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<MinusIcon className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
<NumberInput
|
||||||
|
value={serverQuantity}
|
||||||
|
onChange={(e) => {
|
||||||
|
setServerQuantity(e.target.value as unknown as number);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
variant="outline"
|
||||||
|
onClick={() => {
|
||||||
|
setServerQuantity(serverQuantity + 1);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<PlusIcon className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
className={cn(
|
||||||
|
"justify-between",
|
||||||
|
// : "justify-end",
|
||||||
|
"flex flex-row items-center gap-2 mt-4",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div className="justify-end w-full">
|
||||||
|
<Link
|
||||||
|
href="https://app.dokploy.com/register"
|
||||||
|
target="_blank"
|
||||||
|
className={buttonVariants({ className: "w-full" })}
|
||||||
|
>
|
||||||
|
Subscribe
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Container>
|
</Container>
|
||||||
@@ -212,48 +365,43 @@ export function Pricing() {
|
|||||||
const faqs = [
|
const faqs = [
|
||||||
[
|
[
|
||||||
{
|
{
|
||||||
question: "How does Dokploy's free plan work?",
|
question: "How does Dokploy's Open Source plan work?",
|
||||||
answer:
|
answer:
|
||||||
"The free plan allows you to self-host Dokploy on your own infrastructure with unlimited deployments and full access to all features.",
|
"You can host Dokploy UI on your own infrastructure and you will be responsible for the maintenance and updates.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
question: "Do I need to provide my own server for the managed plan?",
|
question: "Do I need to provide my own server for the managed plan?",
|
||||||
answer:
|
answer:
|
||||||
"Yes, in the managed plan, you provide your own server, and we manage the Dokploy UI infrastructure for you.",
|
"Yes, in the managed plan, you provide your own server eg(Hetzner, Hostinger, AWS, ETC.) VPS, and we manage the Dokploy UI infrastructure for you.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
question: "What happens if I need more than one server?",
|
question: "What happens if I need more than one server?",
|
||||||
answer:
|
answer:
|
||||||
"Each additional server costs $3.99/month and can be easily added to your account.",
|
"The first server costs $4.50/month, if you buy more than one it will be $3.50/month per server.",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
{
|
|
||||||
question: "Can I use my custom domain with Dokploy?",
|
|
||||||
answer:
|
|
||||||
"Yes, custom domain support is available on all plans, including the free version.",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
question: "Is there a limit on the number of deployments?",
|
question: "Is there a limit on the number of deployments?",
|
||||||
answer:
|
answer:
|
||||||
"No, there is no limit on the number of deployments in any of the plans.",
|
"No, there is no limit on the number of deployments in any of the plans.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
question: "Do I have to manually configure Traefik?",
|
question: "What happens if I exceed my purchased server limit?",
|
||||||
answer:
|
answer:
|
||||||
"Dokploy offers dynamic Traefik configuration out-of-the-box, so no manual setup is needed.",
|
"The most recently added servers will be deactivated. You won't be able to create services on inactive servers until they are reactivated.",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
question: "Do you offer a refunds?",
|
||||||
|
answer:
|
||||||
|
"We do not offer refunds. However, you can cancel your subscription at any time. Feel free to try our open-source version for free before making a purchase.",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
[
|
[
|
||||||
{
|
|
||||||
question: "How do automated backups work?",
|
|
||||||
answer:
|
|
||||||
"Automated backups are included in the managed plan and are limited to database backups only.",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
question: "What kind of support do you offer?",
|
question: "What kind of support do you offer?",
|
||||||
answer:
|
answer:
|
||||||
"We offer community support for the free plan and priority support for paid plans.",
|
"We offer community support for the open source version and priority support for paid plans.",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
question: "Is Dokploy open-source?",
|
question: "Is Dokploy open-source?",
|
||||||
@@ -279,7 +427,7 @@ export function Faqs() {
|
|||||||
{"Frequently asked questions"}
|
{"Frequently asked questions"}
|
||||||
</h2>
|
</h2>
|
||||||
<p className="mt-4 text-lg tracking-tight text-muted-foreground">
|
<p className="mt-4 text-lg tracking-tight text-muted-foreground">
|
||||||
If you can’t find what you’re looking for, please submit an issue
|
If you can’t find what you’re looking for, please send us an email
|
||||||
to:{" "}
|
to:{" "}
|
||||||
<Link href={"mailto:support@dokploy.com"} className="text-primary">
|
<Link href={"mailto:support@dokploy.com"} className="text-primary">
|
||||||
support@dokploy.com
|
support@dokploy.com
|
||||||
|
|||||||
36
apps/website/components/ui/badge.tsx
Normal file
36
apps/website/components/ui/badge.tsx
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
import { type VariantProps, cva } from "class-variance-authority";
|
||||||
|
import type * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const badgeVariants = cva(
|
||||||
|
"inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
|
||||||
|
{
|
||||||
|
variants: {
|
||||||
|
variant: {
|
||||||
|
default:
|
||||||
|
"border-transparent bg-primary text-primary-foreground hover:bg-primary/80",
|
||||||
|
secondary:
|
||||||
|
"border-transparent bg-secondary text-secondary-foreground hover:bg-secondary/80",
|
||||||
|
destructive:
|
||||||
|
"border-transparent bg-destructive text-destructive-foreground hover:bg-destructive/80",
|
||||||
|
outline: "text-foreground",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
defaultVariants: {
|
||||||
|
variant: "default",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
export interface BadgeProps
|
||||||
|
extends React.HTMLAttributes<HTMLDivElement>,
|
||||||
|
VariantProps<typeof badgeVariants> {}
|
||||||
|
|
||||||
|
function Badge({ className, variant, ...props }: BadgeProps) {
|
||||||
|
return (
|
||||||
|
<div className={cn(badgeVariants({ variant }), className)} {...props} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export { Badge, badgeVariants };
|
||||||
69
apps/website/components/ui/input.tsx
Normal file
69
apps/website/components/ui/input.tsx
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
export interface InputProps
|
||||||
|
extends React.InputHTMLAttributes<HTMLInputElement> {
|
||||||
|
errorMessage?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
||||||
|
({ className, errorMessage, type, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<input
|
||||||
|
type={type}
|
||||||
|
className={cn(
|
||||||
|
// bg-gray
|
||||||
|
"flex h-10 w-full rounded-md bg-input px-3 py-2 text-sm file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
{errorMessage && (
|
||||||
|
<span className="text-sm text-red-600 text-secondary-foreground">
|
||||||
|
{errorMessage}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
Input.displayName = "Input";
|
||||||
|
|
||||||
|
const NumberInput = React.forwardRef<HTMLInputElement, InputProps>(
|
||||||
|
({ className, errorMessage, ...props }, ref) => {
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
type="text"
|
||||||
|
className={cn("text-left", className)}
|
||||||
|
ref={ref}
|
||||||
|
{...props}
|
||||||
|
value={props.value === undefined ? undefined : String(props.value)}
|
||||||
|
onChange={(e) => {
|
||||||
|
const value = e.target.value;
|
||||||
|
if (value === "") {
|
||||||
|
props.onChange?.(e);
|
||||||
|
} else {
|
||||||
|
const number = Number.parseInt(value, 10);
|
||||||
|
if (!Number.isNaN(number)) {
|
||||||
|
const syntheticEvent = {
|
||||||
|
...e,
|
||||||
|
target: {
|
||||||
|
...e.target,
|
||||||
|
value: number,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
props.onChange?.(
|
||||||
|
syntheticEvent as unknown as React.ChangeEvent<HTMLInputElement>,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
);
|
||||||
|
NumberInput.displayName = "NumberInput";
|
||||||
|
|
||||||
|
export { Input, NumberInput };
|
||||||
53
apps/website/components/ui/tabs.tsx
Normal file
53
apps/website/components/ui/tabs.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import * as TabsPrimitive from "@radix-ui/react-tabs";
|
||||||
|
import * as React from "react";
|
||||||
|
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
|
|
||||||
|
const Tabs = TabsPrimitive.Root;
|
||||||
|
|
||||||
|
const TabsList = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.List>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.List
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TabsList.displayName = TabsPrimitive.List.displayName;
|
||||||
|
|
||||||
|
const TabsTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName;
|
||||||
|
|
||||||
|
const TabsContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||||
|
className,
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
));
|
||||||
|
TabsContent.displayName = TabsPrimitive.Content.displayName;
|
||||||
|
|
||||||
|
export { Tabs, TabsList, TabsTrigger, TabsContent };
|
||||||
@@ -24,9 +24,9 @@
|
|||||||
"projectsDes": "Manage and organize all your projects in one place, keeping detailed track of progress and resource allocation.",
|
"projectsDes": "Manage and organize all your projects in one place, keeping detailed track of progress and resource allocation.",
|
||||||
"applications": "Applications & Databases",
|
"applications": "Applications & Databases",
|
||||||
"applicationsDes": "Centralize control over your applications and databases for enhanced security and efficiency, simplifying access and management across your infrastructure.",
|
"applicationsDes": "Centralize control over your applications and databases for enhanced security and efficiency, simplifying access and management across your infrastructure.",
|
||||||
"compose": "compose",
|
"compose": "Compose",
|
||||||
"composeDes": "Native Docker Compose support for manage complex applications and services with ease.",
|
"composeDes": "Native Docker Compose support for manage complex applications and services with ease.",
|
||||||
"multinode": "multinode",
|
"multinode": "Multinode",
|
||||||
"multinodeDes": "Scale applications to multiples nodes using docker swarm to manage the cluster.",
|
"multinodeDes": "Scale applications to multiples nodes using docker swarm to manage the cluster.",
|
||||||
"monitoring": "Monitoring",
|
"monitoring": "Monitoring",
|
||||||
"monitoringDes": "Monitor your systems' performance and health in real time, ensuring continuous and uninterrupted operation.",
|
"monitoringDes": "Monitor your systems' performance and health in real time, ensuring continuous and uninterrupted operation.",
|
||||||
|
|||||||
@@ -31,7 +31,8 @@
|
|||||||
"tailwind-merge": "^2.2.2",
|
"tailwind-merge": "^2.2.2",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"tailwindcss-animate": "^1.0.7",
|
"tailwindcss-animate": "^1.0.7",
|
||||||
"typescript": "5.1.6"
|
"typescript": "5.1.6",
|
||||||
|
"@radix-ui/react-tabs": "1.1.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "1.7.0",
|
"@biomejs/biome": "1.7.0",
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ install_dokploy() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
advertise_addr=$(get_ip)
|
advertise_addr="${ADVERTISE_ADDR:-$(get_ip)}"
|
||||||
|
|
||||||
docker swarm init --advertise-addr $advertise_addr
|
docker swarm init --advertise-addr $advertise_addr
|
||||||
|
|
||||||
@@ -92,6 +92,7 @@ install_dokploy() {
|
|||||||
--update-order stop-first \
|
--update-order stop-first \
|
||||||
--constraint 'node.role == manager' \
|
--constraint 'node.role == manager' \
|
||||||
-e RELEASE_TAG=canary \
|
-e RELEASE_TAG=canary \
|
||||||
|
-e ADVERTISE_ADDR=$advertise_addr \
|
||||||
dokploy/dokploy:canary
|
dokploy/dokploy:canary
|
||||||
|
|
||||||
GREEN="\033[0;32m"
|
GREEN="\033[0;32m"
|
||||||
|
|||||||
@@ -41,7 +41,23 @@ install_dokploy() {
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
docker swarm leave --force 2>/dev/null
|
docker swarm leave --force 2>/dev/null
|
||||||
advertise_addr=$(curl -s ifconfig.me)
|
|
||||||
|
get_ip() {
|
||||||
|
# Try to get IPv4
|
||||||
|
local ipv4=$(curl -4s https://ifconfig.io 2>/dev/null)
|
||||||
|
|
||||||
|
if [ -n "$ipv4" ]; then
|
||||||
|
echo "$ipv4"
|
||||||
|
else
|
||||||
|
# Try to get IPv6
|
||||||
|
local ipv6=$(curl -6s https://ifconfig.io 2>/dev/null)
|
||||||
|
if [ -n "$ipv6" ]; then
|
||||||
|
echo "$ipv6"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
advertise_addr="${ADVERTISE_ADDR:-$(get_ip)}"
|
||||||
|
|
||||||
docker swarm init --advertise-addr $advertise_addr
|
docker swarm init --advertise-addr $advertise_addr
|
||||||
|
|
||||||
@@ -76,6 +92,7 @@ install_dokploy() {
|
|||||||
--update-order stop-first \
|
--update-order stop-first \
|
||||||
--constraint 'node.role == manager' \
|
--constraint 'node.role == manager' \
|
||||||
-e RELEASE_TAG=feature \
|
-e RELEASE_TAG=feature \
|
||||||
|
-e ADVERTISE_ADDR=$advertise_addr \
|
||||||
dokploy/dokploy:feature
|
dokploy/dokploy:feature
|
||||||
|
|
||||||
GREEN="\033[0;32m"
|
GREEN="\033[0;32m"
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ install_dokploy() {
|
|||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
advertise_addr=$(get_ip)
|
advertise_addr="${ADVERTISE_ADDR:-$(get_ip)}"
|
||||||
|
|
||||||
docker swarm init --advertise-addr $advertise_addr
|
docker swarm init --advertise-addr $advertise_addr
|
||||||
|
|
||||||
@@ -90,6 +90,7 @@ install_dokploy() {
|
|||||||
--update-parallelism 1 \
|
--update-parallelism 1 \
|
||||||
--update-order stop-first \
|
--update-order stop-first \
|
||||||
--constraint 'node.role == manager' \
|
--constraint 'node.role == manager' \
|
||||||
|
-e ADVERTISE_ADDR=$advertise_addr \
|
||||||
dokploy/dokploy:latest
|
dokploy/dokploy:latest
|
||||||
|
|
||||||
GREEN="\033[0;32m"
|
GREEN="\033[0;32m"
|
||||||
|
|||||||
@@ -8,6 +8,7 @@
|
|||||||
"dokploy:build": "pnpm --filter=dokploy run build",
|
"dokploy:build": "pnpm --filter=dokploy run build",
|
||||||
"dokploy:start": "pnpm --filter=dokploy run start",
|
"dokploy:start": "pnpm --filter=dokploy run start",
|
||||||
"test": "pnpm --filter=dokploy run test",
|
"test": "pnpm --filter=dokploy run test",
|
||||||
|
"server:script": "pnpm --filter=server run switch:dev",
|
||||||
"server:dev": "pnpm --filter=server run dev",
|
"server:dev": "pnpm --filter=server run dev",
|
||||||
"server:build": "pnpm --filter=server run build",
|
"server:build": "pnpm --filter=server run build",
|
||||||
"docker:build:canary": "./apps/dokploy/docker/build.sh canary",
|
"docker:build:canary": "./apps/dokploy/docker/build.sh canary",
|
||||||
|
|||||||
@@ -1,28 +1,28 @@
|
|||||||
// import { build } from "esbuild";
|
import path from "node:path";
|
||||||
// import alias from "esbuild-plugin-alias";
|
import { fileURLToPath } from "node:url";
|
||||||
// import path from "node:path";
|
import { build } from "esbuild";
|
||||||
// import { fileURLToPath } from "node:url";
|
import alias from "esbuild-plugin-alias";
|
||||||
// const __filename = fileURLToPath(import.meta.url); // get the resolved path to the file
|
const __filename = fileURLToPath(import.meta.url); // get the resolved path to the file
|
||||||
// const __dirname = path.dirname(__filename);
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
// build({
|
build({
|
||||||
// entryPoints: ["./src/**/*.ts"],
|
entryPoints: ["./src/**/*.ts"],
|
||||||
// // outfile: "./dist/index.js",
|
// outfile: "./dist/index.js",
|
||||||
// outdir: "./dist",
|
outdir: "./dist",
|
||||||
// bundle: true,
|
bundle: true,
|
||||||
// minify: false,
|
minify: false,
|
||||||
// platform: "node",
|
platform: "node",
|
||||||
// target: "esnext",
|
target: "esnext",
|
||||||
// format: "esm",
|
format: "esm",
|
||||||
// plugins: [
|
plugins: [
|
||||||
// alias({
|
alias({
|
||||||
// "@/server": path.resolve(__dirname, "src"),
|
"@dokploy/server": path.resolve(__dirname, "src"),
|
||||||
// }),
|
}),
|
||||||
// ],
|
],
|
||||||
// packages: "external",
|
packages: "external",
|
||||||
// // Opcional: si deseas emitir declaraciones de tipos con esbuild-plugin-dts
|
// Opcional: si deseas emitir declaraciones de tipos con esbuild-plugin-dts
|
||||||
// })
|
})
|
||||||
// .then(() => {
|
.then(() => {
|
||||||
// console.log("Build successful");
|
console.log("Build successful");
|
||||||
// })
|
})
|
||||||
// .catch(() => process.exit(1));
|
.catch(() => process.exit(1));
|
||||||
|
|||||||
@@ -1,14 +1,42 @@
|
|||||||
{
|
{
|
||||||
"name": "@dokploy/server",
|
"name": "@dokploy/server",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"main": "dist/index.js",
|
"main": "./src/index.ts",
|
||||||
"types": "dist/index.d.ts",
|
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"exports": {
|
||||||
|
".": "./src/index.ts",
|
||||||
|
"./db": {
|
||||||
|
"import": "./src/db/index.ts",
|
||||||
|
"require": "./dist/db/index.cjs.js"
|
||||||
|
},
|
||||||
|
"./setup/*": {
|
||||||
|
"import": "./src/setup/*.ts",
|
||||||
|
"require": "./dist/setup/index.cjs.js"
|
||||||
|
},
|
||||||
|
"./constants": {
|
||||||
|
"import": "./src/constants/index.ts",
|
||||||
|
"require": "./dist/constants.cjs.js"
|
||||||
|
},
|
||||||
|
"./dist": {
|
||||||
|
"import": "./dist/index.js",
|
||||||
|
"require": "./dist/index.cjs.js"
|
||||||
|
},
|
||||||
|
"./dist/db": {
|
||||||
|
"import": "./dist/db/index.js",
|
||||||
|
"require": "./dist/db/*.cjs"
|
||||||
|
},
|
||||||
|
"./dist/db/schema": {
|
||||||
|
"import": "./dist/db/schema/index.js",
|
||||||
|
"require": "./dist/db/schema/*.cjs"
|
||||||
|
}
|
||||||
|
},
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "rm -rf ./dist && tsc --project tsconfig.server.json && tsc-alias -p tsconfig.server.json",
|
"build": "rm -rf ./dist && tsc --project tsconfig.server.json && tsc-alias -p tsconfig.server.json",
|
||||||
"build:types": "tsc --emitDeclarationOnly --experimenta-dts",
|
"build:types": "tsc --emitDeclarationOnly --experimenta-dts",
|
||||||
|
"switch:dev": "node scripts/switchToSrc.js",
|
||||||
|
"switch:prod": "node scripts/switchToDist.js",
|
||||||
"dev": "rm -rf ./dist && pnpm esbuild && tsc --emitDeclarationOnly --outDir dist -p tsconfig.server.json",
|
"dev": "rm -rf ./dist && pnpm esbuild && tsc --emitDeclarationOnly --outDir dist -p tsconfig.server.json",
|
||||||
"esbuild": "tsx ./esbuild.config.ts",
|
"esbuild": "tsx ./esbuild.config.ts && tsc --project tsconfig.server.json --emitDeclarationOnly ",
|
||||||
"typecheck": "tsc --noEmit"
|
"typecheck": "tsc --noEmit"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
@@ -49,6 +77,7 @@
|
|||||||
"ssh2": "1.15.0"
|
"ssh2": "1.15.0"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"esbuild-plugin-alias": "0.2.1",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^3.4.1",
|
||||||
"tsx": "^4.7.1",
|
"tsx": "^4.7.1",
|
||||||
"tsc-alias": "1.8.10",
|
"tsc-alias": "1.8.10",
|
||||||
|
|||||||
27
packages/server/scripts/switchToDist.js
Normal file
27
packages/server/scripts/switchToDist.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
const packagePath = path.resolve(__dirname, "../package.json");
|
||||||
|
const pkg = JSON.parse(fs.readFileSync(packagePath, "utf-8"));
|
||||||
|
|
||||||
|
pkg.exports = {
|
||||||
|
".": {
|
||||||
|
import: "./dist/index.js",
|
||||||
|
require: "./dist/index.cjs.js",
|
||||||
|
},
|
||||||
|
"./db": {
|
||||||
|
import: "./dist/db/index.js",
|
||||||
|
require: "./dist/db/index.cjs.js",
|
||||||
|
},
|
||||||
|
"./*": {
|
||||||
|
import: "./dist/*",
|
||||||
|
require: "./dist/*.cjs",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
fs.writeFileSync(packagePath, JSON.stringify(pkg, null, 2));
|
||||||
|
console.log("Switched exports to use dist for production");
|
||||||
45
packages/server/scripts/switchToSrc.js
Normal file
45
packages/server/scripts/switchToSrc.js
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import fs from "node:fs";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
|
const __filename = fileURLToPath(import.meta.url);
|
||||||
|
const __dirname = path.dirname(__filename);
|
||||||
|
|
||||||
|
const packagePath = path.resolve(__dirname, "../package.json");
|
||||||
|
|
||||||
|
// Leer el archivo package.json
|
||||||
|
const pkg = JSON.parse(fs.readFileSync(packagePath, "utf-8"));
|
||||||
|
|
||||||
|
// Modificar los exports
|
||||||
|
pkg.exports = {
|
||||||
|
".": "./src/index.ts",
|
||||||
|
"./db": {
|
||||||
|
import: "./src/db/index.ts",
|
||||||
|
require: "./dist/db/index.cjs.js",
|
||||||
|
},
|
||||||
|
"./setup/*": {
|
||||||
|
import: "./src/setup/*.ts",
|
||||||
|
require: "./dist/setup/index.cjs.js",
|
||||||
|
},
|
||||||
|
"./constants": {
|
||||||
|
import: "./src/constants/index.ts",
|
||||||
|
require: "./dist/constants.cjs.js",
|
||||||
|
},
|
||||||
|
"./dist": {
|
||||||
|
import: "./dist/index.js",
|
||||||
|
require: "./dist/index.cjs.js",
|
||||||
|
},
|
||||||
|
|
||||||
|
"./dist/db": {
|
||||||
|
import: "./dist/db/index.js",
|
||||||
|
require: "./dist/db/*.cjs",
|
||||||
|
},
|
||||||
|
"./dist/db/schema": {
|
||||||
|
import: "./dist/db/schema/index.js",
|
||||||
|
require: "./dist/db/schema/*.cjs",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
// Guardar los cambios en package.json
|
||||||
|
fs.writeFileSync(packagePath, JSON.stringify(pkg, null, 2));
|
||||||
|
console.log("Switched exports to use src for development");
|
||||||
@@ -1,7 +1,7 @@
|
|||||||
import { webcrypto } from "node:crypto";
|
import { webcrypto } from "node:crypto";
|
||||||
import type { IncomingMessage, ServerResponse } from "node:http";
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
||||||
import { findAdminByAuthId } from "@/server/services/admin";
|
import { findAdminByAuthId } from "@dokploy/server/services/admin";
|
||||||
import { findUserByAuthId } from "@/server/services/user";
|
import { findUserByAuthId } from "@dokploy/server/services/user";
|
||||||
import { DrizzlePostgreSQLAdapter } from "@lucia-auth/adapter-drizzle";
|
import { DrizzlePostgreSQLAdapter } from "@lucia-auth/adapter-drizzle";
|
||||||
import { TimeSpan } from "lucia";
|
import { TimeSpan } from "lucia";
|
||||||
import { Lucia } from "lucia/dist/core.js";
|
import { Lucia } from "lucia/dist/core.js";
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { relations } from "drizzle-orm";
|
import { relations } from "drizzle-orm";
|
||||||
import { boolean, pgTable, text } from "drizzle-orm/pg-core";
|
import { boolean, integer, pgTable, text } from "drizzle-orm/pg-core";
|
||||||
import { createInsertSchema } from "drizzle-zod";
|
import { createInsertSchema } from "drizzle-zod";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -28,6 +28,9 @@ export const admins = pgTable("admin", {
|
|||||||
createdAt: text("createdAt")
|
createdAt: text("createdAt")
|
||||||
.notNull()
|
.notNull()
|
||||||
.$defaultFn(() => new Date().toISOString()),
|
.$defaultFn(() => new Date().toISOString()),
|
||||||
|
stripeCustomerId: text("stripeCustomerId"),
|
||||||
|
stripeSubscriptionId: text("stripeSubscriptionId"),
|
||||||
|
serversQuantity: integer("serversQuantity").notNull().default(0),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const adminsRelations = relations(admins, ({ one, many }) => ({
|
export const adminsRelations = relations(admins, ({ one, many }) => ({
|
||||||
|
|||||||
@@ -48,6 +48,8 @@ export const auth = pgTable("auth", {
|
|||||||
createdAt: text("createdAt")
|
createdAt: text("createdAt")
|
||||||
.notNull()
|
.notNull()
|
||||||
.$defaultFn(() => new Date().toISOString()),
|
.$defaultFn(() => new Date().toISOString()),
|
||||||
|
resetPasswordToken: text("resetPasswordToken"),
|
||||||
|
resetPasswordExpiresAt: text("resetPasswordExpiresAt"),
|
||||||
});
|
});
|
||||||
|
|
||||||
export const authRelations = relations(auth, ({ many }) => ({
|
export const authRelations = relations(auth, ({ many }) => ({
|
||||||
|
|||||||
@@ -53,7 +53,10 @@ export const apiCreateDestination = createSchema
|
|||||||
endpoint: true,
|
endpoint: true,
|
||||||
secretAccessKey: true,
|
secretAccessKey: true,
|
||||||
})
|
})
|
||||||
.required();
|
.required()
|
||||||
|
.extend({
|
||||||
|
serverId: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
export const apiFindOneDestination = createSchema
|
export const apiFindOneDestination = createSchema
|
||||||
.pick({
|
.pick({
|
||||||
@@ -77,4 +80,7 @@ export const apiUpdateDestination = createSchema
|
|||||||
secretAccessKey: true,
|
secretAccessKey: true,
|
||||||
destinationId: true,
|
destinationId: true,
|
||||||
})
|
})
|
||||||
.required();
|
.required()
|
||||||
|
.extend({
|
||||||
|
serverId: z.string().optional(),
|
||||||
|
});
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { relations } from "drizzle-orm";
|
import { relations } from "drizzle-orm";
|
||||||
import { boolean, integer, pgTable, text } from "drizzle-orm/pg-core";
|
import { boolean, integer, pgEnum, pgTable, text } from "drizzle-orm/pg-core";
|
||||||
import { createInsertSchema } from "drizzle-zod";
|
import { createInsertSchema } from "drizzle-zod";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -17,6 +17,8 @@ import { redis } from "./redis";
|
|||||||
import { sshKeys } from "./ssh-key";
|
import { sshKeys } from "./ssh-key";
|
||||||
import { generateAppName } from "./utils";
|
import { generateAppName } from "./utils";
|
||||||
|
|
||||||
|
export const serverStatus = pgEnum("serverStatus", ["active", "inactive"]);
|
||||||
|
|
||||||
export const server = pgTable("server", {
|
export const server = pgTable("server", {
|
||||||
serverId: text("serverId")
|
serverId: text("serverId")
|
||||||
.notNull()
|
.notNull()
|
||||||
@@ -37,6 +39,8 @@ export const server = pgTable("server", {
|
|||||||
adminId: text("adminId")
|
adminId: text("adminId")
|
||||||
.notNull()
|
.notNull()
|
||||||
.references(() => admins.adminId, { onDelete: "cascade" }),
|
.references(() => admins.adminId, { onDelete: "cascade" }),
|
||||||
|
serverStatus: serverStatus("serverStatus").notNull().default("active"),
|
||||||
|
|
||||||
sshKeyId: text("sshKeyId").references(() => sshKeys.sshKeyId, {
|
sshKeyId: text("sshKeyId").references(() => sshKeys.sshKeyId, {
|
||||||
onDelete: "set null",
|
onDelete: "set null",
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -37,7 +37,6 @@ export * from "./services/application";
|
|||||||
export * from "./setup/config-paths";
|
export * from "./setup/config-paths";
|
||||||
export * from "./setup/postgres-setup";
|
export * from "./setup/postgres-setup";
|
||||||
export * from "./setup/redis-setup";
|
export * from "./setup/redis-setup";
|
||||||
export * from "./setup/registry-setup";
|
|
||||||
export * from "./setup/server-setup";
|
export * from "./setup/server-setup";
|
||||||
export * from "./setup/setup";
|
export * from "./setup/setup";
|
||||||
export * from "./setup/traefik-setup";
|
export * from "./setup/traefik-setup";
|
||||||
@@ -97,7 +96,6 @@ export * from "./utils/traefik/domain";
|
|||||||
export * from "./utils/traefik/file-types";
|
export * from "./utils/traefik/file-types";
|
||||||
export * from "./utils/traefik/middleware";
|
export * from "./utils/traefik/middleware";
|
||||||
export * from "./utils/traefik/redirect";
|
export * from "./utils/traefik/redirect";
|
||||||
export * from "./utils/traefik/registry";
|
|
||||||
export * from "./utils/traefik/security";
|
export * from "./utils/traefik/security";
|
||||||
export * from "./utils/traefik/types";
|
export * from "./utils/traefik/types";
|
||||||
export * from "./utils/traefik/web-server";
|
export * from "./utils/traefik/web-server";
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user