Compare commits

...

84 Commits

Author SHA1 Message Date
Mauricio Siu
1a6bf06929 Merge pull request #606 from PaiJi/chore/i18n-zh-hans
feat(i18n): add zh-Hans for Pricing and Sponsor
2024-10-27 02:05:24 -06:00
JiPai
ecc98b0944 feat(i18n): add zh-Hans for Pricing page 2024-10-27 15:38:22 +08:00
JiPai
950ee77a49 feat(i18n): add zh-Hans translation for homepage 2024-10-27 14:32:23 +08:00
Mauricio Siu
38a72806a7 refactor(dokploy): add webhook url for new users 2024-10-26 23:06:00 -06:00
Mauricio Siu
fa70696c71 refactor(website): update const value 2024-10-26 22:46:15 -06:00
Mauricio Siu
b123baafa4 refactor(dokploy): add analytics and pricing section to website 2024-10-26 22:44:21 -06:00
Mauricio Siu
d6fa416a3f chore(ci/cd): set secrets 2024-10-26 20:31:17 -06:00
Mauricio Siu
d2c0f19353 refactor(dokploy): add missing import 2024-10-26 19:56:34 -06:00
Mauricio Siu
3ef6c711e6 chore(ci/cd): set live key 2024-10-26 19:51:23 -06:00
Mauricio Siu
64a4d51f9c Merge pull request #605 from Dokploy/feat/add-email-verification
Feat/add email verification
2024-10-26 19:39:59 -06:00
Mauricio Siu
1a20e4f813 refactor: disable endpoints from cloud in self hosted 2024-10-26 19:35:49 -06:00
Mauricio Siu
250c14738c refactor: remove register when is self hosted 2024-10-26 19:32:29 -06:00
Mauricio Siu
28221a4e7a refactor(dokploy): add umami and update favicon 2024-10-26 19:27:01 -06:00
Mauricio Siu
680c22a41e feat(dokploy): add verification email to cloud version 2024-10-26 18:09:55 -06:00
Mauricio Siu
428b9cf0ec Merge pull request #603 from Dokploy/fix/start-schedules-and-server
fix(server): update build paths
2024-10-26 16:24:12 -06:00
Mauricio Siu
78659b2ad9 chore: exclude package.json to format 2024-10-26 16:17:20 -06:00
Mauricio Siu
4b5408c050 chore: lint 2024-10-26 16:04:43 -06:00
Mauricio Siu
5417f6376b chore(server): update switch prod in build command 2024-10-26 15:56:03 -06:00
Mauricio Siu
27c33c7661 fix(server): update build paths 2024-10-26 15:48:30 -06:00
Mauricio Siu
0873618c63 chore(dokploy): set latest tag in cloud 2024-10-26 15:08:49 -06:00
Mauricio Siu
e3b20268d5 refactor(dokploy): add ssr for settings to show instantly the sidebar 2024-10-25 23:56:33 -06:00
Mauricio Siu
742e5a244d refactor(dokploy): prevent to create projects when the user doesn't have servers buyed 2024-10-25 23:44:39 -06:00
Mauricio Siu
1f208f5f5b refactor(dokploy): add next public shareable key 2024-10-25 22:53:58 -06:00
Mauricio Siu
a53ade88e7 refactor(dokploy): add SITE_URL 2024-10-25 22:35:35 -06:00
Mauricio Siu
77e9617770 chore(version): bump version 2024-10-25 21:18:04 -06:00
Mauricio Siu
21e97b0175 Merge pull request #598 from Dokploy/593-use-advertise_addr-from-environment-variable-in-dokpoy-service
feat(dokploy): add env for ADVERTISE_ADDR to installation #593
2024-10-25 20:50:58 -06:00
Mauricio Siu
a6618a14d5 feat(dokploy): add env for ADVERTISE_ADDR to installation #593 2024-10-25 20:48:46 -06:00
Mauricio Siu
59308ab013 Merge pull request #596 from Dokploy/595-dokploy-internal-postgres-database-is-publicly-accessible-by-default
fix(dokploy): remove expose ports in production
2024-10-25 20:19:14 -06:00
Mauricio Siu
e19c8d7a7a fix(dokploy): remove expose ports in production 2024-10-25 17:09:06 -06:00
Mauricio Siu
421c93795b refactor(dokploy): prevent start worker when is cloud 2024-10-25 16:56:32 -06:00
Mauricio Siu
182f908c31 chore: add new sponsor 2024-10-25 10:20:56 -06:00
Mauricio Siu
20616363e9 refactor: update badge server 2024-10-25 00:39:34 -06:00
Mauricio Siu
d85073b26d Merge pull request #591 from Dokploy/feat/remove-build-on-server
Feat/remove build on server
2024-10-25 00:23:56 -06:00
Mauricio Siu
303d1b1b87 chore: add missing command 2024-10-25 00:21:12 -06:00
Mauricio Siu
60d4e1ba63 chore: update dockerfiles 2024-10-25 00:17:10 -06:00
Mauricio Siu
83d52b68f0 refactor: update 2024-10-25 00:05:55 -06:00
Mauricio Siu
af3b1a27f4 refactor: update tests 2024-10-24 23:57:40 -06:00
Mauricio Siu
7f94593c07 chore: revert ci/cd 2024-10-24 23:35:21 -06:00
Mauricio Siu
5df7654873 chore: update imports 2024-10-24 23:33:15 -06:00
Mauricio Siu
054836fd4c chore: update workflows 2024-10-24 23:28:52 -06:00
Mauricio Siu
484ead1f1f chore: update workflows 2024-10-24 23:26:06 -06:00
Mauricio Siu
fbada4c5de chore: set right filter pnpm dockerfile 2024-10-24 23:22:23 -06:00
Mauricio Siu
491113416b chore: remove server build 2024-10-24 23:20:51 -06:00
Mauricio Siu
c42f5cb799 refactor: update 2024-10-24 23:18:23 -06:00
Mauricio Siu
47aa223f87 refactor: remove save on build on next app and integrate turbopack 2024-10-24 23:13:24 -06:00
Mauricio Siu
cb586c9b74 Merge branch 'canary' into feat/payments 2024-10-24 13:50:17 -06:00
Mauricio Siu
554ac59b97 Merge pull request #589 from alok-debnath/typo-fixes
Typo fixes in README.md
2024-10-24 13:43:28 -06:00
Alok Debnath
0247898876 minor typo fixes in README.md for better readability 2024-10-25 00:06:09 +05:30
Mauricio Siu
fc2778db35 Merge pull request #585 from Dokploy/505-mongodb-backup-is-empty-in-output-pipeline
fix(dokploy): add missing --archive to mongodump
2024-10-24 01:14:10 -06:00
Mauricio Siu
85d6ff9012 chore(version): bump version 2024-10-24 01:11:43 -06:00
Mauricio Siu
fa053b4d1f refactor(dokploy): remove stripe from global scope 2024-10-24 01:10:17 -06:00
Mauricio Siu
522f8baec7 fix(dokploy): add missing --archive to mongodump 2024-10-24 01:07:02 -06:00
Mauricio Siu
bcc7afa3e4 refactor(dokploy): fix ts errors 2024-10-24 00:55:59 -06:00
Mauricio Siu
647a5d05a6 test(dokploy): add missing fields 2024-10-24 00:55:03 -06:00
Mauricio Siu
e15d41f80d chore: add radix tabs 2024-10-24 00:22:57 -06:00
Mauricio Siu
6c7c919d49 refactor: set servers quantity in 0 when the subscription is created 2024-10-24 00:21:28 -06:00
Mauricio Siu
22eb965919 Merge pull request #584 from dmbr0/patch-2
Fix typo in README.md
2024-10-23 20:08:37 -06:00
Alex Whitney
c0746b95b3 Fix typo in README.md 2024-10-23 20:18:59 -04:00
Mauricio Siu
dfdedf9e48 chore(dokploy): simplify migrations 2024-10-23 01:16:42 -06:00
Mauricio Siu
7e76eb4dd1 refactor: delete log 2024-10-23 01:06:34 -06:00
Mauricio Siu
c1f777e23e refactor: remove serverIp 2024-10-23 01:01:04 -06:00
Mauricio Siu
975d13c7e1 refactor(dokploy): disable stats monitoring 2024-10-23 00:58:38 -06:00
Mauricio Siu
017bdd2778 refactor(dokploy): add flag to prevent run commands when is cloud 2024-10-23 00:54:40 -06:00
Mauricio Siu
548df8c0f4 Merge branch 'canary' into feat/payments 2024-10-22 20:31:18 -06:00
Mauricio Siu
1e6dbb5e8e feat(dokploy): add reset password for cloud 2024-10-22 01:25:13 -06:00
Mauricio Siu
431dadb6c2 feat: add pricing 2024-10-22 00:29:17 -06:00
Mauricio Siu
22e42b62ad feat: add is cloud in ssr 2024-10-21 22:38:23 -06:00
Mauricio Siu
49ee8ce132 Merge branch 'canary' into feat/payments 2024-10-21 22:22:59 -06:00
Mauricio Siu
9bace8e58b refactor: clean stripe customer if the customer is deleted 2024-10-21 13:01:08 -06:00
Mauricio Siu
c0afcaf3f6 refactor: show banner when the server is disabled 2024-10-21 02:19:10 -06:00
Mauricio Siu
53edf06476 refactor: remove comments 2024-10-21 00:48:04 -06:00
Mauricio Siu
255e9e4095 refactor: remove dokploy restart on notifications 2024-10-21 00:46:33 -06:00
Mauricio Siu
03f923c6e2 refactor: update show servers 2024-10-21 00:43:04 -06:00
Mauricio Siu
4685ef7439 refactor: update stripe envs 2024-10-21 00:39:14 -06:00
Mauricio Siu
626cfb80b4 refactor: add website url redirect 2024-10-21 00:37:11 -06:00
Mauricio Siu
9591fbff08 refactor: add Is cloud flag 2024-10-21 00:34:59 -06:00
Mauricio Siu
fbda00f059 refactor: update webhooks and added validation to prevent deploy when the server is inactive 2024-10-21 00:34:16 -06:00
Mauricio Siu
1907e7e59c feat: add webhook 2024-10-20 23:08:26 -06:00
Mauricio Siu
ffe7b04bea feat: add stripe webhooks 2024-10-20 15:08:44 -06:00
Mauricio Siu
fe0a662afd feat(cloud): add billing wip 2024-10-20 00:14:27 -06:00
Mauricio Siu
319584d911 Merge branch 'canary' into feat/payments 2024-10-18 21:59:12 -06:00
Mauricio Siu
2d40b2dfe5 feat: add stripe integration 2024-10-18 01:43:45 -06:00
Mauricio Siu
e32b713742 Merge branch 'canary' into feat/payments 2024-10-18 01:25:00 -06:00
Mauricio Siu
988357fb55 refactor: update payments 2024-10-17 21:52:24 -06:00
209 changed files with 13535 additions and 2138 deletions

View File

@@ -70,8 +70,12 @@ jobs:
file: ./Dockerfile.cloud
push: true
tags: |
siumauricio/cloud:${{ github.ref_name == 'main' && 'main' || 'canary' }}
siumauricio/cloud:${{ github.ref_name == 'main' && 'latest' || 'canary' }}
platforms: linux/amd64
build-args: |
NEXT_PUBLIC_UMAMI_HOST=${{ secrets.NEXT_PUBLIC_UMAMI_HOST }}
NEXT_PUBLIC_UMAMI_WEBSITE_ID=${{ secrets.NEXT_PUBLIC_UMAMI_WEBSITE_ID }}
NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=${{ secrets.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY }}
build-and-push-schedule-image:
runs-on: ubuntu-latest
@@ -93,7 +97,7 @@ jobs:
file: ./Dockerfile.schedule
push: true
tags: |
siumauricio/schedule:${{ github.ref_name == 'main' && 'main' || 'canary' }}
siumauricio/schedule:${{ github.ref_name == 'main' && 'latest' || 'canary' }}
platforms: linux/amd64
@@ -117,5 +121,5 @@ jobs:
file: ./Dockerfile.server
push: true
tags: |
siumauricio/server:${{ github.ref_name == 'main' && 'main' || 'canary' }}
siumauricio/server:${{ github.ref_name == 'main' && 'latest' || 'canary' }}
platforms: linux/amd64

View File

@@ -71,10 +71,9 @@ Run the command that will spin up all the required services and files.
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
pnpm run server:build
pnpm run server:script
```
Now run the development server.

View File

@@ -12,7 +12,16 @@ RUN apt-get update && apt-get install -y python3 make g++ git && rm -rf /var/lib
# Install dependencies
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --filter=@dokploy/server --filter=./apps/dokploy install --frozen-lockfile
# Deploy only the dokploy app
ARG NEXT_PUBLIC_UMAMI_HOST
ENV NEXT_PUBLIC_UMAMI_HOST=$NEXT_PUBLIC_UMAMI_HOST
ARG NEXT_PUBLIC_UMAMI_WEBSITE_ID
ENV NEXT_PUBLIC_UMAMI_WEBSITE_ID=$NEXT_PUBLIC_UMAMI_WEBSITE_ID
ARG NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
ENV NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY=$NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY
ENV NODE_ENV=production
RUN pnpm --filter=@dokploy/server build

View File

@@ -15,29 +15,29 @@
</div>
</div>
<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
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.).
- **Databases**: Create and manage databases with support for MySQL, PostgreSQL, MongoDB, MariaDB, Redis.
- **Backups**: Automate backups for databases to a external storage destination.
- **Databases**: Create and manage databases with support for MySQL, PostgreSQL, MongoDB, MariaDB, and Redis.
- **Backups**: Automate backups for databases to an external storage destination.
- **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.
- **Templates**: Deploy in a single click open source templates (Plausible, Pocketbase, Calcom, etc.).
- **Multi Node**: Scale applications to multiple nodes using Docker Swarm to manage the cluster.
- **Templates**: Deploy open-source templates (Plausible, Pocketbase, Calcom, etc.) with a single click.
- **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.
- **CLI/API**: Manage your applications and databases using the command line or trought the API.
- **Notifications**: Get notified when your deployments are successful or failed (Slack, Discord, Telegram, Email, etc.)
- **Multi Server**: Deploy and manager your applications remotely to external servers.
- **CLI/API**: Manage your applications and databases using the command line or through the API.
- **Notifications**: Get notified when your deployments succeed or fail (via Slack, Discord, Telegram, Email, etc.).
- **Multi Server**: Deploy and manage your applications remotely to external servers.
- **Self-Hosted**: Self-host Dokploy on your VPS.
## 🚀 Getting Started
To get started run the following command in a VPS:
To get started, run the following command on a VPS:
```bash
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;">
<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://photoquest.wedding/?ref=dokploy"><img src="https://photoquest.wedding/favicon/android-chrome-512x512.png" width="60px" alt="Rivo.gg"/></a>
</div>
#### Organizations:

View File

@@ -8,7 +8,7 @@ export const deployJobSchema = z.discriminatedUnion("applicationType", [
server: z.boolean().optional(),
type: z.enum(["deploy", "redeploy"]),
applicationType: z.literal("application"),
serverId: z.string(),
serverId: z.string().min(1),
}),
z.object({
composeId: z.string(),
@@ -17,7 +17,7 @@ export const deployJobSchema = z.discriminatedUnion("applicationType", [
server: z.boolean().optional(),
type: z.enum(["deploy", "redeploy"]),
applicationType: z.literal("compose"),
serverId: z.string(),
serverId: z.string().min(1),
}),
]);

View File

@@ -1,16 +0,0 @@
export interface LemonSqueezyLicenseResponse {
valid: boolean;
error?: string;
meta?: {
store_id: string;
order_id: number;
order_item_id: number;
product_id: number;
product_name: string;
variant_id: number;
variant_name: string;
customer_id: number;
customer_name: string;
customer_email: string;
};
}

View File

@@ -1,45 +1,12 @@
import {
deployApplication,
deployCompose,
deployRemoteApplication,
deployRemoteCompose,
rebuildApplication,
rebuildCompose,
rebuildRemoteApplication,
rebuildRemoteCompose,
updateApplicationStatus,
updateCompose,
} from "@dokploy/server";
import type { DeployJob } from "./schema";
import type { LemonSqueezyLicenseResponse } from "./types";
// const LEMON_SQUEEZY_API_KEY = process.env.LEMON_SQUEEZY_API_KEY;
// const LEMON_SQUEEZY_STORE_ID = process.env.LEMON_SQUEEZY_STORE_ID;
// export const validateLemonSqueezyLicense = async (
// licenseKey: string,
// ): Promise<LemonSqueezyLicenseResponse> => {
// try {
// const response = await fetch(
// "https://api.lemonsqueezy.com/v1/licenses/validate",
// {
// method: "POST",
// headers: {
// "Content-Type": "application/json",
// "x-api-key": LEMON_SQUEEZY_API_KEY as string,
// },
// body: JSON.stringify({
// license_key: licenseKey,
// store_id: LEMON_SQUEEZY_STORE_ID as string,
// }),
// },
// );
// return response.json();
// } catch (error) {
// console.error("Error validating license:", error);
// return { valid: false, error: "Error validating license" };
// }
// };
export const deploy = async (job: DeployJob) => {
try {

View File

@@ -14,6 +14,7 @@ import {
PlugZapIcon,
TerminalIcon,
} from "lucide-react";
import Script from "next/script";
const inter = Inter({
subsets: ["latin"],
});
@@ -63,6 +64,10 @@ export default function Layout({
className={inter.className}
suppressHydrationWarning
>
<Script
src="https://umami.dokploy.com/script.js"
data-website-id="6ad2aa56-6d38-4f39-97a8-1a8fcdda8d51"
/>
<GoogleAnalytics />
<body>
<I18nProvider

View File

@@ -15,13 +15,13 @@ description: '学习如何在服务器上手动安装 Dokploy。'
```bash
#!/bin/bash
# 确保以根用户身份运行脚本
# Ensure the script is run as root
if [ "$(id -u)" != "0" ]; then
echo "This script must be run as root" >&2
exit 1
fi
# 检查 Linux 操作系统(非 macOS 或 Docker 容器内的操作系统)
# Check for Linux OS (not macOS or inside a Docker container)
if [ "$(uname)" = "Darwin" ]; then
echo "This script must be run on Linux" >&2
exit 1
@@ -32,7 +32,7 @@ if [ -f /.dockerenv ]; then
exit 1
fi
# 检查端口是否被占用
# Check for occupied ports
if ss -tulnp | grep ':80 ' >/dev/null; then
echo "Error: Port 80 is already in use" >&2
exit 1
@@ -43,32 +43,53 @@ if ss -tulnp | grep ':443 ' >/dev/null; then
exit 1
fi
# 检查命令是否存在
# Function to check if a command exists
command_exists() {
command -v "$@" > /dev/null 2>&1
}
# 如果未安装 Docker 则安装
# Install Docker if it is not installed
if command_exists docker; then
echo "Docker already installed"
else
curl -sSL https://get.docker.com | sh
fi
# 初始化 Docker Swarm
# Initialize Docker Swarm
docker swarm leave --force 2>/dev/null
advertise_addr=$(curl -s ifconfig.me)
docker swarm init --advertise-addr $advertise_addr
echo "Swarm initialized"
# 创建网络
docker network rm -f dokploy-network 2>/dev/null
docker network create --driver overlay --attachable dokploy-network
echo "Network created"
get_ip() {
# Try to get IPv4
local ipv4=$(curl -4s https://ifconfig.io 2>/dev/null)
# 准备配置目录
mkdir -p /etc/dokploy
chmod -R 777 /etc/dokploy
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
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
docker pull dokploy/dokploy:latest
@@ -84,9 +105,10 @@ docker service create \
-e PORT=<Value For PORT eg(3000)> \
-e TRAEFIK_SSL_PORT=<Value For SSL PORT eg(444)> \
-e TRAEFIK_PORT=<VALUE FOR TRAEFIK HTTP PORT eg(81)> \
-e ADVERTISE_ADDR=$advertise_addr \
dokploy/dokploy:latest
# 输出成功消息
# Output success message
GREEN="\033[0;32m"
YELLOW="\033[1;33m"
BLUE="\033[0;34m"

View File

@@ -57,18 +57,39 @@ fi
# Initialize Docker Swarm
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
docker network rm -f dokploy-network 2>/dev/null
docker network create --driver overlay --attachable dokploy-network
echo "Network created"
get_ip() {
# Try to get IPv4
local ipv4=$(curl -4s https://ifconfig.io 2>/dev/null)
# Prepare configuration directory
mkdir -p /etc/dokploy
chmod -R 777 /etc/dokploy
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
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
docker pull dokploy/dokploy:latest
@@ -84,6 +105,7 @@ docker service create \
-e PORT=<Value For PORT eg(3000)> \
-e TRAEFIK_SSL_PORT=<Value For SSL PORT eg(444)> \
-e TRAEFIK_PORT=<VALUE FOR TRAEFIK HTTP PORT eg(81)> \
-e ADVERTISE_ADDR=$advertise_addr \
dokploy/dokploy:latest
# Output success message

View File

@@ -1,12 +1,23 @@
import fs from "node:fs/promises";
import path from "node:path";
import { paths } from "@dokploy/server/dist/constants";
import { paths } from "@dokploy/server/constants";
const { APPLICATIONS_PATH } = paths();
import type { ApplicationNested } from "@dokploy/server";
import { unzipDrop } from "@dokploy/server";
import AdmZip from "adm-zip";
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") {
const undici = require("undici");
globalThis.File = undici.File as any;
@@ -82,16 +93,6 @@ const baseApp: ApplicationNested = {
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", () => {
// const { APPLICATIONS_PATH } = paths();
beforeAll(async () => {

View File

@@ -24,6 +24,9 @@ const baseAdmin: Admin = {
sshPrivateKey: null,
enableDockerCleanup: false,
enableLogRotation: false,
serversQuantity: 0,
stripeCustomerId: "",
stripeSubscriptionId: "",
};
beforeEach(() => {

View File

@@ -1,13 +1,8 @@
import path from "node:path";
import tsconfigPaths from "vite-tsconfig-paths";
import { defineConfig } from "vitest/config";
export default defineConfig({
plugins: [
tsconfigPaths({
root: "./",
projects: ["tsconfig.json"],
}),
],
test: {
include: ["__test__/**/*.test.ts"], // Incluir solo los archivos de test en el directorio __test__
exclude: ["**/node_modules/**", "**/dist/**", "**/.docker/**"],
@@ -18,4 +13,12 @@ export default defineConfig({
NODE: "test",
},
},
resolve: {
alias: {
"@dokploy/server": path.resolve(
__dirname,
"../../../packages/server/src",
),
},
},
});

View File

@@ -0,0 +1,288 @@
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,
Loader2,
MinusIcon,
PlusIcon,
} from "lucide-react";
import Link from "next/link";
import React, { useState } from "react";
const stripePromise = loadStripe(
process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY || "",
);
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;
};
export const ShowBilling = () => {
const { data: servers } = api.server.all.useQuery(undefined);
const { data: admin } = api.admin.one.useQuery();
const { data, isLoading } = 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>
)}
<div className="flex flex-col gap-1.5 mt-4">
<span className="text-base text-primary">
Need Help? We are here to help you.
</span>
<span className="text-sm text-muted-foreground">
Join to our Discord server and we will help you.
</span>
<Button className="rounded-full bg-[#5965F2] hover:bg-[#4A55E0] w-fit">
<Link
href="https://discord.gg/2tBnJ3jDJc"
aria-label="Dokploy on GitHub"
target="_blank"
className="flex flex-row items-center gap-2 text-white"
>
<svg
role="img"
className="h-6 w-6 fill-white"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z" />
</svg>
Join Discord
</Link>
</Button>
</div>
{isLoading ? (
<span className="text-base text-muted-foreground flex flex-row gap-3 items-center justify-center min-h-[10vh]">
Loading...
<Loader2 className="animate-spin" />
</span>
) : (
<>
{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>
);
};

View File

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

View File

@@ -8,7 +8,6 @@ import {
import { api } from "@/utils/api";
import { Server } from "lucide-react";
import { AddRegistry } from "./add-docker-registry";
import { AddSelfHostedRegistry } from "./add-self-docker-registry";
import { DeleteRegistry } from "./delete-registry";
import { UpdateDockerRegistry } from "./update-docker-registry";
@@ -31,8 +30,6 @@ export const ShowRegistry = () => {
<div className="flex flex-row gap-2">
{data && data?.length > 0 && (
<>
{!haveSelfHostedRegistry && <AddSelfHostedRegistry />}
<AddRegistry />
</>
)}
@@ -47,7 +44,6 @@ export const ShowRegistry = () => {
</span>
<div className="flex flex-row md:flex-row gap-2 flex-wrap w-full justify-center">
<AddSelfHostedRegistry />
<AddRegistry />
</div>
</div>

View File

@@ -18,6 +18,16 @@ import {
FormMessage,
} from "@/components/ui/form";
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 { zodResolver } from "@hookform/resolvers/zod";
import { useEffect } from "react";
@@ -32,12 +42,15 @@ const addDestination = z.object({
bucket: z.string(),
region: z.string(),
endpoint: z.string(),
serverId: z.string().optional(),
});
type AddDestination = z.infer<typeof addDestination>;
export const AddDestination = () => {
const utils = api.useUtils();
const { data: servers } = api.server.withSSHKey.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery();
const { mutateAsync, isError, error, isLoading } =
api.destination.create.useMutation();
@@ -189,30 +202,106 @@ export const AddDestination = () => {
/>
</form>
<DialogFooter className="flex w-full flex-row !justify-between pt-3">
<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"),
})
.then(async () => {
toast.success("Connection Success");
<DialogFooter
className={cn(
isCloud ? "!flex-col" : "flex-row",
"flex w-full !justify-between pt-3 gap-4",
)}
>
{isCloud ? (
<div className="flex flex-col gap-4 border p-2 rounded-lg">
<span className="text-sm text-muted-foreground">
Select a server to test the destination. If you don't have a
server choose the default one.
</span>
<FormField
control={form.control}
name="serverId"
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(() => {
toast.error("Error to connect the provider");
});
}}
>
Test connection
</Button>
.then(async () => {
toast.success("Connection Success");
})
.catch(() => {
toast.error("Error to connect the provider");
});
}}
>
Test connection
</Button>
)}
<Button
isLoading={isLoading}
form="hook-form-destination-add"

View File

@@ -18,6 +18,16 @@ import {
FormMessage,
} from "@/components/ui/form";
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 { zodResolver } from "@hookform/resolvers/zod";
import { PenBoxIcon } from "lucide-react";
@@ -33,6 +43,7 @@ const updateDestination = z.object({
bucket: z.string(),
region: z.string(),
endpoint: z.string(),
serverId: z.string().optional(),
});
type UpdateDestination = z.infer<typeof updateDestination>;
@@ -43,6 +54,8 @@ interface Props {
export const UpdateDestination = ({ destinationId }: Props) => {
const utils = api.useUtils();
const { data: servers } = api.server.withSSHKey.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery();
const [isOpen, setIsOpen] = useState(false);
const { data, refetch } = api.destination.one.useQuery(
{
@@ -220,34 +233,107 @@ export const UpdateDestination = ({ destinationId }: Props) => {
</div>
</form>
<DialogFooter className="flex w-full flex-row !justify-between pt-3">
<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"),
})
.then(async () => {
toast.success("Connection Success");
<DialogFooter
className={cn(
isCloud ? "!flex-col" : "flex-row",
"flex w-full !justify-between pt-3 gap-4",
)}
>
{isCloud ? (
<div className="flex flex-col gap-4 border p-2 rounded-lg">
<span className="text-sm text-muted-foreground">
Select a server to test the destination. If you don't have a
server choose the default one.
</span>
<FormField
control={form.control}
name="serverId"
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(() => {
toast.error("Error to connect the provider");
});
}}
>
Test connection
</Button>
.then(async () => {
toast.success("Connection Success");
})
.catch(() => {
toast.error("Error to connect the provider");
});
}}
>
Test connection
</Button>
)}
<Button
isLoading={form.formState.isSubmitting}
form="hook-form"
type="submit"
isLoading={form.formState.isSubmitting}
>
Update
</Button>

View File

@@ -109,7 +109,7 @@ export type NotificationSchema = z.infer<typeof notificationSchema>;
export const AddNotification = () => {
const utils = api.useUtils();
const [visible, setVisible] = useState(false);
const { data: isCloud } = api.settings.isCloud.useQuery();
const { mutateAsync: testSlackConnection, isLoading: isLoadingSlack } =
api.notification.testSlackConnection.useMutation();
@@ -660,26 +660,28 @@ export const AddNotification = () => {
)}
/>
<FormField
control={form.control}
name="dokployRestart"
render={({ field }) => (
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
<div className="space-y-0.5">
<FormLabel>Dokploy Restart</FormLabel>
<FormDescription>
Trigger the action when a dokploy is restarted.
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
{!isCloud && (
<FormField
control={form.control}
name="dokployRestart"
render={({ field }) => (
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
<div className="space-y-0.5">
<FormLabel>Dokploy Restart</FormLabel>
<FormDescription>
Trigger the action when a dokploy is restarted.
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
)}
</div>
</div>
</form>

View File

@@ -63,7 +63,7 @@ export const UpdateNotification = ({ notificationId }: Props) => {
const telegramMutation = api.notification.updateTelegram.useMutation();
const discordMutation = api.notification.updateDiscord.useMutation();
const emailMutation = api.notification.updateEmail.useMutation();
const { data: isCloud } = api.settings.isCloud.useQuery();
const form = useForm<NotificationSchema>({
defaultValues: {
type: "slack",
@@ -618,27 +618,29 @@ export const UpdateNotification = ({ notificationId }: Props) => {
</FormItem>
)}
/>
<FormField
control={form.control}
defaultValue={form.control._defaultValues.dokployRestart}
name="dokployRestart"
render={({ field }) => (
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
<div className="space-y-0.5">
<FormLabel>Dokploy Restart</FormLabel>
<FormDescription>
Trigger the action when a dokploy is restarted.
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
{!isCloud && (
<FormField
control={form.control}
defaultValue={form.control._defaultValues.dokployRestart}
name="dokployRestart"
render={({ field }) => (
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
<div className="space-y-0.5">
<FormLabel>Dokploy Restart</FormLabel>
<FormDescription>
Trigger the action when a dokploy is restarted.
</FormDescription>
</div>
<FormControl>
<Switch
checked={field.value}
onCheckedChange={field.onChange}
/>
</FormControl>
</FormItem>
)}
/>
)}
</div>
</div>
</form>

View File

@@ -52,8 +52,6 @@ export const ProfileForm = () => {
const { data, refetch } = api.auth.get.useQuery();
const { mutateAsync, isLoading } = api.auth.update.useMutation();
const { mutateAsync: generateToken, isLoading: isLoadingToken } =
api.auth.generateToken.useMutation();
const form = useForm<Profile>({
defaultValues: {
email: data?.email || "",
@@ -76,7 +74,7 @@ export const ProfileForm = () => {
const onSubmit = async (values: Profile) => {
await mutateAsync({
email: values.email,
email: values.email.toLowerCase(),
password: values.password,
image: values.image,
})

View File

@@ -31,6 +31,7 @@ import { Textarea } from "@/components/ui/textarea";
import { api } from "@/utils/api";
import { zodResolver } from "@hookform/resolvers/zod";
import { PlusIcon } from "lucide-react";
import Link from "next/link";
import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { useForm } from "react-hook-form";
@@ -57,6 +58,9 @@ type Schema = z.infer<typeof Schema>;
export const AddServer = () => {
const utils = api.useUtils();
const [isOpen, setIsOpen] = useState(false);
const { data: canCreateMoreServers, refetch } =
api.stripe.canCreateMoreServers.useQuery();
const { data: sshKeys } = api.sshKey.all.useQuery();
const { mutateAsync, error, isError } = api.server.create.useMutation();
const form = useForm<Schema>({
@@ -82,6 +86,10 @@ export const AddServer = () => {
});
}, [form, form.reset, form.formState.isSubmitSuccessful]);
useEffect(() => {
refetch();
}, [isOpen]);
const onSubmit = async (data: Schema) => {
await mutateAsync({
name: data.name,
@@ -116,6 +124,14 @@ export const AddServer = () => {
Add a server to deploy your applications remotely.
</DialogDescription>
</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>}
<Form {...form}>
<form
@@ -254,6 +270,7 @@ export const AddServer = () => {
<DialogFooter>
<Button
isLoading={form.formState.isSubmitting}
disabled={!canCreateMoreServers}
form="hook-form-add-server"
type="submit"
>

View File

@@ -36,6 +36,9 @@ export const ShowServers = () => {
const { data, refetch } = api.server.all.useQuery();
const { mutateAsync } = api.server.remove.useMutation();
const { data: sshKeys } = api.sshKey.all.useQuery();
const { data: isCloud } = api.settings.isCloud.useQuery();
const { data: canCreateMoreServers } =
api.stripe.canCreateMoreServers.useQuery();
return (
<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">
<ServerIcon className="size-8" />
<span className="text-base text-muted-foreground">
No Servers found. Add a server to deploy your applications
remotely.
{!canCreateMoreServers ? (
<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>
</div>
)
@@ -87,6 +104,9 @@ export const ShowServers = () => {
<TableHeader>
<TableRow>
<TableHead className="w-[100px]">Name</TableHead>
{isCloud && (
<TableHead className="text-center">Status</TableHead>
)}
<TableHead className="text-center">IP Address</TableHead>
<TableHead className="text-center">Port</TableHead>
<TableHead className="text-center">Username</TableHead>
@@ -98,9 +118,23 @@ export const ShowServers = () => {
<TableBody>
{data?.map((server) => {
const canDelete = server.totalSum === 0;
const isActive = server.serverStatus === "active";
return (
<TableRow key={server.serverId}>
<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">
<Badge>{server.ipAddress}</Badge>
</TableCell>
@@ -131,18 +165,25 @@ export const ShowServers = () => {
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuLabel>Actions</DropdownMenuLabel>
{server.sshKeyId && (
<TerminalModal serverId={server.serverId}>
<span>Enter the terminal</span>
</TerminalModal>
{isActive && (
<>
{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
disabled={!canDelete}
title={
@@ -187,17 +228,21 @@ export const ShowServers = () => {
</DropdownMenuItem>
</DialogAction>
{server.sshKeyId && (
{isActive && (
<>
<DropdownMenuSeparator />
<DropdownMenuLabel>Extra</DropdownMenuLabel>
{server.sshKeyId && (
<>
<DropdownMenuSeparator />
<DropdownMenuLabel>Extra</DropdownMenuLabel>
<ShowTraefikFileSystemModal
serverId={server.serverId}
/>
<ShowDockerContainersModal
serverId={server.serverId}
/>
<ShowTraefikFileSystemModal
serverId={server.serverId}
/>
<ShowDockerContainersModal
serverId={server.serverId}
/>
</>
)}
</>
)}
</DropdownMenuContent>

View File

@@ -54,7 +54,7 @@ export const AddUser = () => {
const onSubmit = async (data: AddUser) => {
await mutateAsync({
email: data.email,
email: data.email.toLowerCase(),
})
.then(async () => {
toast.success("Invitation created");

View File

@@ -1,162 +0,0 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { Button } from "@/components/ui/button";
import {
Dialog,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog";
import {
Form,
FormControl,
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 { SquarePen } from "lucide-react";
import { useEffect } from "react";
import { useForm } from "react-hook-form";
import { toast } from "sonner";
import { z } from "zod";
const updateUserSchema = z.object({
email: z
.string()
.min(1, "Email is required")
.email({ message: "Invalid email" }),
password: z.string(),
});
type UpdateUser = z.infer<typeof updateUserSchema>;
interface Props {
authId: string;
}
export const UpdateUser = ({ authId }: Props) => {
const utils = api.useUtils();
const { mutateAsync, error, isError, isLoading } =
api.auth.updateByAdmin.useMutation();
const { data } = api.auth.one.useQuery(
{
id: authId,
},
{
enabled: !!authId,
},
);
const form = useForm<UpdateUser>({
defaultValues: {
email: "",
password: "",
},
resolver: zodResolver(updateUserSchema),
});
useEffect(() => {
if (data) {
form.reset({
email: data.email || "",
password: "",
});
}
}, [data, form, form.reset]);
const onSubmit = async (formData: UpdateUser) => {
await mutateAsync({
email: formData.email === data?.email ? null : formData.email,
password: formData.password,
id: authId,
})
.then(() => {
toast.success("User updated succesfully");
utils.user.all.invalidate();
})
.catch(() => {
toast.error("Error to update the user");
})
.finally(() => {});
};
return (
<Dialog>
<DialogTrigger asChild className="w-fit">
<Button
variant="ghost"
className=" cursor-pointer space-x-3 w-fit"
onSelect={(e) => e.preventDefault()}
>
<SquarePen className="size-4 text-muted-foreground" />
</Button>
</DialogTrigger>
<DialogContent className="max-h-screen overflow-y-auto sm:max-w-lg">
<DialogHeader>
<DialogTitle>Update User</DialogTitle>
<DialogDescription>Update the user</DialogDescription>
</DialogHeader>
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
<div className="grid gap-4">
<div className="grid items-center gap-4">
<Form {...form}>
<form
onSubmit={form.handleSubmit(onSubmit)}
id="hook-form-update-user"
className="grid w-full gap-4 "
>
<FormField
control={form.control}
name="email"
render={({ field }) => (
<FormItem>
<FormLabel>Email</FormLabel>
<FormControl>
<Input placeholder="XNl5C@example.com" {...field} />
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<FormField
control={form.control}
name="password"
render={({ field }) => (
<FormItem>
<FormLabel>Password</FormLabel>
<FormControl>
<Input
type="password"
placeholder="*******"
{...field}
/>
</FormControl>
<FormMessage />
</FormItem>
)}
/>
<DialogFooter>
<Button
form="hook-form-update-user"
type="submit"
isLoading={isLoading}
>
Update
</Button>
</DialogFooter>
</form>
</Form>
</div>
</div>
</DialogContent>
</Dialog>
);
};

View File

@@ -80,6 +80,7 @@ export const SettingsLayout = ({ children }: Props) => {
icon: ListMusic,
href: "/dashboard/settings/registry",
},
...(!isCloud
? [
{
@@ -102,6 +103,16 @@ export const SettingsLayout = ({ children }: Props) => {
icon: Server,
href: "/dashboard/settings/servers",
},
...(isCloud
? [
{
title: "Billing",
label: "",
icon: CreditCardIcon,
href: "/dashboard/settings/billing",
},
]
: []),
]
: []),
...(user?.canAccessToSSHKeys
@@ -137,6 +148,7 @@ import {
Activity,
Bell,
BoxesIcon,
CreditCardIcon,
Database,
GitBranch,
KeyIcon,

View 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;

View File

@@ -0,0 +1,2 @@
ALTER TABLE "auth" ADD COLUMN "confirmationToken" text;--> statement-breakpoint
ALTER TABLE "auth" ADD COLUMN "confirmationExpiresAt" text;

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -288,6 +288,20 @@
"when": 1728780577084,
"tag": "0040_graceful_wolfsbane",
"breakpoints": true
},
{
"idx": 41,
"version": "6",
"when": 1729667438853,
"tag": "0041_huge_bruce_banner",
"breakpoints": true
},
{
"idx": 42,
"version": "6",
"when": 1729984439862,
"tag": "0042_fancy_havok",
"breakpoints": true
}
]
}

View File

@@ -1,6 +1,6 @@
{
"name": "dokploy",
"version": "v0.10.2",
"version": "v0.10.4",
"private": true,
"license": "Apache-2.0",
"type": "module",
@@ -11,7 +11,7 @@
"build-next": "next build",
"setup": "tsx -r dotenv/config setup.ts && sleep 5 && pnpm run migration:run",
"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",
"migration:generate": "drizzle-kit generate --config ./server/db/drizzle.config.ts",
"migration:run": "tsx -r dotenv/config migration.ts",
@@ -34,12 +34,12 @@
"test": "vitest --config __test__/vitest.config.ts"
},
"dependencies": {
"@dokploy/server": "workspace:*",
"@codemirror/lang-json": "^6.0.1",
"@codemirror/lang-yaml": "^6.1.1",
"@codemirror/language": "^6.10.1",
"@codemirror/legacy-modes": "6.4.0",
"@codemirror/view": "6.29.0",
"@dokploy/server": "workspace:*",
"@dokploy/trpc-openapi": "0.0.4",
"@hookform/resolvers": "^3.3.4",
"@octokit/webhooks": "^13.2.7",
@@ -61,6 +61,7 @@
"@radix-ui/react-tabs": "^1.0.4",
"@radix-ui/react-toggle": "^1.0.3",
"@radix-ui/react-tooltip": "^1.0.7",
"@stripe/stripe-js": "4.8.0",
"@tanstack/react-query": "^4.36.1",
"@tanstack/react-table": "^8.16.0",
"@trpc/client": "^10.43.6",
@@ -89,7 +90,7 @@
"lucia": "^3.0.1",
"lucide-react": "^0.312.0",
"nanoid": "3",
"next": "^14.1.3",
"next": "^15.0.1",
"next-themes": "^0.2.1",
"node-pty": "1.0.0",
"node-schedule": "2.1.1",
@@ -102,6 +103,8 @@
"recharts": "^2.12.7",
"slugify": "^1.6.6",
"sonner": "^1.4.0",
"ssh2": "1.15.0",
"stripe": "17.2.0",
"superjson": "^2.2.1",
"swagger-ui-react": "^5.17.14",
"tailwind-merge": "^2.2.0",
@@ -111,11 +114,9 @@
"ws": "8.16.0",
"xterm-addon-fit": "^0.8.0",
"zod": "^3.23.4",
"zod-form-data": "^2.0.2",
"ssh2": "1.15.0"
"zod-form-data": "^2.0.2"
},
"devDependencies": {
"autoprefixer": "10.4.12",
"@types/adm-zip": "^0.5.5",
"@types/bcrypt": "5.0.2",
"@types/js-yaml": "4.0.9",
@@ -124,8 +125,10 @@
"@types/node-schedule": "2.1.6",
"@types/react": "^18.2.37",
"@types/react-dom": "^18.2.15",
"@types/ssh2": "1.15.1",
"@types/swagger-ui-react": "^4.18.3",
"@types/ws": "8.5.10",
"autoprefixer": "10.4.12",
"drizzle-kit": "^0.21.1",
"esbuild": "0.20.2",
"lint-staged": "^15.2.7",
@@ -134,8 +137,7 @@
"tsx": "^4.7.0",
"typescript": "^5.4.2",
"vite-tsconfig-paths": "4.3.2",
"vitest": "^1.6.0",
"@types/ssh2": "1.15.1"
"vitest": "^1.6.0"
},
"ct3aMetadata": {
"initVersion": "7.25.2"

View File

@@ -7,6 +7,7 @@ import { ThemeProvider } from "next-themes";
import type { AppProps } from "next/app";
import { Inter } from "next/font/google";
import Head from "next/head";
import Script from "next/script";
import type { ReactElement, ReactNode } from "react";
const inter = Inter({ subsets: ["latin"] });
@@ -36,6 +37,14 @@ const MyApp = ({
<Head>
<title>Dokploy</title>
</Head>
{process.env.NEXT_PUBLIC_UMAMI_HOST &&
process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID && (
<Script
src={process.env.NEXT_PUBLIC_UMAMI_HOST}
data-website-id={process.env.NEXT_PUBLIC_UMAMI_WEBSITE_ID}
/>
)}
<ThemeProvider
attribute="class"
defaultTheme="system"

View File

@@ -3,8 +3,9 @@ import { Head, Html, Main, NextScript } from "next/document";
export default function Document() {
return (
<Html lang="en" className="font-sans">
<Head />
<Head>
<link rel="icon" href="/icon.svg" />
</Head>
<body className="flex h-full flex-col font-sans">
<Main />
<NextScript />

View File

@@ -1,6 +1,6 @@
import { db } from "@/server/db";
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 { deploy } from "@/server/utils/deploy";
import { IS_CLOUD } from "@dokploy/server";

View File

@@ -1,6 +1,6 @@
import { db } from "@/server/db";
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 { deploy } from "@/server/utils/deploy";
import { IS_CLOUD } from "@dokploy/server";

View File

@@ -1,6 +1,6 @@
import { db } from "@/server/db";
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 { deploy } from "@/server/utils/deploy";
import { IS_CLOUD } from "@dokploy/server";

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

View File

@@ -0,0 +1,96 @@
import { OnboardingLayout } from "@/components/layouts/onboarding-layout";
import { Logo } from "@/components/shared/logo";
import { CardDescription, CardTitle } from "@/components/ui/card";
import { db } from "@/server/db";
import { auth } from "@/server/db/schema";
import { IS_CLOUD, updateAuthById } from "@dokploy/server";
import { isBefore } from "date-fns";
import { eq } from "drizzle-orm";
import type { GetServerSidePropsContext } from "next";
import Link from "next/link";
import type { ReactElement } from "react";
export default function Home() {
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">Email Confirmed</CardTitle>
<CardDescription>
Congratulations, your email is confirmed.
</CardDescription>
<div>
<Link href="/" className="w-full text-primary">
Click here to login
</Link>
</div>
</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.confirmationToken, token),
});
if (
!authR ||
authR?.confirmationToken === null ||
authR?.confirmationExpiresAt === null
) {
return {
redirect: {
permanent: true,
destination: "/",
},
};
}
const isExpired = isBefore(new Date(authR.confirmationExpiresAt), new Date());
if (isExpired) {
return {
redirect: {
permanent: true,
destination: "/",
},
};
}
await updateAuthById(authR.id, {
confirmationToken: null,
confirmationExpiresAt: null,
});
return {
props: {
token: authR.confirmationToken,
},
};
}

View File

@@ -22,13 +22,20 @@ import {
BreadcrumbItem,
BreadcrumbLink,
} from "@/components/ui/breadcrumb";
import { Label } from "@/components/ui/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api";
import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server";
import { GlobeIcon } from "lucide-react";
import { GlobeIcon, HelpCircle, ServerOff } from "lucide-react";
import type {
GetServerSidePropsContext,
InferGetServerSidePropsType,
@@ -100,8 +107,40 @@ const Service = (
</h1>
<span className="text-sm">{data?.appName}</span>
</div>
<div>
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
<div className="flex flex-row h-fit w-fit gap-2">
<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>
{data?.description && (
@@ -119,90 +158,111 @@ const Service = (
</div>
</header>
</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} />
)}
{data?.server?.serverStatus === "inactive" ? (
<div className="flex h-[55vh] border-2 rounded-xl border-dashed p-4">
<div className="max-w-3xl mx-auto flex flex-col items-center justify-center self-center gap-3">
<ServerOff className="size-10 text-muted-foreground self-center" />
<span className="text-center text-base text-muted-foreground">
This service is hosted on the server {data.server.name}, but this
server has been disabled because your current plan doesn't include
enough servers. Please purchase more servers to regain access to
this application.
</span>
<span className="text-center text-base text-muted-foreground">
Go to{" "}
<Link href="/dashboard/settings/billing" className="text-primary">
Billing
</Link>
</span>
</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">
<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">
<TabsContent value="general">
<div className="flex flex-col gap-4 pt-2.5">
<DockerMonitoring appName={data?.appName || ""} />
<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">
<DockerMonitoring appName={data?.appName || ""} />
</div>
</TabsContent>
)}
<TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDockerLogs
appName={data?.appName || ""}
serverId={data?.serverId || ""}
/>
</div>
</TabsContent>
<TabsContent value="deployments" className="w-full">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDeployments applicationId={applicationId} />
</div>
</TabsContent>
<TabsContent value="domains" className="w-full">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDomains applicationId={applicationId} />
</div>
</TabsContent>
<TabsContent value="advanced">
<div className="flex flex-col gap-4 pt-2.5">
<AddCommand applicationId={applicationId} />
<ShowClusterSettings applicationId={applicationId} />
<ShowApplicationResources applicationId={applicationId} />
<ShowVolumes applicationId={applicationId} />
<ShowRedirects applicationId={applicationId} />
<ShowSecurity applicationId={applicationId} />
<ShowPorts applicationId={applicationId} />
<ShowTraefikConfig applicationId={applicationId} />
</div>
</TabsContent>
</Tabs>
<TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDockerLogs
appName={data?.appName || ""}
serverId={data?.serverId || ""}
/>
</div>
</TabsContent>
<TabsContent value="deployments" className="w-full">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDeployments applicationId={applicationId} />
</div>
</TabsContent>
<TabsContent value="domains" className="w-full">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDomains applicationId={applicationId} />
</div>
</TabsContent>
<TabsContent value="advanced">
<div className="flex flex-col gap-4 pt-2.5">
<AddCommand applicationId={applicationId} />
<ShowClusterSettings applicationId={applicationId} />
<ShowApplicationResources applicationId={applicationId} />
<ShowVolumes applicationId={applicationId} />
<ShowRedirects applicationId={applicationId} />
<ShowSecurity applicationId={applicationId} />
<ShowPorts applicationId={applicationId} />
<ShowTraefikConfig applicationId={applicationId} />
</div>
</TabsContent>
</Tabs>
)}
</div>
);
};

View File

@@ -16,13 +16,21 @@ import {
BreadcrumbItem,
BreadcrumbLink,
} from "@/components/ui/breadcrumb";
import { Label } from "@/components/ui/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api";
import { validateRequest } from "@dokploy/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 {
GetServerSidePropsContext,
InferGetServerSidePropsType,
@@ -94,8 +102,40 @@ const Service = (
</h1>
<span className="text-sm">{data?.appName}</span>
</div>
<div>
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
<div className="flex flex-row h-fit w-fit gap-2">
<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>
{data?.description && (
<p className="text-sm text-muted-foreground max-w-6xl">
@@ -113,98 +153,118 @@ const Service = (
</div>
</header>
</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} />
{(auth?.rol === "admin" || user?.canDeleteServices) && (
<DeleteCompose composeId={composeId} />
)}
{data?.server?.serverStatus === "inactive" ? (
<div className="flex h-[55vh] border-2 rounded-xl border-dashed p-4">
<div className="max-w-3xl mx-auto flex flex-col items-center justify-center self-center gap-3">
<ServerOff className="size-10 text-muted-foreground self-center" />
<span className="text-center text-base text-muted-foreground">
This service is hosted on the server {data.server.name}, but this
server has been disabled because your current plan doesn't include
enough servers. Please purchase more servers to regain access to
this application.
</span>
<span className="text-center text-base text-muted-foreground">
Go to{" "}
<Link href="/dashboard/settings/billing" className="text-primary">
Billing
</Link>
</span>
</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">
<div className="flex flex-col gap-4 pt-2.5">
<ShowGeneralCompose composeId={composeId} />
{(auth?.rol === "admin" || user?.canDeleteServices) && (
<DeleteCompose composeId={composeId} />
)}
</div>
</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">
<TabsContent value="general">
<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 || ""}
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 || ""}
appName={data?.appName || ""}
appType={data?.composeType || "docker-compose"}
/>
</div>
</TabsContent>
<TabsContent value="deployments">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDeploymentsCompose composeId={composeId} />
</div>
</TabsContent>
<TabsContent value="deployments">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDeploymentsCompose composeId={composeId} />
</div>
</TabsContent>
<TabsContent value="domains">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDomainsCompose composeId={composeId} />
</div>
</TabsContent>
<TabsContent value="advanced">
<div className="flex flex-col gap-4 pt-2.5">
<AddCommandCompose composeId={composeId} />
<ShowVolumesCompose composeId={composeId} />
</div>
</TabsContent>
</Tabs>
<TabsContent value="domains">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDomainsCompose composeId={composeId} />
</div>
</TabsContent>
<TabsContent value="advanced">
<div className="flex flex-col gap-4 pt-2.5">
<AddCommandCompose composeId={composeId} />
<ShowVolumesCompose composeId={composeId} />
</div>
</TabsContent>
</Tabs>
)}
</div>
);
};

View File

@@ -17,12 +17,20 @@ import {
BreadcrumbItem,
BreadcrumbLink,
} from "@/components/ui/breadcrumb";
import { Label } from "@/components/ui/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api";
import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server";
import { HelpCircle, ServerOff } from "lucide-react";
import type {
GetServerSidePropsContext,
InferGetServerSidePropsType,
@@ -82,8 +90,40 @@ const Mariadb = (
</h1>
<span className="text-sm">{data?.appName}</span>
</div>
<div>
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
<div className="flex flex-row h-fit w-fit gap-2">
<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>
{data?.description && (
<p className="text-sm text-muted-foreground max-w-6xl">
@@ -99,79 +139,99 @@ const Mariadb = (
</div>
</header>
</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}`;
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-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} />
)}
{data?.server?.serverStatus === "inactive" ? (
<div className="flex h-[55vh] border-2 rounded-xl border-dashed p-4">
<div className="max-w-3xl mx-auto flex flex-col items-center justify-center self-center gap-3">
<ServerOff className="size-10 text-muted-foreground self-center" />
<span className="text-center text-base text-muted-foreground">
This service is hosted on the server {data.server.name}, but this
server has been disabled because your current plan doesn't include
enough servers. Please purchase more servers to regain access to
this application.
</span>
<span className="text-center text-base text-muted-foreground">
Go to{" "}
<Link href="/dashboard/settings/billing" className="text-primary">
Billing
</Link>
</span>
</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">
<div className="flex flex-col gap-4 pt-2.5">
<ShowGeneralMariadb mariadbId={mariadbId} />
<ShowInternalMariadbCredentials mariadbId={mariadbId} />
<ShowExternalMariadbCredentials mariadbId={mariadbId} />
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-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>
</TabsContent>
<TabsContent value="environment">
<div className="flex flex-col gap-4 pt-2.5">
<ShowMariadbEnvironment mariadbId={mariadbId} />
</div>
</TabsContent>
{!data?.serverId && (
<TabsContent value="monitoring">
<TabsContent value="general">
<div className="flex flex-col gap-4 pt-2.5">
<DockerMonitoring appName={data?.appName || ""} />
<ShowGeneralMariadb mariadbId={mariadbId} />
<ShowInternalMariadbCredentials mariadbId={mariadbId} />
<ShowExternalMariadbCredentials mariadbId={mariadbId} />
</div>
</TabsContent>
)}
<TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDockerLogs
serverId={data?.serverId || ""}
appName={data?.appName || ""}
/>
</div>
</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>
<TabsContent value="environment">
<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">
<DockerMonitoring appName={data?.appName || ""} />
</div>
</TabsContent>
)}
<TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDockerLogs
serverId={data?.serverId || ""}
appName={data?.appName || ""}
/>
</div>
</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>
);
};

View File

@@ -17,12 +17,20 @@ import {
BreadcrumbItem,
BreadcrumbLink,
} from "@/components/ui/breadcrumb";
import { Label } from "@/components/ui/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api";
import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server";
import { HelpCircle, ServerOff } from "lucide-react";
import type {
GetServerSidePropsContext,
InferGetServerSidePropsType,
@@ -83,8 +91,40 @@ const Mongo = (
</h1>
<span className="text-sm">{data?.appName}</span>
</div>
<div>
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
<div className="flex flex-row h-fit w-fit gap-2">
<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>
{data?.description && (
<p className="text-sm text-muted-foreground max-w-6xl">
@@ -100,80 +140,100 @@ const Mongo = (
</div>
</header>
</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}`;
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-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} />
)}
{data?.server?.serverStatus === "inactive" ? (
<div className="flex h-[55vh] border-2 rounded-xl border-dashed p-4">
<div className="max-w-3xl mx-auto flex flex-col items-center justify-center self-center gap-3">
<ServerOff className="size-10 text-muted-foreground self-center" />
<span className="text-center text-base text-muted-foreground">
This service is hosted on the server {data.server.name}, but this
server has been disabled because your current plan doesn't include
enough servers. Please purchase more servers to regain access to
this application.
</span>
<span className="text-center text-base text-muted-foreground">
Go to{" "}
<Link href="/dashboard/settings/billing" className="text-primary">
Billing
</Link>
</span>
</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">
<div className="flex flex-col gap-4 pt-2.5">
<ShowGeneralMongo mongoId={mongoId} />
<ShowInternalMongoCredentials mongoId={mongoId} />
<ShowExternalMongoCredentials mongoId={mongoId} />
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-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>
</TabsContent>
<TabsContent value="environment">
<div className="flex flex-col gap-4 pt-2.5">
<ShowMongoEnvironment mongoId={mongoId} />
</div>
</TabsContent>
{!data?.serverId && (
<TabsContent value="monitoring">
<TabsContent value="general">
<div className="flex flex-col gap-4 pt-2.5">
<DockerMonitoring appName={data?.appName || ""} />
<ShowGeneralMongo mongoId={mongoId} />
<ShowInternalMongoCredentials mongoId={mongoId} />
<ShowExternalMongoCredentials mongoId={mongoId} />
</div>
</TabsContent>
)}
<TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDockerLogs
serverId={data?.serverId || ""}
appName={data?.appName || ""}
/>
</div>
</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>
<TabsContent value="environment">
<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">
<DockerMonitoring appName={data?.appName || ""} />
</div>
</TabsContent>
)}
<TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDockerLogs
serverId={data?.serverId || ""}
appName={data?.appName || ""}
/>
</div>
</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>
);
};

View File

@@ -17,12 +17,20 @@ import {
BreadcrumbItem,
BreadcrumbLink,
} from "@/components/ui/breadcrumb";
import { Label } from "@/components/ui/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api";
import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server";
import { HelpCircle, ServerOff } from "lucide-react";
import type {
GetServerSidePropsContext,
InferGetServerSidePropsType,
@@ -81,8 +89,40 @@ const MySql = (
</h1>
<span className="text-sm">{data?.appName}</span>
</div>
<div>
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
<div className="flex flex-row h-fit w-fit gap-2">
<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>
{data?.description && (
<p className="text-sm text-muted-foreground max-w-6xl">
@@ -99,80 +139,100 @@ const MySql = (
</div>
</header>
</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}`;
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-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} />
)}
{data?.server?.serverStatus === "inactive" ? (
<div className="flex h-[55vh] border-2 rounded-xl border-dashed p-4">
<div className="max-w-3xl mx-auto flex flex-col items-center justify-center self-center gap-3">
<ServerOff className="size-10 text-muted-foreground self-center" />
<span className="text-center text-base text-muted-foreground">
This service is hosted on the server {data.server.name}, but this
server has been disabled because your current plan doesn't include
enough servers. Please purchase more servers to regain access to
this application.
</span>
<span className="text-center text-base text-muted-foreground">
Go to{" "}
<Link href="/dashboard/settings/billing" className="text-primary">
Billing
</Link>
</span>
</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">
<div className="flex flex-col gap-4 pt-2.5">
<ShowGeneralMysql mysqlId={mysqlId} />
<ShowInternalMysqlCredentials mysqlId={mysqlId} />
<ShowExternalMysqlCredentials mysqlId={mysqlId} />
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-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>
</TabsContent>
<TabsContent value="environment" className="w-full">
<div className="flex flex-col gap-4 pt-2.5">
<ShowMysqlEnvironment mysqlId={mysqlId} />
</div>
</TabsContent>
{!data?.serverId && (
<TabsContent value="monitoring">
<TabsContent value="general">
<div className="flex flex-col gap-4 pt-2.5">
<DockerMonitoring appName={data?.appName || ""} />
<ShowGeneralMysql mysqlId={mysqlId} />
<ShowInternalMysqlCredentials mysqlId={mysqlId} />
<ShowExternalMysqlCredentials mysqlId={mysqlId} />
</div>
</TabsContent>
)}
<TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDockerLogs
serverId={data?.serverId || ""}
appName={data?.appName || ""}
/>
</div>
</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>
<TabsContent value="environment" className="w-full">
<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">
<DockerMonitoring appName={data?.appName || ""} />
</div>
</TabsContent>
)}
<TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDockerLogs
serverId={data?.serverId || ""}
appName={data?.appName || ""}
/>
</div>
</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>
);
};

View File

@@ -17,12 +17,20 @@ import {
BreadcrumbItem,
BreadcrumbLink,
} from "@/components/ui/breadcrumb";
import { Label } from "@/components/ui/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api";
import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server";
import { HelpCircle, ServerOff } from "lucide-react";
import type {
GetServerSidePropsContext,
InferGetServerSidePropsType,
@@ -82,8 +90,40 @@ const Postgresql = (
</h1>
<span className="text-sm">{data?.appName}</span>
</div>
<div>
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
<div className="flex flex-row h-fit w-fit gap-2">
<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>
{data?.description && (
<p className="text-sm text-muted-foreground max-w-6xl">
@@ -100,80 +140,100 @@ const Postgresql = (
</div>
</header>
</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}`;
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-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} />
)}
{data?.server?.serverStatus === "inactive" ? (
<div className="flex h-[55vh] border-2 rounded-xl border-dashed p-4">
<div className="max-w-3xl mx-auto flex flex-col items-center justify-center self-center gap-3">
<ServerOff className="size-10 text-muted-foreground self-center" />
<span className="text-center text-base text-muted-foreground">
This service is hosted on the server {data.server.name}, but this
server has been disabled because your current plan doesn't include
enough servers. Please purchase more servers to regain access to
this application.
</span>
<span className="text-center text-base text-muted-foreground">
Go to{" "}
<Link href="/dashboard/settings/billing" className="text-primary">
Billing
</Link>
</span>
</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">
<div className="flex flex-col gap-4 pt-2.5">
<ShowGeneralPostgres postgresId={postgresId} />
<ShowInternalPostgresCredentials postgresId={postgresId} />
<ShowExternalPostgresCredentials postgresId={postgresId} />
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-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>
</TabsContent>
<TabsContent value="environment">
<div className="flex flex-col gap-4 pt-2.5">
<ShowPostgresEnvironment postgresId={postgresId} />
</div>
</TabsContent>
{!data?.serverId && (
<TabsContent value="monitoring">
<TabsContent value="general">
<div className="flex flex-col gap-4 pt-2.5">
<DockerMonitoring appName={data?.appName || ""} />
<ShowGeneralPostgres postgresId={postgresId} />
<ShowInternalPostgresCredentials postgresId={postgresId} />
<ShowExternalPostgresCredentials postgresId={postgresId} />
</div>
</TabsContent>
)}
<TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDockerLogs
serverId={data?.serverId || ""}
appName={data?.appName || ""}
/>
</div>
</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>
<TabsContent value="environment">
<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">
<DockerMonitoring appName={data?.appName || ""} />
</div>
</TabsContent>
)}
<TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5">
<ShowDockerLogs
serverId={data?.serverId || ""}
appName={data?.appName || ""}
/>
</div>
</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>
);
};

View File

@@ -16,12 +16,20 @@ import {
BreadcrumbItem,
BreadcrumbLink,
} from "@/components/ui/breadcrumb";
import { Label } from "@/components/ui/label";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import {
Tooltip,
TooltipContent,
TooltipProvider,
TooltipTrigger,
} from "@/components/ui/tooltip";
import { cn } from "@/lib/utils";
import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api";
import { validateRequest } from "@dokploy/server";
import { createServerSideHelpers } from "@trpc/react-query/server";
import { HelpCircle, ServerOff } from "lucide-react";
import type {
GetServerSidePropsContext,
InferGetServerSidePropsType,
@@ -81,8 +89,40 @@ const Redis = (
</h1>
<span className="text-sm">{data?.appName}</span>
</div>
<div>
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
<div className="flex flex-row h-fit w-fit gap-2">
<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>
{data?.description && (
<p className="text-sm text-muted-foreground max-w-6xl">
@@ -99,74 +139,94 @@ const Redis = (
</div>
</header>
</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}`;
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-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} />
)}
{data?.server?.serverStatus === "inactive" ? (
<div className="flex h-[55vh] border-2 rounded-xl border-dashed p-4">
<div className="max-w-3xl mx-auto flex flex-col items-center justify-center self-center gap-3">
<ServerOff className="size-10 text-muted-foreground self-center" />
<span className="text-center text-base text-muted-foreground">
This service is hosted on the server {data.server.name}, but this
server has been disabled because your current plan doesn't include
enough servers. Please purchase more servers to regain access to
this application.
</span>
<span className="text-center text-base text-muted-foreground">
Go to{" "}
<Link href="/dashboard/settings/billing" className="text-primary">
Billing
</Link>
</span>
</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">
<div className="flex flex-col gap-4 pt-2.5">
<ShowGeneralRedis redisId={redisId} />
<ShowInternalRedisCredentials redisId={redisId} />
<ShowExternalRedisCredentials redisId={redisId} />
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-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>
</TabsContent>
<TabsContent value="environment">
<div className="flex flex-col gap-4 pt-2.5">
<ShowRedisEnvironment redisId={redisId} />
</div>
</TabsContent>
{!data?.serverId && (
<TabsContent value="monitoring">
<TabsContent value="general">
<div className="flex flex-col gap-4 pt-2.5">
<DockerMonitoring appName={data?.appName || ""} />
<ShowGeneralRedis redisId={redisId} />
<ShowInternalRedisCredentials redisId={redisId} />
<ShowExternalRedisCredentials redisId={redisId} />
</div>
</TabsContent>
)}
<TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5">
<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>
<TabsContent value="environment">
<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">
<DockerMonitoring appName={data?.appName || ""} />
</div>
</TabsContent>
)}
<TabsContent value="logs">
<div className="flex flex-col gap-4 pt-2.5">
<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>
);
};

View File

@@ -1,8 +1,11 @@
import { ShowProjects } from "@/components/dashboard/projects/show";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { appRouter } from "@/server/api/root";
import { 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 Dashboard = () => {
return <ShowProjects />;
@@ -16,7 +19,22 @@ Dashboard.getLayout = (page: ReactElement) => {
export async function getServerSideProps(
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) {
return {
@@ -27,6 +45,8 @@ export async function getServerSideProps(
};
}
return {
props: {},
props: {
trpcState: helpers.dehydrate(),
},
};
}

View File

@@ -1,9 +1,12 @@
import { AppearanceForm } from "@/components/dashboard/settings/appearance-form";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { SettingsLayout } from "@/components/layouts/settings-layout";
import { appRouter } from "@/server/api/root";
import { 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 (
@@ -25,7 +28,30 @@ Page.getLayout = (page: ReactElement) => {
export async function getServerSideProps(
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();
await helpers.auth.get.prefetch();
if (user?.rol === "user") {
await helpers.user.byAuthId.prefetch({
authId: user.authId,
});
}
if (!user) {
return {
redirect: {
@@ -34,8 +60,9 @@ export async function getServerSideProps(
},
};
}
return {
props: {},
props: {
trpcState: helpers.dehydrate(),
},
};
}

View File

@@ -0,0 +1,67 @@
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.auth.get.prefetch();
await helpers.settings.isCloud.prefetch();
return {
props: {
trpcState: helpers.dehydrate(),
},
};
}

View File

@@ -1,10 +1,12 @@
import { ShowCertificates } from "@/components/dashboard/settings/certificates/show-certificates";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { SettingsLayout } from "@/components/layouts/settings-layout";
import { appRouter } from "@/server/api/root";
import { 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 (
<div className="flex flex-col gap-4 w-full">
@@ -25,7 +27,8 @@ Page.getLayout = (page: ReactElement) => {
export async function getServerSideProps(
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") {
return {
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.auth.get.prefetch();
await helpers.settings.isCloud.prefetch();
return {
props: {},
props: {
trpcState: helpers.dehydrate(),
},
};
}

View File

@@ -1,10 +1,12 @@
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 { 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 (
@@ -26,6 +28,7 @@ Page.getLayout = (page: ReactElement) => {
export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ serviceId: string }>,
) {
const { req, res } = ctx;
if (IS_CLOUD) {
return {
redirect: {
@@ -43,8 +46,22 @@ 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.auth.get.prefetch();
return {
props: {},
props: {
trpcState: helpers.dehydrate(),
},
};
}

View File

@@ -1,9 +1,12 @@
import { ShowDestinations } from "@/components/dashboard/settings/destination/show-destinations";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { SettingsLayout } from "@/components/layouts/settings-layout";
import { appRouter } from "@/server/api/root";
import { 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 (
@@ -25,7 +28,8 @@ Page.getLayout = (page: ReactElement) => {
export async function getServerSideProps(
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") {
return {
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.auth.get.prefetch();
await helpers.settings.isCloud.prefetch();
return {
props: {},
props: {
trpcState: helpers.dehydrate(),
},
};
}

View File

@@ -49,9 +49,10 @@ export async function getServerSideProps(
},
transformer: superjson,
});
await helpers.auth.get.prefetch();
try {
await helpers.project.all.prefetch();
await helpers.settings.isCloud.prefetch();
const auth = await helpers.auth.get.fetch();
if (auth.rol === "user") {

View File

@@ -1,10 +1,12 @@
import { ShowDestinations } from "@/components/dashboard/settings/destination/show-destinations";
import { ShowNotifications } from "@/components/dashboard/settings/notifications/show-notifications";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { SettingsLayout } from "@/components/layouts/settings-layout";
import { appRouter } from "@/server/api/root";
import { 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 (
@@ -26,7 +28,8 @@ Page.getLayout = (page: ReactElement) => {
export async function getServerSideProps(
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") {
return {
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.auth.get.prefetch();
await helpers.settings.isCloud.prefetch();
return {
props: {},
props: {
trpcState: helpers.dehydrate(),
},
};
}

View File

@@ -2,10 +2,13 @@ import { GenerateToken } from "@/components/dashboard/settings/profile/generate-
import { ProfileForm } from "@/components/dashboard/settings/profile/profile-form";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { SettingsLayout } from "@/components/layouts/settings-layout";
import { appRouter } from "@/server/api/root";
import { api } from "@/utils/api";
import { 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 = () => {
const { data } = api.auth.get.useQuery();
@@ -37,7 +40,29 @@ Page.getLayout = (page: ReactElement) => {
export async function getServerSideProps(
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();
await helpers.auth.get.prefetch();
if (user?.rol === "user") {
await helpers.user.byAuthId.prefetch({
authId: user.authId,
});
}
if (!user) {
return {
redirect: {
@@ -48,6 +73,8 @@ export async function getServerSideProps(
}
return {
props: {},
props: {
trpcState: helpers.dehydrate(),
},
};
}

View File

@@ -1,9 +1,12 @@
import { ShowRegistry } from "@/components/dashboard/settings/cluster/registry/show-registry";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { SettingsLayout } from "@/components/layouts/settings-layout";
import { appRouter } from "@/server/api/root";
import { 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 (
@@ -25,7 +28,8 @@ Page.getLayout = (page: ReactElement) => {
export async function getServerSideProps(
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") {
return {
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.auth.get.prefetch();
await helpers.settings.isCloud.prefetch();
return {
props: {},
props: {
trpcState: helpers.dehydrate(),
},
};
}

View File

@@ -2,9 +2,12 @@ import { WebDomain } from "@/components/dashboard/settings/web-domain";
import { WebServer } from "@/components/dashboard/settings/web-server";
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 (
@@ -27,6 +30,7 @@ Page.getLayout = (page: ReactElement) => {
export async function getServerSideProps(
ctx: GetServerSidePropsContext<{ serviceId: string }>,
) {
const { req, res } = ctx;
if (IS_CLOUD) {
return {
redirect: {
@@ -35,7 +39,7 @@ export async function getServerSideProps(
},
};
}
const { user } = await validateRequest(ctx.req, ctx.res);
const { user, session } = await validateRequest(ctx.req, ctx.res);
if (!user) {
return {
redirect: {
@@ -53,7 +57,22 @@ 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.auth.get.prefetch();
return {
props: {},
props: {
trpcState: helpers.dehydrate(),
},
};
}

View File

@@ -1,9 +1,12 @@
import { ShowServers } from "@/components/dashboard/settings/servers/show-servers";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { SettingsLayout } from "@/components/layouts/settings-layout";
import { appRouter } from "@/server/api/root";
import { 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 (
@@ -25,7 +28,8 @@ Page.getLayout = (page: ReactElement) => {
export async function getServerSideProps(
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) {
return {
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.auth.get.prefetch();
await helpers.settings.isCloud.prefetch();
return {
props: {},
props: {
trpcState: helpers.dehydrate(),
},
};
}

View File

@@ -53,6 +53,7 @@ export async function getServerSideProps(
try {
await helpers.project.all.prefetch();
const auth = await helpers.auth.get.fetch();
await helpers.settings.isCloud.prefetch();
if (auth.rol === "user") {
const user = await helpers.user.byAuthId.fetch({

View File

@@ -1,9 +1,12 @@
import { ShowUsers } from "@/components/dashboard/settings/users/show-users";
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
import { SettingsLayout } from "@/components/layouts/settings-layout";
import { appRouter } from "@/server/api/root";
import { 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 (
@@ -25,7 +28,8 @@ Page.getLayout = (page: ReactElement) => {
export async function getServerSideProps(
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") {
return {
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.auth.get.prefetch();
await helpers.settings.isCloud.prefetch();
return {
props: {},
props: {
trpcState: helpers.dehydrate(),
},
};
}

View File

@@ -1,5 +1,6 @@
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 {
@@ -55,12 +56,16 @@ type AuthResponse = {
authId: string;
};
export default function Home() {
interface Props {
IS_CLOUD: boolean;
}
export default function Home({ IS_CLOUD }: Props) {
const [temp, setTemp] = useState<AuthResponse>({
is2FAEnabled: false,
authId: "",
});
const { mutateAsync, isLoading } = api.auth.login.useMutation();
const { mutateAsync, isLoading, error, isError } =
api.auth.login.useMutation();
const router = useRouter();
const form = useForm<Login>({
defaultValues: {
@@ -112,6 +117,12 @@ export default function Home() {
</CardDescription>
<Card className="mx-auto w-full max-w-lg bg-transparent ">
<div className="p-3.5" />
{isError && (
<AlertBlock type="error" className="mx-4 my-2">
<span>{error?.message}</span>
</AlertBlock>
)}
<CardContent>
{!temp.is2FAEnabled ? (
<Form {...form}>
@@ -167,22 +178,33 @@ export default function Home() {
<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="/register"
>
Create an account
</Link>
{IS_CLOUD && (
<Link
className="hover:underline text-muted-foreground"
href="/register"
>
Create an account
</Link>
)}
</div>
<div className="mt-4 text-sm flex flex-row justify-center gap-2">
<Link
className="hover:underline text-muted-foreground"
href="https://docs.dokploy.com/docs/core/get-started/reset-password"
target="_blank"
>
Lost your password?
</Link>
{IS_CLOUD ? (
<Link
className="hover:underline text-muted-foreground"
href="/send-reset-password"
>
Lost your password?
</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 className="p-2" />
@@ -212,7 +234,9 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
} catch (error) {}
return {
props: {},
props: {
IS_CLOUD: IS_CLOUD,
},
};
}
const hasAdmin = await isAdminPresent();

View File

@@ -16,7 +16,7 @@ import {
} from "@/components/ui/form";
import { Input } from "@/components/ui/input";
import { api } from "@/utils/api";
import { getUserByToken } from "@dokploy/server";
import { IS_CLOUD, getUserByToken } from "@dokploy/server";
import { zodResolver } from "@hookform/resolvers/zod";
import { AlertTriangle } from "lucide-react";
import type { GetServerSidePropsContext } from "next";
@@ -69,9 +69,10 @@ type Register = z.infer<typeof registerSchema>;
interface Props {
token: string;
invitation: Awaited<ReturnType<typeof getUserByToken>>;
isCloud: boolean;
}
const Invitation = ({ token, invitation }: Props) => {
const Invitation = ({ token, invitation, isCloud }: Props) => {
const router = useRouter();
const { data } = api.admin.getUserByToken.useQuery(
{
@@ -83,7 +84,8 @@ const Invitation = ({ token, invitation }: Props) => {
},
);
const { mutateAsync, error, isError } = api.auth.createUser.useMutation();
const { mutateAsync, error, isError, isSuccess } =
api.auth.createUser.useMutation();
const form = useForm<Register>({
defaultValues: {
@@ -112,7 +114,9 @@ const Invitation = ({ token, invitation }: Props) => {
})
.then(() => {
toast.success("User registration succesfuly", {
duration: 2000,
description:
"Please check your inbox or spam folder to confirm your account.",
duration: 100000,
});
router.push("/dashboard/projects");
})
@@ -146,6 +150,7 @@ const Invitation = ({ token, invitation }: Props) => {
</span>
</div>
)}
<CardContent>
<Form {...form}>
<form
@@ -210,6 +215,25 @@ const Invitation = ({ token, invitation }: Props) => {
Register
</Button>
</div>
<div className="mt-4 text-sm flex flex-row justify-between gap-2 w-full">
{isCloud && (
<>
<Link
className="hover:underline text-muted-foreground"
href="/"
>
Login
</Link>
<Link
className="hover:underline text-muted-foreground"
href="/send-reset-password"
>
Lost your password?
</Link>
</>
)}
</div>
</form>
</Form>
</CardContent>
@@ -250,6 +274,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
return {
props: {
isCloud: IS_CLOUD,
token: token,
invitation: invitation,
},

View File

@@ -1,3 +1,4 @@
import { AlertBlock } from "@/components/shared/alert-block";
import { Logo } from "@/components/shared/logo";
import { Button } from "@/components/ui/button";
import {
@@ -72,7 +73,8 @@ interface Props {
const Register = ({ isCloud }: Props) => {
const router = useRouter();
const { mutateAsync, error, isError } = api.auth.createAdmin.useMutation();
const { mutateAsync, error, isError, data } =
api.auth.createAdmin.useMutation();
const form = useForm<Register>({
defaultValues: {
@@ -89,14 +91,16 @@ const Register = ({ isCloud }: Props) => {
const onSubmit = async (values: Register) => {
await mutateAsync({
email: values.email,
email: values.email.toLowerCase(),
password: values.password,
})
.then(() => {
toast.success("User registration succesfuly", {
duration: 2000,
});
router.push("/");
if (!isCloud) {
router.push("/");
}
})
.catch((e) => e);
};
@@ -130,6 +134,14 @@ const Register = ({ isCloud }: Props) => {
</span>
</div>
)}
{data && (
<AlertBlock type="success" className="mx-4 my-2">
<span>
Registration succesfuly, Please check your inbox or spam
folder to confirm your account.
</span>
</AlertBlock>
)}
<CardContent>
<Form {...form}>
<form

View File

@@ -0,0 +1,229 @@
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 { db } from "@/server/db";
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,
},
};
}

View 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: {},
};
}

View File

@@ -15,7 +15,6 @@ const Home: NextPage = () => {
const [spec, setSpec] = useState({});
useEffect(() => {
// Esto solo se ejecutará en el cliente
if (data) {
const protocolAndHost = `${window.location.protocol}//${window.location.host}/api`;
const newSpec = {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 17 KiB

View File

@@ -29,6 +29,7 @@ import { securityRouter } from "./routers/security";
import { serverRouter } from "./routers/server";
import { settingsRouter } from "./routers/settings";
import { sshRouter } from "./routers/ssh-key";
import { stripeRouter } from "./routers/stripe";
import { userRouter } from "./routers/user";
/**
@@ -69,6 +70,7 @@ export const appRouter = createTRPCRouter({
gitlab: gitlabRouter,
github: githubRouter,
server: serverRouter,
stripe: stripeRouter,
});
// export type definition of API

View File

@@ -6,7 +6,6 @@ import {
apiRemoveUser,
users,
} from "@/server/db/schema";
import {
createInvitation,
findAdminById,

View File

@@ -19,11 +19,8 @@ import {
apiUpdateApplication,
applications,
} from "@/server/db/schema";
import {
type DeploymentJob,
cleanQueuesByApplication,
} from "@/server/queues/deployments-queue";
import { myQueue } from "@/server/queues/queueSetup";
import type { DeploymentJob } from "@/server/queues/queue-types";
import { cleanQueuesByApplication, myQueue } from "@/server/queues/queueSetup";
import { deploy } from "@/server/utils/deploy";
import { uploadFileSchema } from "@/utils/schema";
import {

View File

@@ -4,11 +4,13 @@ import {
apiFindOneAuth,
apiLogin,
apiUpdateAuth,
apiUpdateAuthByAdmin,
apiVerify2FA,
apiVerifyLogin2FA,
auth,
} from "@/server/db/schema";
import { WEBSITE_URL } from "@/server/utils/stripe";
import {
type Auth,
IS_CLOUD,
createAdmin,
createUser,
@@ -18,12 +20,18 @@ import {
getUserByToken,
lucia,
luciaToken,
sendDiscordNotification,
sendEmailNotification,
updateAuthById,
validateRequest,
verify2FA,
} from "@dokploy/server";
import { TRPCError } from "@trpc/server";
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 {
adminProcedure,
@@ -47,6 +55,12 @@ export const authRouter = createTRPCRouter({
}
}
const newAdmin = await createAdmin(input);
if (IS_CLOUD) {
await sendDiscordNotificationWelcome(newAdmin);
await sendVerificationEmail(newAdmin.id);
return true;
}
const session = await lucia.createSession(newAdmin.id || "", {});
ctx.res.appendHeader(
"Set-Cookie",
@@ -54,7 +68,12 @@ export const authRouter = createTRPCRouter({
);
return true;
} catch (error) {
throw error;
throw new TRPCError({
code: "BAD_REQUEST",
// @ts-ignore
message: `Error: ${error?.code === "23505" ? "Email already exists" : "Error to create admin"}`,
cause: error,
});
}
}),
createUser: publicProcedure
@@ -68,7 +87,13 @@ export const authRouter = createTRPCRouter({
message: "Invalid token",
});
}
const newUser = await createUser(input);
if (IS_CLOUD) {
await sendVerificationEmail(token.authId);
return true;
}
const session = await lucia.createSession(newUser?.authId || "", {});
ctx.res.appendHeader(
"Set-Cookie",
@@ -100,6 +125,15 @@ export const authRouter = createTRPCRouter({
});
}
if (auth?.confirmationToken && IS_CLOUD) {
await sendVerificationEmail(auth.id);
throw new TRPCError({
code: "BAD_REQUEST",
message:
"Email not confirmed, we have sent you a confirmation email please check your inbox.",
});
}
if (auth?.is2FAEnabled) {
return {
is2FAEnabled: true,
@@ -120,7 +154,7 @@ export const authRouter = createTRPCRouter({
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Credentials do not match",
message: `Error: ${error instanceof Error ? error.message : "Error to login"}`,
cause: error,
});
}
@@ -145,7 +179,7 @@ export const authRouter = createTRPCRouter({
.input(apiUpdateAuth)
.mutation(async ({ ctx, input }) => {
const auth = await updateAuthById(ctx.user.authId, {
...(input.email && { email: input.email }),
...(input.email && { email: input.email.toLowerCase() }),
...(input.password && {
password: bcrypt.hashSync(input.password, 10),
}),
@@ -177,19 +211,6 @@ export const authRouter = createTRPCRouter({
return auth;
}),
updateByAdmin: protectedProcedure
.input(apiUpdateAuthByAdmin)
.mutation(async ({ input }) => {
const auth = await updateAuthById(input.id, {
...(input.email && { email: input.email }),
...(input.password && {
password: bcrypt.hashSync(input.password, 10),
}),
...(input.image && { image: input.image }),
});
return auth;
}),
generate2FASecret: protectedProcedure.query(async ({ ctx }) => {
return await generate2FASecret(ctx.user.authId);
}),
@@ -230,7 +251,208 @@ export const authRouter = createTRPCRouter({
});
return auth;
}),
verifyToken: protectedProcedure.mutation(async () => {
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(),
});
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="${WEBSITE_URL}/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;
}),
confirmEmail: adminProcedure
.input(
z.object({
confirmationToken: z.string().min(1),
}),
)
.mutation(async ({ ctx, input }) => {
if (!IS_CLOUD) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Functionality not available in cloud version",
});
}
const authR = await db.query.auth.findFirst({
where: eq(auth.confirmationToken, input.confirmationToken),
});
if (!authR || authR.confirmationExpiresAt === null) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Token not found",
});
}
if (authR.confirmationToken !== input.confirmationToken) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Confirmation Token not found",
});
}
const isExpired = isBefore(
new Date(authR.confirmationExpiresAt),
new Date(),
);
if (isExpired) {
throw new TRPCError({
code: "NOT_FOUND",
message: "Confirmation Token expired",
});
}
1;
await updateAuthById(authR.id, {
confirmationToken: null,
confirmationExpiresAt: null,
});
return true;
}),
});
export const sendVerificationEmail = async (authId: string) => {
const token = nanoid();
const result = await updateAuthById(authId, {
confirmationToken: token,
confirmationExpiresAt: new Date(
new Date().getTime() + 24 * 60 * 60 * 1000,
).toISOString(),
});
if (!result) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "User not found",
});
}
await sendEmailNotification(
{
fromAddress: process.env.SMTP_FROM_ADDRESS || "",
toAddresses: [result?.email],
smtpServer: process.env.SMTP_SERVER || "",
smtpPort: Number(process.env.SMTP_PORT),
username: process.env.SMTP_USERNAME || "",
password: process.env.SMTP_PASSWORD || "",
},
"Confirm your email | Dokploy",
`
Welcome to Dokploy!
Please confirm your email by clicking the link below:
<a href="${WEBSITE_URL}/confirm-email?token=${result?.confirmationToken}">
Confirm Email
</a>
`,
);
return true;
};
export const sendDiscordNotificationWelcome = async (newAdmin: Auth) => {
await sendDiscordNotification(
{
webhookUrl: process.env.DISCORD_WEBHOOK_URL || "",
},
{
title: "✅ New User Registered",
color: 0x00ff00,
fields: [
{
name: "Email",
value: newAdmin.email,
inline: true,
},
],
timestamp: newAdmin.createdAt,
footer: {
text: "Dokploy User Registration Notification",
},
},
);
};

View File

@@ -14,6 +14,7 @@ import {
findMongoByBackupId,
findMySqlByBackupId,
findPostgresByBackupId,
findServerById,
removeBackupById,
removeScheduleBackup,
runMariadbBackup,
@@ -36,6 +37,25 @@ export const backupRouter = createTRPCRouter({
const backup = await findBackupById(newBackup.backupId);
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({
cronSchedule: backup.schedule,
backupId: backup.backupId,

View File

@@ -9,11 +9,7 @@ import {
apiUpdateCompose,
compose,
} from "@/server/db/schema";
import {
type DeploymentJob,
cleanQueuesByCompose,
} from "@/server/queues/deployments-queue";
import { myQueue } from "@/server/queues/queueSetup";
import { cleanQueuesByCompose, myQueue } from "@/server/queues/queueSetup";
import { templates } from "@/templates/templates";
import type { TemplatesKeys } from "@/templates/types/templates-data.type";
import {
@@ -28,6 +24,7 @@ import _ from "lodash";
import { nanoid } from "nanoid";
import { createTRPCRouter, protectedProcedure } from "../trpc";
import type { DeploymentJob } from "@/server/queues/queue-types";
import { deploy } from "@/server/utils/deploy";
import {
IS_CLOUD,
@@ -41,7 +38,6 @@ import {
createComposeByTemplate,
createDomain,
createMount,
findAdmin,
findAdminById,
findComposeById,
findDomainsByComposeId,
@@ -252,7 +248,6 @@ export const composeRouter = createTRPCRouter({
descriptionLog: "",
server: !!compose.serverId,
};
console.log(jobData);
if (IS_CLOUD && compose.serverId) {
jobData.serverId = compose.serverId;

View File

@@ -12,9 +12,10 @@ import {
destinations,
} from "@/server/db/schema";
import {
IS_CLOUD,
createDestintation,
execAsync,
findAdmin,
execAsyncRemote,
findDestinationById,
removeDestinationById,
updateDestinationById,
@@ -53,11 +54,26 @@ export const destinationRouter = createTRPCRouter({
];
const rcloneDestination = `:s3:${bucket}`;
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) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to connect to bucket",
message:
error instanceof Error
? error?.message
: "Error to connect to bucket",
cause: error,
});
}

View File

@@ -18,6 +18,7 @@ import {
deployMariadb,
findMariadbById,
findProjectById,
findServerById,
removeMariadbById,
removeService,
startService,
@@ -151,6 +152,7 @@ export const mariadbRouter = createTRPCRouter({
message: "You are not authorized to deploy this mariadb",
});
}
return deployMariadb(input.mariadbId);
}),
changeStatus: protectedProcedure

View File

@@ -148,12 +148,6 @@ export const notificationRouter = createTRPCRouter({
.input(apiCreateDiscord)
.mutation(async ({ input, ctx }) => {
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);
} catch (error) {
throw new TRPCError({

View File

@@ -20,10 +20,12 @@ import { and, desc, eq, sql } from "drizzle-orm";
import type { AnyPgColumn } from "drizzle-orm/pg-core";
import {
IS_CLOUD,
addNewProject,
checkProjectAccess,
createProject,
deleteProject,
findAdminById,
findProjectById,
findUserByAuthId,
updateProjectById,
@@ -38,6 +40,15 @@ export const projectRouter = createTRPCRouter({
await checkProjectAccess(ctx.user.authId, "create");
}
const admin = await findAdminById(ctx.user.adminId);
if (admin.serversQuantity === 0 && IS_CLOUD) {
throw new TRPCError({
code: "NOT_FOUND",
message: "No servers available, Please subscribe to a plan",
});
}
const project = await createProject(input, ctx.user.adminId);
if (ctx.user.rol === "user") {
await addNewProject(ctx.user.authId, project.projectId);
@@ -47,7 +58,7 @@ export const projectRouter = createTRPCRouter({
} catch (error) {
throw new TRPCError({
code: "BAD_REQUEST",
message: "Error to create the project",
message: `Error to create the project: ${error instanceof Error ? error.message : error}`,
cause: error,
});
}
@@ -128,9 +139,9 @@ export const projectRouter = createTRPCRouter({
)})`,
with: {
applications: {
where: buildServiceFilter(
applications.applicationId,
accesedServices,
where: and(
buildServiceFilter(applications.applicationId, accesedServices),
eq(projects.adminId, ctx.user.adminId),
),
with: { domains: true },
},
@@ -230,5 +241,5 @@ function buildServiceFilter(fieldName: AnyPgColumn, accesedServices: string[]) {
accesedServices.map((serviceId) => sql`${serviceId}`),
sql`, `,
)})`
: sql`1 = 0`; // Always false condition
: sql`1 = 0`;
}

View File

@@ -1,6 +1,5 @@
import {
apiCreateRegistry,
apiEnableSelfHostedRegistry,
apiFindOneRegistry,
apiRemoveRegistry,
apiTestRegistry,
@@ -13,8 +12,6 @@ import {
execAsyncRemote,
findAllRegistryByAdminId,
findRegistryById,
initializeRegistry,
manageRegistry,
removeRegistry,
updateRegistry,
} from "@dokploy/server";
@@ -84,6 +81,13 @@ export const registryRouter = createTRPCRouter({
try {
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") {
await execAsyncRemote(input.serverId, loginCommand);
} else {
@@ -96,34 +100,4 @@ export const registryRouter = createTRPCRouter({
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;
}),
});

View File

@@ -1,3 +1,4 @@
import { updateServersBasedOnQuantity } from "@/pages/api/stripe/webhook";
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
import { db } from "@/server/db";
import {
@@ -15,15 +16,17 @@ import {
server,
} from "@/server/db/schema";
import {
IS_CLOUD,
createServer,
deleteServer,
findAdminById,
findServerById,
findServersByAdminId,
haveActiveServices,
removeDeploymentsByServerId,
serverSetup,
updateServerById,
} from "@dokploy/server";
// import { serverSetup } from "@/server/setup/server-setup";
import { TRPCError } from "@trpc/server";
import { and, desc, eq, getTableColumns, isNotNull, sql } from "drizzle-orm";
@@ -32,6 +35,14 @@ export const serverRouter = createTRPCRouter({
.input(apiCreateServer)
.mutation(async ({ ctx, input }) => {
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);
return project;
} catch (error) {
@@ -77,13 +88,17 @@ export const serverRouter = createTRPCRouter({
return result;
}),
withSSHKey: protectedProcedure.query(async ({ ctx }) => {
return await db.query.server.findMany({
const result = await db.query.server.findMany({
orderBy: desc(server.createdAt),
where: and(
isNotNull(server.sshKeyId),
eq(server.adminId, ctx.user.adminId),
),
where: IS_CLOUD
? and(
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
.input(apiFindOneServer)
@@ -125,6 +140,15 @@ export const serverRouter = createTRPCRouter({
await removeDeploymentsByServerId(currentServer);
await deleteServer(input.serverId);
if (IS_CLOUD) {
const admin = await findAdminById(ctx.user.adminId);
await updateServersBasedOnQuantity(
admin.adminId,
admin.serversQuantity,
);
}
return currentServer;
} catch (error) {
throw error;
@@ -141,6 +165,13 @@ export const serverRouter = createTRPCRouter({
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, {
...input,
});

View File

@@ -221,6 +221,13 @@ export const settingsRouter = createTRPCRouter({
}
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) {
await schedule({
cronSchedule: "0 0 * * *",
@@ -503,7 +510,7 @@ export const settingsRouter = createTRPCRouter({
if (input?.serverId) {
const result = await execAsyncRemote(input.serverId, command);
stdout = result.stdout;
} else {
} else if (!IS_CLOUD) {
const result = await execAsync(
"docker service inspect --format='{{json .Endpoint.Ports}}' dokploy-traefik",
);
@@ -635,7 +642,7 @@ export const settingsRouter = createTRPCRouter({
return true;
}),
isCloud: adminProcedure.query(async () => {
isCloud: protectedProcedure.query(async () => {
return IS_CLOUD;
}),
health: publicProcedure.query(async () => {

View 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;
}),
});

View File

@@ -1 +1 @@
export * from "@dokploy/server/dist/db/schema";
export * from "@dokploy/server/db/schema";

View File

@@ -11,29 +11,8 @@ import {
updateCompose,
} from "@dokploy/server";
import { type Job, Worker } from "bullmq";
import { myQueue, redisConfig } from "./queueSetup";
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;
import type { DeploymentJob } from "./queue-types";
import { redisConfig } from "./redis-connection";
export const deploymentWorker = new Worker(
"deployments",
@@ -114,25 +93,3 @@ export const deploymentWorker = new Worker(
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}`);
}
}
};

View 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;

View File

@@ -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", {
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 };

View 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",
};

View File

@@ -19,15 +19,12 @@ import { setupDockerContainerLogsWebSocketServer } from "./wss/docker-container-
import { setupDockerContainerTerminalWebSocketServer } from "./wss/docker-container-terminal";
import { setupDockerStatsMonitoringSocketServer } from "./wss/docker-stats";
import { setupDeploymentLogsWebSocketServer } from "./wss/listen-deployment";
import {
getPublicIpWithFallback,
setupTerminalWebSocketServer,
} from "./wss/terminal";
import { setupTerminalWebSocketServer } from "./wss/terminal";
config({ path: ".env" });
const PORT = Number.parseInt(process.env.PORT || "3000", 10);
const dev = process.env.NODE_ENV !== "production";
const app = next({ dev });
const app = next({ dev, turbopack: dev });
const handle = app.getRequestHandler();
void app.prepare().then(async () => {
try {
@@ -40,7 +37,9 @@ void app.prepare().then(async () => {
setupDockerContainerLogsWebSocketServer(server);
setupDockerContainerTerminalWebSocketServer(server);
setupTerminalWebSocketServer(server);
setupDockerStatsMonitoringSocketServer(server);
if (!IS_CLOUD) {
setupDockerStatsMonitoringSocketServer(server);
}
if (process.env.NODE_ENV === "production" && !IS_CLOUD) {
setupDirectories();
@@ -53,7 +52,6 @@ void app.prepare().then(async () => {
await initializeRedis();
initCronJobs();
welcomeServer();
// Timeout to wait for the database to be ready
await new Promise((resolve) => setTimeout(resolve, 7000));
@@ -76,18 +74,3 @@ void app.prepare().then(async () => {
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"),
);
}

View File

@@ -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) => {
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`, {
method: "POST",
headers: {

View File

@@ -0,0 +1,27 @@
export const WEBSITE_URL =
process.env.NODE_ENV === "development"
? "http://localhost:3000"
: process.env.SITE_URL;
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;
};

View File

@@ -3,15 +3,15 @@ import {
createDefaultServerTraefikConfig,
createDefaultTraefikConfig,
initializeTraefik,
} from "@dokploy/server/dist/setup/traefik-setup";
} from "@dokploy/server/setup/traefik-setup";
import { setupDirectories } from "@dokploy/server/dist/setup/config-paths";
import { initializePostgres } from "@dokploy/server/dist/setup/postgres-setup";
import { initializeRedis } from "@dokploy/server/dist/setup/redis-setup";
import { setupDirectories } from "@dokploy/server/setup/config-paths";
import { initializePostgres } from "@dokploy/server/setup/postgres-setup";
import { initializeRedis } from "@dokploy/server/setup/redis-setup";
import {
initializeNetwork,
initializeSwarm,
} from "@dokploy/server/dist/setup/setup";
} from "@dokploy/server/setup/setup";
(async () => {
try {
setupDirectories();

View File

@@ -26,7 +26,8 @@
/* Path Aliases */
"baseUrl": ".",
"paths": {
"@/*": ["./*"]
"@/*": ["./*"],
"@dokploy/server/*": ["../../packages/server/src/*"]
}
},

View File

@@ -9,7 +9,8 @@
"moduleResolution": "Node",
"baseUrl": ".",
"paths": {
"@/*": ["./*"]
"@/*": ["./*"],
"@dokploy/server/*": ["../../packages/server/src/*"]
}
},
"include": ["next-env.d.ts", "./server/**/*"]

View File

@@ -3,6 +3,7 @@ import {
cleanUpSystemPrune,
cleanUpUnusedImages,
findBackupById,
findServerById,
runMariadbBackup,
runMongoBackup,
runMySqlBackup,
@@ -21,22 +22,47 @@ export const runJobs = async (job: QueueJob) => {
const { backupId } = job;
const backup = await findBackupById(backupId);
const { databaseType, postgres, mysql, mongo, mariadb } = backup;
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);
} 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);
} 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);
} 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);
}
}
if (job.type === "server") {
const { serverId } = job;
const server = await findServerById(serverId);
if (server.serverStatus === "inactive") {
logger.info("Server is inactive");
return;
}
await cleanUpUnusedImages(serverId);
await cleanUpDockerBuilder(serverId);
await cleanUpSystemPrune(serverId);
// await sendDockerCleanupNotifications();
}
} catch (error) {
logger.error(error);

View File

@@ -1,8 +1,6 @@
import clsx from "clsx";
import { Inter, Lexend } from "next/font/google";
import "@/styles/tailwind.css";
import GoogleAnalytics from "@/components/analitycs/google";
import { NextIntlClientProvider } from "next-intl";
import { getMessages } from "next-intl/server";
@@ -88,7 +86,14 @@ export default async function RootLayout({
lang={locale}
className={clsx("h-full scroll-smooth", inter.variable, lexend.variable)}
>
<GoogleAnalytics />
<head>
<script
defer
src="https://umami.dokploy.com/script.js"
data-website-id="7d1422e4-3776-4870-8145-7d7b2075d470"
/>
</head>
{/* <GoogleAnalytics /> */}
<body className="flex h-full flex-col">
<NextIntlClientProvider messages={messages}>
<Header />

View File

@@ -1,11 +1,40 @@
import Link from "next/link";
"use client";
import { useTranslations } from "next-intl";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
} from "@/components/ui/select";
import { Link, useRouter } from "@/i18n/routing";
import { useLocale, useTranslations } from "next-intl";
import type { SVGProps } from "react";
import { Container } from "./Container";
import { NavLink } from "./NavLink";
import { Logo } from "./shared/Logo";
import { buttonVariants } from "./ui/button";
const I18nIcon = (props: JSX.IntrinsicAttributes & SVGProps<SVGSVGElement>) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={24}
height={24}
fill="currentColor"
stroke="currentColor"
strokeWidth={0}
viewBox="0 0 512 512"
{...props}
>
<path
stroke="none"
d="m478.33 433.6-90-218a22 22 0 0 0-40.67 0l-90 218a22 22 0 1 0 40.67 16.79L316.66 406h102.67l18.33 44.39A22 22 0 0 0 458 464a22 22 0 0 0 20.32-30.4zM334.83 362 368 281.65 401.17 362zm-66.99-19.08a22 22 0 0 0-4.89-30.7c-.2-.15-15-11.13-36.49-34.73 39.65-53.68 62.11-114.75 71.27-143.49H330a22 22 0 0 0 0-44H214V70a22 22 0 0 0-44 0v20H54a22 22 0 0 0 0 44h197.25c-9.52 26.95-27.05 69.5-53.79 108.36-31.41-41.68-43.08-68.65-43.17-68.87a22 22 0 0 0-40.58 17c.58 1.38 14.55 34.23 52.86 83.93.92 1.19 1.83 2.35 2.74 3.51-39.24 44.35-77.74 71.86-93.85 80.74a22 22 0 1 0 21.07 38.63c2.16-1.18 48.6-26.89 101.63-85.59 22.52 24.08 38 35.44 38.93 36.1a22 22 0 0 0 30.75-4.9z"
/>
</svg>
);
export function Footer() {
const router = useRouter();
const locale = useLocale();
const t = useTranslations("HomePage");
const linkT = useTranslations("Link");
@@ -31,7 +60,7 @@ export function Footer() {
</nav>
</div>
<div className="flex flex-col items-center border-t border-slate-400/10 py-10 sm:flex-row-reverse sm:justify-between">
<div className="flex gap-x-6">
<div className="flex gap-x-6 items-center">
<Link
href="https://x.com/getdokploy"
className="group"
@@ -56,6 +85,30 @@ export function Footer() {
<path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0 1 12 6.844a9.59 9.59 0 0 1 2.504.337c1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.02 10.02 0 0 0 22 12.017C22 6.484 17.522 2 12 2Z" />
</svg>
</Link>
<Select
onValueChange={(locale) => {
router.replace("/", {
locale: locale as "en" | "zh-Hans",
});
}}
value={locale}
>
<SelectTrigger
className={buttonVariants({
variant: "outline",
className:
" flex items-center gap-2 !rounded-full visited:outline-none focus-within:outline-none focus:outline-none",
})}
>
<I18nIcon width={20} height={20} />
</SelectTrigger>
<SelectContent>
<SelectItem value="en">{t("navigation.i18nEn")}</SelectItem>
<SelectItem value="zh-Hans">
{t("navigation.i18nZh-Hans")}
</SelectItem>
</SelectContent>
</Select>
</div>
<p className="mt-6 text-sm text-muted-foreground sm:mt-0">
{t("footer.copyright", {

View File

@@ -1,16 +1,10 @@
"use client";
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
} from "@/components/ui/select";
import { Link, useRouter } from "@/i18n/routing";
import { Link } from "@/i18n/routing";
import { cn } from "@/lib/utils";
import { Popover, Transition } from "@headlessui/react";
import { HeartIcon } from "lucide-react";
import { useLocale, useTranslations } from "next-intl";
import { useTranslations } from "next-intl";
import { Fragment, type JSX, type SVGProps } from "react";
import { Container } from "./Container";
import { NavLink } from "./NavLink";
@@ -125,10 +119,7 @@ function MobileNavigation() {
as="div"
className="absolute inset-x-0 top-full mt-4 flex origin-top flex-col rounded-2xl border border-border bg-background p-4 text-lg tracking-tight text-primary shadow-xl ring-1 ring-border/5"
>
<MobileNavLink href="/#features">
{t("navigation.features")}
</MobileNavLink>
{/* <MobileNavLink href="/#testimonials">Testimonials</MobileNavLink> */}
<MobileNavLink href="/pricing">Pricing</MobileNavLink>
<MobileNavLink href="/#faqs">{t("navigation.faqs")}</MobileNavLink>
<MobileNavLink href={linkT("docs.intro")} target="_blank">
{t("navigation.docs")}
@@ -141,8 +132,6 @@ function MobileNavigation() {
}
export function Header() {
const router = useRouter();
const locale = useLocale();
const t = useTranslations("HomePage");
const linkT = useTranslations("Link");
@@ -155,8 +144,7 @@ export function Header() {
<Logo className="h-10 w-auto" />
</Link>
<div className="hidden md:flex md:gap-x-6">
<NavLink href="/#features">{t("navigation.features")}</NavLink>
{/* <NavLink href="/#testimonials">Testimonials</NavLink> */}
<NavLink href="/pricing">{t("navigation.pricing")}</NavLink>
<NavLink href="/#faqs">{t("navigation.faqs")}</NavLink>
<NavLink href={linkT("docs.intro")} target="_blank">
{t("navigation.docs")}
@@ -164,31 +152,6 @@ export function Header() {
</div>
</div>
<div className="flex items-center gap-x-2 md:gap-x-5">
<Select
onValueChange={(locale) => {
router.replace("/", {
locale: locale as "en" | "zh-Hans",
});
}}
value={locale}
>
<SelectTrigger
className={buttonVariants({
variant: "outline",
className:
" flex items-center gap-2 !rounded-full visited:outline-none focus-within:outline-none focus:outline-none",
})}
>
<I18nIcon width={20} height={20} />
</SelectTrigger>
<SelectContent>
<SelectItem value="en">{t("navigation.i18nEn")}</SelectItem>
<SelectItem value="zh-Hans">
{t("navigation.i18nZh-Hans")}
</SelectItem>
</SelectContent>
</Select>
<Link
className={buttonVariants({
variant: "outline",
@@ -202,25 +165,14 @@ export function Header() {
</span>
<HeartIcon className="animate-heartbeat size-4 fill-red-600 text-red-500 " />
</Link>
<Button
className="rounded-full bg-[#5965F2] hover:bg-[#4A55E0]"
asChild
>
<Button className="rounded-xl" asChild>
<Link
href="https://discord.gg/2tBnJ3jDJc"
href="https://app.dokploy.com"
aria-label="Dokploy on GitHub"
target="_blank"
className="flex flex-row items-center gap-2 text-white"
// className="flex flex-row items-center gap-2 text-white"
>
<svg
role="img"
className="h-6 w-6 fill-white"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z" />
</svg>
{t("navigation.discord")}
{t("navigation.dashboard")}
</Link>
</Button>
<div className="-mr-1 md:hidden">

View File

@@ -1,10 +1,13 @@
"use client";
import { Check, Copy } from "lucide-react";
import { cn } from "@/lib/utils";
import { ArrowRight, ArrowRightIcon, Check, Copy } from "lucide-react";
import { useTranslations } from "next-intl";
import Link from "next/link";
import { useEffect, useState } from "react";
import { Container } from "./Container";
import AnimatedShinyText from "./ui/animated-shiny-text";
import { Button } from "./ui/button";
import { HoverBorderGradient } from "./ui/hover-border-gradient";
const ProductHunt = () => {
return (
@@ -69,7 +72,22 @@ export function Hero() {
</div>
<div className="relative">
<h1 className="mx-auto max-w-4xl font-display text-5xl font-medium tracking-tight text-muted-foreground sm:text-7xl">
<Link href="/pricing" className="relative z-10 mb-4 inline-block">
<div className="flex items-center justify-center">
<div
className={cn(
"group rounded-full border border-black/5 bg-neutral-100 text-sm font-medium text-white transition-all ease-in hover:cursor-pointer hover:bg-neutral-200 dark:border-white/5 dark:bg-neutral-900 dark:hover:bg-neutral-800",
)}
>
<AnimatedShinyText className="inline-flex items-center justify-center px-4 py-1 text-neutral-800 transition ease-out hover:text-neutral-900 hover:duration-300 hover:dark:text-neutral-400">
<span>🚀 {t("hero.cloud")} </span>
<ArrowRightIcon className="ml-1 size-3 transition-transform duration-300 ease-in-out group-hover:translate-x-0.5" />
</AnimatedShinyText>
</div>
</div>
</Link>
<h1 className="mx-auto max-w-4xl font-display text-5xl font-medium tracking-tight text-muted-foreground sm:text-7xl">
{t("hero.deploy")}{" "}
<span className="relative whitespace-nowrap text-primary">
<svg
@@ -125,7 +143,9 @@ export function Hero() {
Discord
</Link>
</Button> */}
<Button className="rounded-xl" asChild>
</div>
<div className="mx-auto flex w-full max-w-sm flex-wrap items-center justify-center gap-3 md:flex-nowrap">
<Button className="w-full rounded-xl" asChild>
<Link
href="https://github.com/dokploy/dokploy"
aria-label="Dokploy on GitHub"
@@ -138,6 +158,27 @@ export function Hero() {
Github
</Link>
</Button>
<Button
className="w-full rounded-xl bg-[#5965F2] hover:bg-[#4A55E0]"
asChild
>
<Link
href="https://discord.gg/2tBnJ3jDJc"
aria-label="Dokploy on GitHub"
target="_blank"
className="flex flex-row items-center gap-2 text-white"
>
<svg
role="img"
className="h-6 w-6 fill-white"
viewBox="0 0 24 24"
xmlns="http://www.w3.org/2000/svg"
>
<path d="M20.317 4.3698a19.7913 19.7913 0 00-4.8851-1.5152.0741.0741 0 00-.0785.0371c-.211.3753-.4447.8648-.6083 1.2495-1.8447-.2762-3.68-.2762-5.4868 0-.1636-.3933-.4058-.8742-.6177-1.2495a.077.077 0 00-.0785-.037 19.7363 19.7363 0 00-4.8852 1.515.0699.0699 0 00-.0321.0277C.5334 9.0458-.319 13.5799.0992 18.0578a.0824.0824 0 00.0312.0561c2.0528 1.5076 4.0413 2.4228 5.9929 3.0294a.0777.0777 0 00.0842-.0276c.4616-.6304.8731-1.2952 1.226-1.9942a.076.076 0 00-.0416-.1057c-.6528-.2476-1.2743-.5495-1.8722-.8923a.077.077 0 01-.0076-.1277c.1258-.0943.2517-.1923.3718-.2914a.0743.0743 0 01.0776-.0105c3.9278 1.7933 8.18 1.7933 12.0614 0a.0739.0739 0 01.0785.0095c.1202.099.246.1981.3728.2924a.077.077 0 01-.0066.1276 12.2986 12.2986 0 01-1.873.8914.0766.0766 0 00-.0407.1067c.3604.698.7719 1.3628 1.225 1.9932a.076.076 0 00.0842.0286c1.961-.6067 3.9495-1.5219 6.0023-3.0294a.077.077 0 00.0313-.0552c.5004-5.177-.8382-9.6739-3.5485-13.6604a.061.061 0 00-.0312-.0286zM8.02 15.3312c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9555-2.4189 2.157-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.9555 2.4189-2.1569 2.4189zm7.9748 0c-1.1825 0-2.1569-1.0857-2.1569-2.419 0-1.3332.9554-2.4189 2.1569-2.4189 1.2108 0 2.1757 1.0952 2.1568 2.419 0 1.3332-.946 2.4189-2.1568 2.4189Z" />
</svg>
{t("navigation.discord")}
</Link>
</Button>
</div>
</div>
<div className="mt-16 flex flex-row justify-center gap-x-8 rounded-lg sm:gap-x-0 sm:gap-y-10 xl:gap-x-12 xl:gap-y-0">
@@ -182,22 +223,20 @@ export function Hero() {
}
export const ShowSponsors = () => {
const t = useTranslations("HomePage");
return (
<div className="mt-20 flex flex-col justify-center gap-y-10">
<div className="flex flex-col gap-4 justify-start">
<div className="flex flex-col justify-start gap-4">
<h1 className="mx-auto max-w-2xl font-display text-3xl font-medium tracking-tight text-primary sm:text-5xl">
Sponsors
{t("hero.sponsors.title")}
</h1>
<p className="mx-auto max-w-2xl text-lg tracking-tight text-muted-foreground">
Dokploy is an open source project that is maintained by a community of
volunteers. We would like to thank our sponsors for their support and
contributions to the project, which help us to continue to develop and
improve Dokploy.
{t("hero.sponsors.description")}
</p>
</div>
<div className="flex flex-col gap-4 md:gap-6 justify-start">
<h2 className="font-display text-2xl font-medium tracking-tight text-primary sm:text-2xl text-left">
Hero Sponsors 🎖
<div className="flex flex-col items-center justify-start gap-4 md:gap-6">
<h2 className="text-left font-display text-2xl font-medium tracking-tight text-primary sm:text-2xl">
{t("hero.sponsors.level.hero")} 🎖
</h2>
<div className="flex flex-wrap items-center gap-4">
<a
@@ -209,7 +248,7 @@ export const ShowSponsors = () => {
<img
src="https://raw.githubusercontent.com/Dokploy/dokploy/canary/.github/sponsors/hostinger.jpg"
alt="hostinger.com"
className="rounded-xl w-[190px] h-auto"
className="h-auto w-[190px] rounded-xl"
/>
</a>
<a
@@ -221,16 +260,16 @@ export const ShowSponsors = () => {
<img
src="https://raw.githubusercontent.com/Dokploy/dokploy/canary/.github/sponsors/lxaer.png"
alt="lxaer.com"
className="rounded-xl w-[70px] h-auto"
className="h-auto w-[70px] rounded-xl"
/>
</a>
</div>
</div>
<div className="flex flex-col gap-4 md:gap-8 justify-start">
<h2 className="font-display text-2xl font-medium tracking-tight text-primary sm:text-2xl text-left">
Premium Supporters 🥇
<div className="flex flex-col items-center justify-start gap-4 md:gap-8">
<h2 className="text-left font-display text-2xl font-medium tracking-tight text-primary sm:text-2xl">
{t("hero.sponsors.level.premium")} 🥇
</h2>
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4">
<div className="flex flex-col items-center justify-start gap-4 md:gap-6">
<a
href="https://supafort.com/?ref=dokploy"
target="_blank"
@@ -245,9 +284,9 @@ export const ShowSponsors = () => {
</a>
</div>
</div>
<div className="flex flex-col gap-4 md:gap-8 justify-start">
<h2 className="font-display text-2xl font-medium tracking-tight text-primary sm:text-2xl text-left">
Supporting Members 🥉
<div className="flex flex-col items-center justify-start gap-4 md:gap-8">
<h2 className="text-left font-display text-2xl font-medium tracking-tight text-primary sm:text-2xl">
{t("hero.sponsors.level.supporting")} 🥉
</h2>
<div className="flex flex-row gap-10">
<a
@@ -276,9 +315,9 @@ export const ShowSponsors = () => {
</a>
</div>
</div>
<div className="flex flex-col gap-4 md:gap-8 justify-start">
<h2 className="font-display text-2xl font-medium tracking-tight text-primary sm:text-2xl text-left">
Community Backers 🤝
<div className="justify-star flex flex-col items-center gap-4 md:gap-8">
<h2 className="text-left font-display text-2xl font-medium tracking-tight text-primary sm:text-2xl">
{t("hero.sponsors.level.community")} 🤝
</h2>
<div className="flex flex-row gap-10">
<a
@@ -305,11 +344,24 @@ export const ShowSponsors = () => {
alt="Rivo.gg"
/>
</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 className="flex flex-col gap-4 md:gap-8 justify-start">
<h2 className="font-display text-2xl font-medium tracking-tight text-primary sm:text-2xl text-left">
Organizations:
<div className="flex flex-col items-center justify-start gap-4 md:gap-8">
<h2 className="text-left font-display text-2xl font-medium tracking-tight text-primary sm:text-2xl">
{t("hero.sponsors.level.organizations")}
</h2>
<div className="flex flex-row gap-10">
<a
@@ -324,9 +376,9 @@ export const ShowSponsors = () => {
</a>
</div>
</div>
<div className="flex flex-col gap-4 md:gap-8 justify-start">
<h2 className="font-display text-2xl font-medium tracking-tight text-primary sm:text-2xl text-left">
Individuals:
<div className="flex flex-col items-center justify-start gap-4 md:gap-8">
<h2 className="text-left font-display text-2xl font-medium tracking-tight text-primary sm:text-2xl">
{t("hero.sponsors.level.individuals")}
</h2>
<div className="flex flex-row gap-10">
<a

View File

@@ -1,7 +1,5 @@
import { Link } from "@/i18n/routing";
import { useTranslations } from "next-intl";
import { Footer } from "./Footer";
import { Header } from "./Header";
export function SlimLayout() {
const t = useTranslations("404");
@@ -16,6 +14,7 @@ export function SlimLayout() {
<Link href="/" className="text-primary">
{t("action")}
</Link>
p{" "}
</p>
</div>
</main>

File diff suppressed because one or more lines are too long

Some files were not shown because too many files have changed in this diff Show More