mirror of
https://github.com/Dokploy/dokploy
synced 2025-06-26 18:27:59 +00:00
Compare commits
84 Commits
v0.10.2
...
481-when-a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1a6bf06929 | ||
|
|
ecc98b0944 | ||
|
|
950ee77a49 | ||
|
|
38a72806a7 | ||
|
|
fa70696c71 | ||
|
|
b123baafa4 | ||
|
|
d6fa416a3f | ||
|
|
d2c0f19353 | ||
|
|
3ef6c711e6 | ||
|
|
64a4d51f9c | ||
|
|
1a20e4f813 | ||
|
|
250c14738c | ||
|
|
28221a4e7a | ||
|
|
680c22a41e | ||
|
|
428b9cf0ec | ||
|
|
78659b2ad9 | ||
|
|
4b5408c050 | ||
|
|
5417f6376b | ||
|
|
27c33c7661 | ||
|
|
0873618c63 | ||
|
|
e3b20268d5 | ||
|
|
742e5a244d | ||
|
|
1f208f5f5b | ||
|
|
a53ade88e7 | ||
|
|
77e9617770 | ||
|
|
21e97b0175 | ||
|
|
a6618a14d5 | ||
|
|
59308ab013 | ||
|
|
e19c8d7a7a | ||
|
|
421c93795b | ||
|
|
182f908c31 | ||
|
|
20616363e9 | ||
|
|
d85073b26d | ||
|
|
303d1b1b87 | ||
|
|
60d4e1ba63 | ||
|
|
83d52b68f0 | ||
|
|
af3b1a27f4 | ||
|
|
7f94593c07 | ||
|
|
5df7654873 | ||
|
|
054836fd4c | ||
|
|
484ead1f1f | ||
|
|
fbada4c5de | ||
|
|
491113416b | ||
|
|
c42f5cb799 | ||
|
|
47aa223f87 | ||
|
|
cb586c9b74 | ||
|
|
554ac59b97 | ||
|
|
0247898876 | ||
|
|
fc2778db35 | ||
|
|
85d6ff9012 | ||
|
|
fa053b4d1f | ||
|
|
522f8baec7 | ||
|
|
bcc7afa3e4 | ||
|
|
647a5d05a6 | ||
|
|
e15d41f80d | ||
|
|
6c7c919d49 | ||
|
|
22eb965919 | ||
|
|
c0746b95b3 | ||
|
|
dfdedf9e48 | ||
|
|
7e76eb4dd1 | ||
|
|
c1f777e23e | ||
|
|
975d13c7e1 | ||
|
|
017bdd2778 | ||
|
|
548df8c0f4 | ||
|
|
1e6dbb5e8e | ||
|
|
431dadb6c2 | ||
|
|
22e42b62ad | ||
|
|
49ee8ce132 | ||
|
|
9bace8e58b | ||
|
|
c0afcaf3f6 | ||
|
|
53edf06476 | ||
|
|
255e9e4095 | ||
|
|
03f923c6e2 | ||
|
|
4685ef7439 | ||
|
|
626cfb80b4 | ||
|
|
9591fbff08 | ||
|
|
fbda00f059 | ||
|
|
1907e7e59c | ||
|
|
ffe7b04bea | ||
|
|
fe0a662afd | ||
|
|
319584d911 | ||
|
|
2d40b2dfe5 | ||
|
|
e32b713742 | ||
|
|
988357fb55 |
10
.github/workflows/deploy.yml
vendored
10
.github/workflows/deploy.yml
vendored
@@ -70,8 +70,12 @@ jobs:
|
|||||||
file: ./Dockerfile.cloud
|
file: ./Dockerfile.cloud
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
siumauricio/cloud:${{ github.ref_name == 'main' && 'main' || 'canary' }}
|
siumauricio/cloud:${{ github.ref_name == 'main' && 'latest' || 'canary' }}
|
||||||
platforms: linux/amd64
|
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:
|
build-and-push-schedule-image:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -93,7 +97,7 @@ jobs:
|
|||||||
file: ./Dockerfile.schedule
|
file: ./Dockerfile.schedule
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
siumauricio/schedule:${{ github.ref_name == 'main' && 'main' || 'canary' }}
|
siumauricio/schedule:${{ github.ref_name == 'main' && 'latest' || 'canary' }}
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
|
|
||||||
|
|
||||||
@@ -117,5 +121,5 @@ jobs:
|
|||||||
file: ./Dockerfile.server
|
file: ./Dockerfile.server
|
||||||
push: true
|
push: true
|
||||||
tags: |
|
tags: |
|
||||||
siumauricio/server:${{ github.ref_name == 'main' && 'main' || 'canary' }}
|
siumauricio/server:${{ github.ref_name == 'main' && 'latest' || 'canary' }}
|
||||||
platforms: linux/amd64
|
platforms: linux/amd64
|
||||||
@@ -71,10 +71,9 @@ Run the command that will spin up all the required services and files.
|
|||||||
pnpm run dokploy:setup
|
pnpm run dokploy:setup
|
||||||
```
|
```
|
||||||
|
|
||||||
Build the server package (If you make any changes after in the packages/server folder, you need to rebuild and run this command)
|
Run this script
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
pnpm run server:build
|
pnpm run server:script
|
||||||
```
|
```
|
||||||
|
|
||||||
Now run the development server.
|
Now run the development server.
|
||||||
|
|||||||
@@ -12,7 +12,16 @@ RUN apt-get update && apt-get install -y python3 make g++ git && rm -rf /var/lib
|
|||||||
# Install dependencies
|
# Install dependencies
|
||||||
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --filter=@dokploy/server --filter=./apps/dokploy install --frozen-lockfile
|
RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm --filter=@dokploy/server --filter=./apps/dokploy install --frozen-lockfile
|
||||||
|
|
||||||
|
|
||||||
# Deploy only the dokploy app
|
# 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
|
ENV NODE_ENV=production
|
||||||
RUN pnpm --filter=@dokploy/server build
|
RUN pnpm --filter=@dokploy/server build
|
||||||
|
|||||||
24
README.md
24
README.md
@@ -15,29 +15,29 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<br />
|
<br />
|
||||||
Dokploy is a free self-hostable Platform as a Service (PaaS) that simplifies the deployment and management of applications and databases.
|
Dokploy is a free, self-hostable Platform as a Service (PaaS) that simplifies the deployment and management of applications and databases.
|
||||||
|
|
||||||
### Features
|
### Features
|
||||||
|
|
||||||
Dokploy include multiples features to make your life easier.
|
Dokploy includes multiple features to make your life easier.
|
||||||
|
|
||||||
- **Applications**: Deploy any type of application (Node.js, PHP, Python, Go, Ruby, etc.).
|
- **Applications**: Deploy any type of application (Node.js, PHP, Python, Go, Ruby, etc.).
|
||||||
- **Databases**: Create and manage databases with support for MySQL, PostgreSQL, MongoDB, MariaDB, Redis.
|
- **Databases**: Create and manage databases with support for MySQL, PostgreSQL, MongoDB, MariaDB, and Redis.
|
||||||
- **Backups**: Automate backups for databases to a external storage destination.
|
- **Backups**: Automate backups for databases to an external storage destination.
|
||||||
- **Docker Compose**: Native support for Docker Compose to manage complex applications.
|
- **Docker Compose**: Native support for Docker Compose to manage complex applications.
|
||||||
- **Multi Node**: Scale applications to multiples nodes using docker swarm to manage the cluster.
|
- **Multi Node**: Scale applications to multiple nodes using Docker Swarm to manage the cluster.
|
||||||
- **Templates**: Deploy in a single click open source templates (Plausible, Pocketbase, Calcom, etc.).
|
- **Templates**: Deploy open-source templates (Plausible, Pocketbase, Calcom, etc.) with a single click.
|
||||||
- **Traefik Integration**: Automatically integrates with Traefik for routing and load balancing.
|
- **Traefik Integration**: Automatically integrates with Traefik for routing and load balancing.
|
||||||
- **Real-time Monitoring**: Monitor CPU, memory, storage, and network usage, for every resource.
|
- **Real-time Monitoring**: Monitor CPU, memory, storage, and network usage for every resource.
|
||||||
- **Docker Management**: Easily deploy and manage Docker containers.
|
- **Docker Management**: Easily deploy and manage Docker containers.
|
||||||
- **CLI/API**: Manage your applications and databases using the command line or trought the API.
|
- **CLI/API**: Manage your applications and databases using the command line or through the API.
|
||||||
- **Notifications**: Get notified when your deployments are successful or failed (Slack, Discord, Telegram, Email, etc.)
|
- **Notifications**: Get notified when your deployments succeed or fail (via Slack, Discord, Telegram, Email, etc.).
|
||||||
- **Multi Server**: Deploy and manager your applications remotely to external servers.
|
- **Multi Server**: Deploy and manage your applications remotely to external servers.
|
||||||
- **Self-Hosted**: Self-host Dokploy on your VPS.
|
- **Self-Hosted**: Self-host Dokploy on your VPS.
|
||||||
|
|
||||||
## 🚀 Getting Started
|
## 🚀 Getting Started
|
||||||
|
|
||||||
To get started run the following command in a VPS:
|
To get started, run the following command on a VPS:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
curl -sSL https://dokploy.com/install.sh | sh
|
curl -sSL https://dokploy.com/install.sh | sh
|
||||||
@@ -90,6 +90,8 @@ For detailed documentation, visit [docs.dokploy.com](https://docs.dokploy.com).
|
|||||||
<div style="display: flex; gap: 30px; flex-wrap: wrap;">
|
<div style="display: flex; gap: 30px; flex-wrap: wrap;">
|
||||||
<a href="https://steamsets.com/?ref=dokploy"><img src="https://avatars.githubusercontent.com/u/111978405?s=200&v=4" width="60px" alt="Steamsets.com"/></a>
|
<a href="https://steamsets.com/?ref=dokploy"><img src="https://avatars.githubusercontent.com/u/111978405?s=200&v=4" width="60px" alt="Steamsets.com"/></a>
|
||||||
<a href="https://rivo.gg/?ref=dokploy"><img src="https://avatars.githubusercontent.com/u/126797452?s=200&v=4" width="60px" alt="Rivo.gg"/></a>
|
<a href="https://rivo.gg/?ref=dokploy"><img src="https://avatars.githubusercontent.com/u/126797452?s=200&v=4" width="60px" alt="Rivo.gg"/></a>
|
||||||
|
<a href="https://photoquest.wedding/?ref=dokploy"><img src="https://photoquest.wedding/favicon/android-chrome-512x512.png" width="60px" alt="Rivo.gg"/></a>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
#### Organizations:
|
#### Organizations:
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ export const deployJobSchema = z.discriminatedUnion("applicationType", [
|
|||||||
server: z.boolean().optional(),
|
server: z.boolean().optional(),
|
||||||
type: z.enum(["deploy", "redeploy"]),
|
type: z.enum(["deploy", "redeploy"]),
|
||||||
applicationType: z.literal("application"),
|
applicationType: z.literal("application"),
|
||||||
serverId: z.string(),
|
serverId: z.string().min(1),
|
||||||
}),
|
}),
|
||||||
z.object({
|
z.object({
|
||||||
composeId: z.string(),
|
composeId: z.string(),
|
||||||
@@ -17,7 +17,7 @@ export const deployJobSchema = z.discriminatedUnion("applicationType", [
|
|||||||
server: z.boolean().optional(),
|
server: z.boolean().optional(),
|
||||||
type: z.enum(["deploy", "redeploy"]),
|
type: z.enum(["deploy", "redeploy"]),
|
||||||
applicationType: z.literal("compose"),
|
applicationType: z.literal("compose"),
|
||||||
serverId: z.string(),
|
serverId: z.string().min(1),
|
||||||
}),
|
}),
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
|||||||
@@ -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;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,45 +1,12 @@
|
|||||||
import {
|
import {
|
||||||
deployApplication,
|
|
||||||
deployCompose,
|
|
||||||
deployRemoteApplication,
|
deployRemoteApplication,
|
||||||
deployRemoteCompose,
|
deployRemoteCompose,
|
||||||
rebuildApplication,
|
|
||||||
rebuildCompose,
|
|
||||||
rebuildRemoteApplication,
|
rebuildRemoteApplication,
|
||||||
rebuildRemoteCompose,
|
rebuildRemoteCompose,
|
||||||
updateApplicationStatus,
|
updateApplicationStatus,
|
||||||
updateCompose,
|
updateCompose,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import type { DeployJob } from "./schema";
|
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) => {
|
export const deploy = async (job: DeployJob) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
PlugZapIcon,
|
PlugZapIcon,
|
||||||
TerminalIcon,
|
TerminalIcon,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import Script from "next/script";
|
||||||
const inter = Inter({
|
const inter = Inter({
|
||||||
subsets: ["latin"],
|
subsets: ["latin"],
|
||||||
});
|
});
|
||||||
@@ -63,6 +64,10 @@ export default function Layout({
|
|||||||
className={inter.className}
|
className={inter.className}
|
||||||
suppressHydrationWarning
|
suppressHydrationWarning
|
||||||
>
|
>
|
||||||
|
<Script
|
||||||
|
src="https://umami.dokploy.com/script.js"
|
||||||
|
data-website-id="6ad2aa56-6d38-4f39-97a8-1a8fcdda8d51"
|
||||||
|
/>
|
||||||
<GoogleAnalytics />
|
<GoogleAnalytics />
|
||||||
<body>
|
<body>
|
||||||
<I18nProvider
|
<I18nProvider
|
||||||
|
|||||||
@@ -15,13 +15,13 @@ description: '学习如何在服务器上手动安装 Dokploy。'
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
#!/bin/bash
|
#!/bin/bash
|
||||||
# 确保以根用户身份运行脚本
|
# Ensure the script is run as root
|
||||||
if [ "$(id -u)" != "0" ]; then
|
if [ "$(id -u)" != "0" ]; then
|
||||||
echo "This script must be run as root" >&2
|
echo "This script must be run as root" >&2
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 检查 Linux 操作系统(非 macOS 或 Docker 容器内的操作系统)
|
# Check for Linux OS (not macOS or inside a Docker container)
|
||||||
if [ "$(uname)" = "Darwin" ]; then
|
if [ "$(uname)" = "Darwin" ]; then
|
||||||
echo "This script must be run on Linux" >&2
|
echo "This script must be run on Linux" >&2
|
||||||
exit 1
|
exit 1
|
||||||
@@ -32,7 +32,7 @@ if [ -f /.dockerenv ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 检查端口是否被占用
|
# Check for occupied ports
|
||||||
if ss -tulnp | grep ':80 ' >/dev/null; then
|
if ss -tulnp | grep ':80 ' >/dev/null; then
|
||||||
echo "Error: Port 80 is already in use" >&2
|
echo "Error: Port 80 is already in use" >&2
|
||||||
exit 1
|
exit 1
|
||||||
@@ -43,32 +43,53 @@ if ss -tulnp | grep ':443 ' >/dev/null; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 检查命令是否存在
|
# Function to check if a command exists
|
||||||
command_exists() {
|
command_exists() {
|
||||||
command -v "$@" > /dev/null 2>&1
|
command -v "$@" > /dev/null 2>&1
|
||||||
}
|
}
|
||||||
|
|
||||||
# 如果未安装 Docker 则安装
|
# Install Docker if it is not installed
|
||||||
if command_exists docker; then
|
if command_exists docker; then
|
||||||
echo "Docker already installed"
|
echo "Docker already installed"
|
||||||
else
|
else
|
||||||
curl -sSL https://get.docker.com | sh
|
curl -sSL https://get.docker.com | sh
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# 初始化 Docker Swarm
|
# Initialize Docker Swarm
|
||||||
docker swarm leave --force 2>/dev/null
|
docker swarm leave --force 2>/dev/null
|
||||||
advertise_addr=$(curl -s ifconfig.me)
|
|
||||||
docker swarm init --advertise-addr $advertise_addr
|
|
||||||
echo "Swarm initialized"
|
|
||||||
|
|
||||||
# 创建网络
|
get_ip() {
|
||||||
docker network rm -f dokploy-network 2>/dev/null
|
# Try to get IPv4
|
||||||
docker network create --driver overlay --attachable dokploy-network
|
local ipv4=$(curl -4s https://ifconfig.io 2>/dev/null)
|
||||||
echo "Network created"
|
|
||||||
|
|
||||||
# 准备配置目录
|
if [ -n "$ipv4" ]; then
|
||||||
mkdir -p /etc/dokploy
|
echo "$ipv4"
|
||||||
chmod -R 777 /etc/dokploy
|
else
|
||||||
|
# Try to get IPv6
|
||||||
|
local ipv6=$(curl -6s https://ifconfig.io 2>/dev/null)
|
||||||
|
if [ -n "$ipv6" ]; then
|
||||||
|
echo "$ipv6"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
advertise_addr="${ADVERTISE_ADDR:-$(get_ip)}"
|
||||||
|
|
||||||
|
docker swarm init --advertise-addr $advertise_addr
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Error: Failed to initialize Docker Swarm" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker network rm -f dokploy-network 2>/dev/null
|
||||||
|
docker network create --driver overlay --attachable dokploy-network
|
||||||
|
|
||||||
|
echo "Network created"
|
||||||
|
|
||||||
|
mkdir -p /etc/dokploy
|
||||||
|
|
||||||
|
chmod 777 /etc/dokploy
|
||||||
|
|
||||||
# Pull and deploy Dokploy
|
# Pull and deploy Dokploy
|
||||||
docker pull dokploy/dokploy:latest
|
docker pull dokploy/dokploy:latest
|
||||||
@@ -84,9 +105,10 @@ docker service create \
|
|||||||
-e PORT=<Value For PORT eg(3000)> \
|
-e PORT=<Value For PORT eg(3000)> \
|
||||||
-e TRAEFIK_SSL_PORT=<Value For SSL PORT eg(444)> \
|
-e TRAEFIK_SSL_PORT=<Value For SSL PORT eg(444)> \
|
||||||
-e TRAEFIK_PORT=<VALUE FOR TRAEFIK HTTP PORT eg(81)> \
|
-e TRAEFIK_PORT=<VALUE FOR TRAEFIK HTTP PORT eg(81)> \
|
||||||
|
-e ADVERTISE_ADDR=$advertise_addr \
|
||||||
dokploy/dokploy:latest
|
dokploy/dokploy:latest
|
||||||
|
|
||||||
# 输出成功消息
|
# Output success message
|
||||||
GREEN="\033[0;32m"
|
GREEN="\033[0;32m"
|
||||||
YELLOW="\033[1;33m"
|
YELLOW="\033[1;33m"
|
||||||
BLUE="\033[0;34m"
|
BLUE="\033[0;34m"
|
||||||
|
|||||||
@@ -57,18 +57,39 @@ fi
|
|||||||
|
|
||||||
# Initialize Docker Swarm
|
# Initialize Docker Swarm
|
||||||
docker swarm leave --force 2>/dev/null
|
docker swarm leave --force 2>/dev/null
|
||||||
advertise_addr=$(curl -s ifconfig.me)
|
|
||||||
docker swarm init --advertise-addr $advertise_addr
|
|
||||||
echo "Swarm initialized"
|
|
||||||
|
|
||||||
# Create network
|
get_ip() {
|
||||||
docker network rm -f dokploy-network 2>/dev/null
|
# Try to get IPv4
|
||||||
docker network create --driver overlay --attachable dokploy-network
|
local ipv4=$(curl -4s https://ifconfig.io 2>/dev/null)
|
||||||
echo "Network created"
|
|
||||||
|
|
||||||
# Prepare configuration directory
|
if [ -n "$ipv4" ]; then
|
||||||
mkdir -p /etc/dokploy
|
echo "$ipv4"
|
||||||
chmod -R 777 /etc/dokploy
|
else
|
||||||
|
# Try to get IPv6
|
||||||
|
local ipv6=$(curl -6s https://ifconfig.io 2>/dev/null)
|
||||||
|
if [ -n "$ipv6" ]; then
|
||||||
|
echo "$ipv6"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
advertise_addr="${ADVERTISE_ADDR:-$(get_ip)}"
|
||||||
|
|
||||||
|
docker swarm init --advertise-addr $advertise_addr
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
echo "Error: Failed to initialize Docker Swarm" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker network rm -f dokploy-network 2>/dev/null
|
||||||
|
docker network create --driver overlay --attachable dokploy-network
|
||||||
|
|
||||||
|
echo "Network created"
|
||||||
|
|
||||||
|
mkdir -p /etc/dokploy
|
||||||
|
|
||||||
|
chmod 777 /etc/dokploy
|
||||||
|
|
||||||
# Pull and deploy Dokploy
|
# Pull and deploy Dokploy
|
||||||
docker pull dokploy/dokploy:latest
|
docker pull dokploy/dokploy:latest
|
||||||
@@ -84,6 +105,7 @@ docker service create \
|
|||||||
-e PORT=<Value For PORT eg(3000)> \
|
-e PORT=<Value For PORT eg(3000)> \
|
||||||
-e TRAEFIK_SSL_PORT=<Value For SSL PORT eg(444)> \
|
-e TRAEFIK_SSL_PORT=<Value For SSL PORT eg(444)> \
|
||||||
-e TRAEFIK_PORT=<VALUE FOR TRAEFIK HTTP PORT eg(81)> \
|
-e TRAEFIK_PORT=<VALUE FOR TRAEFIK HTTP PORT eg(81)> \
|
||||||
|
-e ADVERTISE_ADDR=$advertise_addr \
|
||||||
dokploy/dokploy:latest
|
dokploy/dokploy:latest
|
||||||
|
|
||||||
# Output success message
|
# Output success message
|
||||||
|
|||||||
@@ -1,12 +1,23 @@
|
|||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { paths } from "@dokploy/server/dist/constants";
|
import { paths } from "@dokploy/server/constants";
|
||||||
const { APPLICATIONS_PATH } = paths();
|
const { APPLICATIONS_PATH } = paths();
|
||||||
import type { ApplicationNested } from "@dokploy/server";
|
import type { ApplicationNested } from "@dokploy/server";
|
||||||
import { unzipDrop } from "@dokploy/server";
|
import { unzipDrop } from "@dokploy/server";
|
||||||
import AdmZip from "adm-zip";
|
import AdmZip from "adm-zip";
|
||||||
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
||||||
|
|
||||||
|
vi.mock("@dokploy/server/constants", async (importOriginal) => {
|
||||||
|
const actual = await importOriginal();
|
||||||
|
return {
|
||||||
|
// @ts-ignore
|
||||||
|
...actual,
|
||||||
|
paths: () => ({
|
||||||
|
APPLICATIONS_PATH: "./__test__/drop/zips/output",
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
if (typeof window === "undefined") {
|
if (typeof window === "undefined") {
|
||||||
const undici = require("undici");
|
const undici = require("undici");
|
||||||
globalThis.File = undici.File as any;
|
globalThis.File = undici.File as any;
|
||||||
@@ -82,16 +93,6 @@ const baseApp: ApplicationNested = {
|
|||||||
dockerContextPath: null,
|
dockerContextPath: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
vi.mock("@dokploy/server/dist/constants", async (importOriginal) => {
|
|
||||||
const actual = await importOriginal();
|
|
||||||
return {
|
|
||||||
// @ts-ignore
|
|
||||||
...actual,
|
|
||||||
paths: () => ({
|
|
||||||
APPLICATIONS_PATH: "./__test__/drop/zips/output",
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
describe("unzipDrop using real zip files", () => {
|
describe("unzipDrop using real zip files", () => {
|
||||||
// const { APPLICATIONS_PATH } = paths();
|
// const { APPLICATIONS_PATH } = paths();
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
|
|||||||
@@ -24,6 +24,9 @@ const baseAdmin: Admin = {
|
|||||||
sshPrivateKey: null,
|
sshPrivateKey: null,
|
||||||
enableDockerCleanup: false,
|
enableDockerCleanup: false,
|
||||||
enableLogRotation: false,
|
enableLogRotation: false,
|
||||||
|
serversQuantity: 0,
|
||||||
|
stripeCustomerId: "",
|
||||||
|
stripeSubscriptionId: "",
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
|||||||
@@ -1,13 +1,8 @@
|
|||||||
|
import path from "node:path";
|
||||||
import tsconfigPaths from "vite-tsconfig-paths";
|
import tsconfigPaths from "vite-tsconfig-paths";
|
||||||
import { defineConfig } from "vitest/config";
|
import { defineConfig } from "vitest/config";
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
plugins: [
|
|
||||||
tsconfigPaths({
|
|
||||||
root: "./",
|
|
||||||
projects: ["tsconfig.json"],
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
test: {
|
test: {
|
||||||
include: ["__test__/**/*.test.ts"], // Incluir solo los archivos de test en el directorio __test__
|
include: ["__test__/**/*.test.ts"], // Incluir solo los archivos de test en el directorio __test__
|
||||||
exclude: ["**/node_modules/**", "**/dist/**", "**/.docker/**"],
|
exclude: ["**/node_modules/**", "**/dist/**", "**/.docker/**"],
|
||||||
@@ -18,4 +13,12 @@ export default defineConfig({
|
|||||||
NODE: "test",
|
NODE: "test",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
resolve: {
|
||||||
|
alias: {
|
||||||
|
"@dokploy/server": path.resolve(
|
||||||
|
__dirname,
|
||||||
|
"../../../packages/server/src",
|
||||||
|
),
|
||||||
|
},
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -0,0 +1,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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
import { Button } from "@/components/ui/button";
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
DialogTrigger,
|
|
||||||
} from "@/components/ui/dialog";
|
|
||||||
import {
|
|
||||||
Form,
|
|
||||||
FormControl,
|
|
||||||
FormDescription,
|
|
||||||
FormField,
|
|
||||||
FormItem,
|
|
||||||
FormLabel,
|
|
||||||
FormMessage,
|
|
||||||
} from "@/components/ui/form";
|
|
||||||
import { Input } from "@/components/ui/input";
|
|
||||||
import { api } from "@/utils/api";
|
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
|
||||||
import { AlertTriangle, Container } from "lucide-react";
|
|
||||||
import { useRouter } from "next/router";
|
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { useForm } from "react-hook-form";
|
|
||||||
import { toast } from "sonner";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
const AddRegistrySchema = z.object({
|
|
||||||
username: z
|
|
||||||
.string()
|
|
||||||
.min(1, {
|
|
||||||
message: "Username is required",
|
|
||||||
})
|
|
||||||
.regex(/^[a-zA-Z0-9]+$/, {
|
|
||||||
message: "Username can only contain letters and numbers",
|
|
||||||
}),
|
|
||||||
password: z.string().min(1, {
|
|
||||||
message: "Password is required",
|
|
||||||
}),
|
|
||||||
registryUrl: z.string().min(1, {
|
|
||||||
message: "Registry URL is required",
|
|
||||||
}),
|
|
||||||
});
|
|
||||||
|
|
||||||
type AddRegistry = z.infer<typeof AddRegistrySchema>;
|
|
||||||
|
|
||||||
export const AddSelfHostedRegistry = () => {
|
|
||||||
const utils = api.useUtils();
|
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
|
||||||
const { mutateAsync, error, isError, isLoading } =
|
|
||||||
api.registry.enableSelfHostedRegistry.useMutation();
|
|
||||||
const router = useRouter();
|
|
||||||
const form = useForm<AddRegistry>({
|
|
||||||
defaultValues: {
|
|
||||||
username: "",
|
|
||||||
password: "",
|
|
||||||
registryUrl: "",
|
|
||||||
},
|
|
||||||
resolver: zodResolver(AddRegistrySchema),
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
form.reset({
|
|
||||||
registryUrl: "",
|
|
||||||
username: "",
|
|
||||||
password: "",
|
|
||||||
});
|
|
||||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
|
||||||
|
|
||||||
const onSubmit = async (data: AddRegistry) => {
|
|
||||||
await mutateAsync({
|
|
||||||
registryUrl: data.registryUrl,
|
|
||||||
username: data.username,
|
|
||||||
password: data.password,
|
|
||||||
})
|
|
||||||
.then(async (data) => {
|
|
||||||
await utils.registry.all.invalidate();
|
|
||||||
toast.success("Self Hosted Registry Created");
|
|
||||||
setIsOpen(false);
|
|
||||||
})
|
|
||||||
.catch(() => {
|
|
||||||
toast.error("Error to create a self hosted registry");
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={isOpen} onOpenChange={setIsOpen}>
|
|
||||||
<DialogTrigger asChild>
|
|
||||||
<Button className="max-sm:w-full">
|
|
||||||
<Container className="h-4 w-4" />
|
|
||||||
Enable Self Hosted Registry
|
|
||||||
</Button>
|
|
||||||
</DialogTrigger>
|
|
||||||
<DialogContent className="sm:m:max-w-lg ">
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Add a self hosted registry</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Fill the next fields to add a self hosted registry.
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
{isError && (
|
|
||||||
<div className="flex flex-row gap-4 rounded-lg bg-red-50 p-2 dark:bg-red-950">
|
|
||||||
<AlertTriangle className="text-red-600 dark:text-red-400" />
|
|
||||||
<span className="text-sm text-red-600 dark:text-red-400">
|
|
||||||
{error?.message}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<Form {...form}>
|
|
||||||
<form
|
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
|
||||||
className="grid w-full gap-4"
|
|
||||||
>
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="username"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Username</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="Username" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="password"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Password</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input
|
|
||||||
placeholder="Password"
|
|
||||||
{...field}
|
|
||||||
type="password"
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex flex-col gap-4">
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="registryUrl"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>Registry URL</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<Input placeholder="registry.dokploy.com" {...field} />
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
Point a DNS record to the VPS IP address.
|
|
||||||
</FormDescription>
|
|
||||||
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button isLoading={isLoading} type="submit">
|
|
||||||
Create
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</form>
|
|
||||||
</Form>
|
|
||||||
</DialogContent>
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { Server } from "lucide-react";
|
import { Server } from "lucide-react";
|
||||||
import { AddRegistry } from "./add-docker-registry";
|
import { AddRegistry } from "./add-docker-registry";
|
||||||
import { AddSelfHostedRegistry } from "./add-self-docker-registry";
|
|
||||||
import { DeleteRegistry } from "./delete-registry";
|
import { DeleteRegistry } from "./delete-registry";
|
||||||
import { UpdateDockerRegistry } from "./update-docker-registry";
|
import { UpdateDockerRegistry } from "./update-docker-registry";
|
||||||
|
|
||||||
@@ -31,8 +30,6 @@ export const ShowRegistry = () => {
|
|||||||
<div className="flex flex-row gap-2">
|
<div className="flex flex-row gap-2">
|
||||||
{data && data?.length > 0 && (
|
{data && data?.length > 0 && (
|
||||||
<>
|
<>
|
||||||
{!haveSelfHostedRegistry && <AddSelfHostedRegistry />}
|
|
||||||
|
|
||||||
<AddRegistry />
|
<AddRegistry />
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -47,7 +44,6 @@ export const ShowRegistry = () => {
|
|||||||
</span>
|
</span>
|
||||||
|
|
||||||
<div className="flex flex-row md:flex-row gap-2 flex-wrap w-full justify-center">
|
<div className="flex flex-row md:flex-row gap-2 flex-wrap w-full justify-center">
|
||||||
<AddSelfHostedRegistry />
|
|
||||||
<AddRegistry />
|
<AddRegistry />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -18,6 +18,16 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
@@ -32,12 +42,15 @@ const addDestination = z.object({
|
|||||||
bucket: z.string(),
|
bucket: z.string(),
|
||||||
region: z.string(),
|
region: z.string(),
|
||||||
endpoint: z.string(),
|
endpoint: z.string(),
|
||||||
|
serverId: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type AddDestination = z.infer<typeof addDestination>;
|
type AddDestination = z.infer<typeof addDestination>;
|
||||||
|
|
||||||
export const AddDestination = () => {
|
export const AddDestination = () => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||||
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
|
|
||||||
const { mutateAsync, isError, error, isLoading } =
|
const { mutateAsync, isError, error, isLoading } =
|
||||||
api.destination.create.useMutation();
|
api.destination.create.useMutation();
|
||||||
@@ -189,30 +202,106 @@ export const AddDestination = () => {
|
|||||||
/>
|
/>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<DialogFooter className="flex w-full flex-row !justify-between pt-3">
|
<DialogFooter
|
||||||
<Button
|
className={cn(
|
||||||
isLoading={isLoadingConnection}
|
isCloud ? "!flex-col" : "flex-row",
|
||||||
type="button"
|
"flex w-full !justify-between pt-3 gap-4",
|
||||||
variant="secondary"
|
)}
|
||||||
onClick={async () => {
|
>
|
||||||
await testConnection({
|
{isCloud ? (
|
||||||
accessKey: form.getValues("accessKeyId"),
|
<div className="flex flex-col gap-4 border p-2 rounded-lg">
|
||||||
bucket: form.getValues("bucket"),
|
<span className="text-sm text-muted-foreground">
|
||||||
endpoint: form.getValues("endpoint"),
|
Select a server to test the destination. If you don't have a
|
||||||
name: "Test",
|
server choose the default one.
|
||||||
region: form.getValues("region"),
|
</span>
|
||||||
secretAccessKey: form.getValues("secretAccessKey"),
|
<FormField
|
||||||
})
|
control={form.control}
|
||||||
.then(async () => {
|
name="serverId"
|
||||||
toast.success("Connection Success");
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Server (Optional)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full">
|
||||||
|
<SelectValue placeholder="Select a server" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectLabel>Servers</SelectLabel>
|
||||||
|
{servers?.map((server) => (
|
||||||
|
<SelectItem
|
||||||
|
key={server.serverId}
|
||||||
|
value={server.serverId}
|
||||||
|
>
|
||||||
|
{server.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
<SelectItem value={"none"}>None</SelectItem>
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant={"secondary"}
|
||||||
|
isLoading={isLoading}
|
||||||
|
onClick={async () => {
|
||||||
|
await testConnection({
|
||||||
|
accessKey: form.getValues("accessKeyId"),
|
||||||
|
bucket: form.getValues("bucket"),
|
||||||
|
endpoint: form.getValues("endpoint"),
|
||||||
|
name: "Test",
|
||||||
|
region: form.getValues("region"),
|
||||||
|
secretAccessKey: form.getValues("secretAccessKey"),
|
||||||
|
serverId: form.getValues("serverId"),
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
toast.success("Connection Success");
|
||||||
|
})
|
||||||
|
.catch((e) => {
|
||||||
|
toast.error("Error to connect the provider", {
|
||||||
|
description: e.message,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Test Connection
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
isLoading={isLoadingConnection}
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={async () => {
|
||||||
|
await testConnection({
|
||||||
|
accessKey: form.getValues("accessKeyId"),
|
||||||
|
bucket: form.getValues("bucket"),
|
||||||
|
endpoint: form.getValues("endpoint"),
|
||||||
|
name: "Test",
|
||||||
|
region: form.getValues("region"),
|
||||||
|
secretAccessKey: form.getValues("secretAccessKey"),
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.then(async () => {
|
||||||
toast.error("Error to connect the provider");
|
toast.success("Connection Success");
|
||||||
});
|
})
|
||||||
}}
|
.catch(() => {
|
||||||
>
|
toast.error("Error to connect the provider");
|
||||||
Test connection
|
});
|
||||||
</Button>
|
}}
|
||||||
|
>
|
||||||
|
Test connection
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
form="hook-form-destination-add"
|
form="hook-form-destination-add"
|
||||||
|
|||||||
@@ -18,6 +18,16 @@ import {
|
|||||||
FormMessage,
|
FormMessage,
|
||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
|
import {
|
||||||
|
Select,
|
||||||
|
SelectContent,
|
||||||
|
SelectGroup,
|
||||||
|
SelectItem,
|
||||||
|
SelectLabel,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from "@/components/ui/select";
|
||||||
|
import { cn } from "@/lib/utils";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { PenBoxIcon } from "lucide-react";
|
import { PenBoxIcon } from "lucide-react";
|
||||||
@@ -33,6 +43,7 @@ const updateDestination = z.object({
|
|||||||
bucket: z.string(),
|
bucket: z.string(),
|
||||||
region: z.string(),
|
region: z.string(),
|
||||||
endpoint: z.string(),
|
endpoint: z.string(),
|
||||||
|
serverId: z.string().optional(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type UpdateDestination = z.infer<typeof updateDestination>;
|
type UpdateDestination = z.infer<typeof updateDestination>;
|
||||||
@@ -43,6 +54,8 @@ interface Props {
|
|||||||
|
|
||||||
export const UpdateDestination = ({ destinationId }: Props) => {
|
export const UpdateDestination = ({ destinationId }: Props) => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
|
const { data: servers } = api.server.withSSHKey.useQuery();
|
||||||
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
const { data, refetch } = api.destination.one.useQuery(
|
const { data, refetch } = api.destination.one.useQuery(
|
||||||
{
|
{
|
||||||
@@ -220,34 +233,107 @@ export const UpdateDestination = ({ destinationId }: Props) => {
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|
||||||
<DialogFooter className="flex w-full flex-row !justify-between pt-3">
|
<DialogFooter
|
||||||
<Button
|
className={cn(
|
||||||
isLoading={isLoadingConnection}
|
isCloud ? "!flex-col" : "flex-row",
|
||||||
type="button"
|
"flex w-full !justify-between pt-3 gap-4",
|
||||||
variant="secondary"
|
)}
|
||||||
onClick={async () => {
|
>
|
||||||
await testConnection({
|
{isCloud ? (
|
||||||
accessKey: form.getValues("accessKeyId"),
|
<div className="flex flex-col gap-4 border p-2 rounded-lg">
|
||||||
bucket: form.getValues("bucket"),
|
<span className="text-sm text-muted-foreground">
|
||||||
endpoint: form.getValues("endpoint"),
|
Select a server to test the destination. If you don't have a
|
||||||
name: "Test",
|
server choose the default one.
|
||||||
region: form.getValues("region"),
|
</span>
|
||||||
secretAccessKey: form.getValues("secretAccessKey"),
|
<FormField
|
||||||
})
|
control={form.control}
|
||||||
.then(async () => {
|
name="serverId"
|
||||||
toast.success("Connection Success");
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Server (Optional)</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Select
|
||||||
|
onValueChange={field.onChange}
|
||||||
|
defaultValue={field.value}
|
||||||
|
>
|
||||||
|
<SelectTrigger className="w-full">
|
||||||
|
<SelectValue placeholder="Select a server" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectGroup>
|
||||||
|
<SelectLabel>Servers</SelectLabel>
|
||||||
|
{servers?.map((server) => (
|
||||||
|
<SelectItem
|
||||||
|
key={server.serverId}
|
||||||
|
value={server.serverId}
|
||||||
|
>
|
||||||
|
{server.name}
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
<SelectItem value={"none"}>None</SelectItem>
|
||||||
|
</SelectGroup>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
variant={"secondary"}
|
||||||
|
onClick={async () => {
|
||||||
|
await testConnection({
|
||||||
|
accessKey: form.getValues("accessKeyId"),
|
||||||
|
bucket: form.getValues("bucket"),
|
||||||
|
endpoint: form.getValues("endpoint"),
|
||||||
|
name: "Test",
|
||||||
|
region: form.getValues("region"),
|
||||||
|
secretAccessKey: form.getValues("secretAccessKey"),
|
||||||
|
serverId: form.getValues("serverId"),
|
||||||
|
})
|
||||||
|
.then(async () => {
|
||||||
|
toast.success("Connection Success");
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to connect the provider");
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Test Connection
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<Button
|
||||||
|
isLoading={isLoadingConnection}
|
||||||
|
type="button"
|
||||||
|
variant="secondary"
|
||||||
|
onClick={async () => {
|
||||||
|
await testConnection({
|
||||||
|
accessKey: form.getValues("accessKeyId"),
|
||||||
|
bucket: form.getValues("bucket"),
|
||||||
|
endpoint: form.getValues("endpoint"),
|
||||||
|
name: "Test",
|
||||||
|
region: form.getValues("region"),
|
||||||
|
secretAccessKey: form.getValues("secretAccessKey"),
|
||||||
})
|
})
|
||||||
.catch(() => {
|
.then(async () => {
|
||||||
toast.error("Error to connect the provider");
|
toast.success("Connection Success");
|
||||||
});
|
})
|
||||||
}}
|
.catch(() => {
|
||||||
>
|
toast.error("Error to connect the provider");
|
||||||
Test connection
|
});
|
||||||
</Button>
|
}}
|
||||||
|
>
|
||||||
|
Test connection
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
isLoading={form.formState.isSubmitting}
|
|
||||||
form="hook-form"
|
form="hook-form"
|
||||||
type="submit"
|
type="submit"
|
||||||
|
isLoading={form.formState.isSubmitting}
|
||||||
>
|
>
|
||||||
Update
|
Update
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ export type NotificationSchema = z.infer<typeof notificationSchema>;
|
|||||||
export const AddNotification = () => {
|
export const AddNotification = () => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const [visible, setVisible] = useState(false);
|
const [visible, setVisible] = useState(false);
|
||||||
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const { mutateAsync: testSlackConnection, isLoading: isLoadingSlack } =
|
const { mutateAsync: testSlackConnection, isLoading: isLoadingSlack } =
|
||||||
api.notification.testSlackConnection.useMutation();
|
api.notification.testSlackConnection.useMutation();
|
||||||
|
|
||||||
@@ -660,26 +660,28 @@ export const AddNotification = () => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
{!isCloud && (
|
||||||
control={form.control}
|
<FormField
|
||||||
name="dokployRestart"
|
control={form.control}
|
||||||
render={({ field }) => (
|
name="dokployRestart"
|
||||||
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
render={({ field }) => (
|
||||||
<div className="space-y-0.5">
|
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
||||||
<FormLabel>Dokploy Restart</FormLabel>
|
<div className="space-y-0.5">
|
||||||
<FormDescription>
|
<FormLabel>Dokploy Restart</FormLabel>
|
||||||
Trigger the action when a dokploy is restarted.
|
<FormDescription>
|
||||||
</FormDescription>
|
Trigger the action when a dokploy is restarted.
|
||||||
</div>
|
</FormDescription>
|
||||||
<FormControl>
|
</div>
|
||||||
<Switch
|
<FormControl>
|
||||||
checked={field.value}
|
<Switch
|
||||||
onCheckedChange={field.onChange}
|
checked={field.value}
|
||||||
/>
|
onCheckedChange={field.onChange}
|
||||||
</FormControl>
|
/>
|
||||||
</FormItem>
|
</FormControl>
|
||||||
)}
|
</FormItem>
|
||||||
/>
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export const UpdateNotification = ({ notificationId }: Props) => {
|
|||||||
const telegramMutation = api.notification.updateTelegram.useMutation();
|
const telegramMutation = api.notification.updateTelegram.useMutation();
|
||||||
const discordMutation = api.notification.updateDiscord.useMutation();
|
const discordMutation = api.notification.updateDiscord.useMutation();
|
||||||
const emailMutation = api.notification.updateEmail.useMutation();
|
const emailMutation = api.notification.updateEmail.useMutation();
|
||||||
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
const form = useForm<NotificationSchema>({
|
const form = useForm<NotificationSchema>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
type: "slack",
|
type: "slack",
|
||||||
@@ -618,27 +618,29 @@ export const UpdateNotification = ({ notificationId }: Props) => {
|
|||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<FormField
|
{!isCloud && (
|
||||||
control={form.control}
|
<FormField
|
||||||
defaultValue={form.control._defaultValues.dokployRestart}
|
control={form.control}
|
||||||
name="dokployRestart"
|
defaultValue={form.control._defaultValues.dokployRestart}
|
||||||
render={({ field }) => (
|
name="dokployRestart"
|
||||||
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
render={({ field }) => (
|
||||||
<div className="space-y-0.5">
|
<FormItem className=" flex flex-row items-center justify-between rounded-lg border p-3 shadow-sm gap-2">
|
||||||
<FormLabel>Dokploy Restart</FormLabel>
|
<div className="space-y-0.5">
|
||||||
<FormDescription>
|
<FormLabel>Dokploy Restart</FormLabel>
|
||||||
Trigger the action when a dokploy is restarted.
|
<FormDescription>
|
||||||
</FormDescription>
|
Trigger the action when a dokploy is restarted.
|
||||||
</div>
|
</FormDescription>
|
||||||
<FormControl>
|
</div>
|
||||||
<Switch
|
<FormControl>
|
||||||
checked={field.value}
|
<Switch
|
||||||
onCheckedChange={field.onChange}
|
checked={field.value}
|
||||||
/>
|
onCheckedChange={field.onChange}
|
||||||
</FormControl>
|
/>
|
||||||
</FormItem>
|
</FormControl>
|
||||||
)}
|
</FormItem>
|
||||||
/>
|
)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -52,8 +52,6 @@ export const ProfileForm = () => {
|
|||||||
const { data, refetch } = api.auth.get.useQuery();
|
const { data, refetch } = api.auth.get.useQuery();
|
||||||
const { mutateAsync, isLoading } = api.auth.update.useMutation();
|
const { mutateAsync, isLoading } = api.auth.update.useMutation();
|
||||||
|
|
||||||
const { mutateAsync: generateToken, isLoading: isLoadingToken } =
|
|
||||||
api.auth.generateToken.useMutation();
|
|
||||||
const form = useForm<Profile>({
|
const form = useForm<Profile>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
email: data?.email || "",
|
email: data?.email || "",
|
||||||
@@ -76,7 +74,7 @@ export const ProfileForm = () => {
|
|||||||
|
|
||||||
const onSubmit = async (values: Profile) => {
|
const onSubmit = async (values: Profile) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
email: values.email,
|
email: values.email.toLowerCase(),
|
||||||
password: values.password,
|
password: values.password,
|
||||||
image: values.image,
|
image: values.image,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ import { Textarea } from "@/components/ui/textarea";
|
|||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { PlusIcon } from "lucide-react";
|
import { PlusIcon } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
@@ -57,6 +58,9 @@ type Schema = z.infer<typeof Schema>;
|
|||||||
export const AddServer = () => {
|
export const AddServer = () => {
|
||||||
const utils = api.useUtils();
|
const utils = api.useUtils();
|
||||||
const [isOpen, setIsOpen] = useState(false);
|
const [isOpen, setIsOpen] = useState(false);
|
||||||
|
const { data: canCreateMoreServers, refetch } =
|
||||||
|
api.stripe.canCreateMoreServers.useQuery();
|
||||||
|
|
||||||
const { data: sshKeys } = api.sshKey.all.useQuery();
|
const { data: sshKeys } = api.sshKey.all.useQuery();
|
||||||
const { mutateAsync, error, isError } = api.server.create.useMutation();
|
const { mutateAsync, error, isError } = api.server.create.useMutation();
|
||||||
const form = useForm<Schema>({
|
const form = useForm<Schema>({
|
||||||
@@ -82,6 +86,10 @@ export const AddServer = () => {
|
|||||||
});
|
});
|
||||||
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
refetch();
|
||||||
|
}, [isOpen]);
|
||||||
|
|
||||||
const onSubmit = async (data: Schema) => {
|
const onSubmit = async (data: Schema) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
name: data.name,
|
name: data.name,
|
||||||
@@ -116,6 +124,14 @@ export const AddServer = () => {
|
|||||||
Add a server to deploy your applications remotely.
|
Add a server to deploy your applications remotely.
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
|
{!canCreateMoreServers && (
|
||||||
|
<AlertBlock type="warning">
|
||||||
|
You cannot create more servers,{" "}
|
||||||
|
<Link href="/dashboard/settings/billing" className="text-primary">
|
||||||
|
Please upgrade your plan
|
||||||
|
</Link>
|
||||||
|
</AlertBlock>
|
||||||
|
)}
|
||||||
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
{isError && <AlertBlock type="error">{error?.message}</AlertBlock>}
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
@@ -254,6 +270,7 @@ export const AddServer = () => {
|
|||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<Button
|
||||||
isLoading={form.formState.isSubmitting}
|
isLoading={form.formState.isSubmitting}
|
||||||
|
disabled={!canCreateMoreServers}
|
||||||
form="hook-form-add-server"
|
form="hook-form-add-server"
|
||||||
type="submit"
|
type="submit"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -36,6 +36,9 @@ export const ShowServers = () => {
|
|||||||
const { data, refetch } = api.server.all.useQuery();
|
const { data, refetch } = api.server.all.useQuery();
|
||||||
const { mutateAsync } = api.server.remove.useMutation();
|
const { mutateAsync } = api.server.remove.useMutation();
|
||||||
const { data: sshKeys } = api.sshKey.all.useQuery();
|
const { data: sshKeys } = api.sshKey.all.useQuery();
|
||||||
|
const { data: isCloud } = api.settings.isCloud.useQuery();
|
||||||
|
const { data: canCreateMoreServers } =
|
||||||
|
api.stripe.canCreateMoreServers.useQuery();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="p-6 space-y-6">
|
<div className="p-6 space-y-6">
|
||||||
@@ -74,8 +77,22 @@ export const ShowServers = () => {
|
|||||||
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
|
<div className="flex flex-col items-center gap-3 min-h-[25vh] justify-center">
|
||||||
<ServerIcon className="size-8" />
|
<ServerIcon className="size-8" />
|
||||||
<span className="text-base text-muted-foreground">
|
<span className="text-base text-muted-foreground">
|
||||||
No Servers found. Add a server to deploy your applications
|
{!canCreateMoreServers ? (
|
||||||
remotely.
|
<div>
|
||||||
|
You cannot create more servers,{" "}
|
||||||
|
<Link
|
||||||
|
href="/dashboard/settings/billing"
|
||||||
|
className="text-primary"
|
||||||
|
>
|
||||||
|
Please upgrade your plan
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
<span>
|
||||||
|
No Servers found. Add a server to deploy your applications
|
||||||
|
remotely.
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
@@ -87,6 +104,9 @@ export const ShowServers = () => {
|
|||||||
<TableHeader>
|
<TableHeader>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableHead className="w-[100px]">Name</TableHead>
|
<TableHead className="w-[100px]">Name</TableHead>
|
||||||
|
{isCloud && (
|
||||||
|
<TableHead className="text-center">Status</TableHead>
|
||||||
|
)}
|
||||||
<TableHead className="text-center">IP Address</TableHead>
|
<TableHead className="text-center">IP Address</TableHead>
|
||||||
<TableHead className="text-center">Port</TableHead>
|
<TableHead className="text-center">Port</TableHead>
|
||||||
<TableHead className="text-center">Username</TableHead>
|
<TableHead className="text-center">Username</TableHead>
|
||||||
@@ -98,9 +118,23 @@ export const ShowServers = () => {
|
|||||||
<TableBody>
|
<TableBody>
|
||||||
{data?.map((server) => {
|
{data?.map((server) => {
|
||||||
const canDelete = server.totalSum === 0;
|
const canDelete = server.totalSum === 0;
|
||||||
|
const isActive = server.serverStatus === "active";
|
||||||
return (
|
return (
|
||||||
<TableRow key={server.serverId}>
|
<TableRow key={server.serverId}>
|
||||||
<TableCell className="w-[100px]">{server.name}</TableCell>
|
<TableCell className="w-[100px]">{server.name}</TableCell>
|
||||||
|
{isCloud && (
|
||||||
|
<TableHead className="text-center">
|
||||||
|
<Badge
|
||||||
|
variant={
|
||||||
|
server.serverStatus === "active"
|
||||||
|
? "default"
|
||||||
|
: "destructive"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{server.serverStatus}
|
||||||
|
</Badge>
|
||||||
|
</TableHead>
|
||||||
|
)}
|
||||||
<TableCell className="text-center">
|
<TableCell className="text-center">
|
||||||
<Badge>{server.ipAddress}</Badge>
|
<Badge>{server.ipAddress}</Badge>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
@@ -131,18 +165,25 @@ export const ShowServers = () => {
|
|||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent align="end">
|
<DropdownMenuContent align="end">
|
||||||
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
<DropdownMenuLabel>Actions</DropdownMenuLabel>
|
||||||
{server.sshKeyId && (
|
|
||||||
<TerminalModal serverId={server.serverId}>
|
{isActive && (
|
||||||
<span>Enter the terminal</span>
|
<>
|
||||||
</TerminalModal>
|
{server.sshKeyId && (
|
||||||
|
<TerminalModal serverId={server.serverId}>
|
||||||
|
<span>Enter the terminal</span>
|
||||||
|
</TerminalModal>
|
||||||
|
)}
|
||||||
|
<SetupServer serverId={server.serverId} />
|
||||||
|
|
||||||
|
<UpdateServer serverId={server.serverId} />
|
||||||
|
{server.sshKeyId && (
|
||||||
|
<ShowServerActions
|
||||||
|
serverId={server.serverId}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<SetupServer serverId={server.serverId} />
|
|
||||||
|
|
||||||
<UpdateServer serverId={server.serverId} />
|
|
||||||
{server.sshKeyId && (
|
|
||||||
<ShowServerActions serverId={server.serverId} />
|
|
||||||
)}
|
|
||||||
<DialogAction
|
<DialogAction
|
||||||
disabled={!canDelete}
|
disabled={!canDelete}
|
||||||
title={
|
title={
|
||||||
@@ -187,17 +228,21 @@ export const ShowServers = () => {
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DialogAction>
|
</DialogAction>
|
||||||
|
|
||||||
{server.sshKeyId && (
|
{isActive && (
|
||||||
<>
|
<>
|
||||||
<DropdownMenuSeparator />
|
{server.sshKeyId && (
|
||||||
<DropdownMenuLabel>Extra</DropdownMenuLabel>
|
<>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuLabel>Extra</DropdownMenuLabel>
|
||||||
|
|
||||||
<ShowTraefikFileSystemModal
|
<ShowTraefikFileSystemModal
|
||||||
serverId={server.serverId}
|
serverId={server.serverId}
|
||||||
/>
|
/>
|
||||||
<ShowDockerContainersModal
|
<ShowDockerContainersModal
|
||||||
serverId={server.serverId}
|
serverId={server.serverId}
|
||||||
/>
|
/>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
|
|||||||
@@ -54,7 +54,7 @@ export const AddUser = () => {
|
|||||||
|
|
||||||
const onSubmit = async (data: AddUser) => {
|
const onSubmit = async (data: AddUser) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
email: data.email,
|
email: data.email.toLowerCase(),
|
||||||
})
|
})
|
||||||
.then(async () => {
|
.then(async () => {
|
||||||
toast.success("Invitation created");
|
toast.success("Invitation created");
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -80,6 +80,7 @@ export const SettingsLayout = ({ children }: Props) => {
|
|||||||
icon: ListMusic,
|
icon: ListMusic,
|
||||||
href: "/dashboard/settings/registry",
|
href: "/dashboard/settings/registry",
|
||||||
},
|
},
|
||||||
|
|
||||||
...(!isCloud
|
...(!isCloud
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
@@ -102,6 +103,16 @@ export const SettingsLayout = ({ children }: Props) => {
|
|||||||
icon: Server,
|
icon: Server,
|
||||||
href: "/dashboard/settings/servers",
|
href: "/dashboard/settings/servers",
|
||||||
},
|
},
|
||||||
|
...(isCloud
|
||||||
|
? [
|
||||||
|
{
|
||||||
|
title: "Billing",
|
||||||
|
label: "",
|
||||||
|
icon: CreditCardIcon,
|
||||||
|
href: "/dashboard/settings/billing",
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
...(user?.canAccessToSSHKeys
|
...(user?.canAccessToSSHKeys
|
||||||
@@ -137,6 +148,7 @@ import {
|
|||||||
Activity,
|
Activity,
|
||||||
Bell,
|
Bell,
|
||||||
BoxesIcon,
|
BoxesIcon,
|
||||||
|
CreditCardIcon,
|
||||||
Database,
|
Database,
|
||||||
GitBranch,
|
GitBranch,
|
||||||
KeyIcon,
|
KeyIcon,
|
||||||
|
|||||||
12
apps/dokploy/drizzle/0041_huge_bruce_banner.sql
Normal file
12
apps/dokploy/drizzle/0041_huge_bruce_banner.sql
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
DO $$ BEGIN
|
||||||
|
CREATE TYPE "public"."serverStatus" AS ENUM('active', 'inactive');
|
||||||
|
EXCEPTION
|
||||||
|
WHEN duplicate_object THEN null;
|
||||||
|
END $$;
|
||||||
|
--> statement-breakpoint
|
||||||
|
ALTER TABLE "admin" ADD COLUMN "stripeCustomerId" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "admin" ADD COLUMN "stripeSubscriptionId" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "admin" ADD COLUMN "serversQuantity" integer DEFAULT 0 NOT NULL;--> statement-breakpoint
|
||||||
|
ALTER TABLE "auth" ADD COLUMN "resetPasswordToken" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "auth" ADD COLUMN "resetPasswordExpiresAt" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "server" ADD COLUMN "serverStatus" "serverStatus" DEFAULT 'active' NOT NULL;
|
||||||
2
apps/dokploy/drizzle/0042_fancy_havok.sql
Normal file
2
apps/dokploy/drizzle/0042_fancy_havok.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE "auth" ADD COLUMN "confirmationToken" text;--> statement-breakpoint
|
||||||
|
ALTER TABLE "auth" ADD COLUMN "confirmationExpiresAt" text;
|
||||||
3956
apps/dokploy/drizzle/meta/0041_snapshot.json
Normal file
3956
apps/dokploy/drizzle/meta/0041_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
3968
apps/dokploy/drizzle/meta/0042_snapshot.json
Normal file
3968
apps/dokploy/drizzle/meta/0042_snapshot.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -288,6 +288,20 @@
|
|||||||
"when": 1728780577084,
|
"when": 1728780577084,
|
||||||
"tag": "0040_graceful_wolfsbane",
|
"tag": "0040_graceful_wolfsbane",
|
||||||
"breakpoints": true
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 41,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1729667438853,
|
||||||
|
"tag": "0041_huge_bruce_banner",
|
||||||
|
"breakpoints": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idx": 42,
|
||||||
|
"version": "6",
|
||||||
|
"when": 1729984439862,
|
||||||
|
"tag": "0042_fancy_havok",
|
||||||
|
"breakpoints": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dokploy",
|
"name": "dokploy",
|
||||||
"version": "v0.10.2",
|
"version": "v0.10.4",
|
||||||
"private": true,
|
"private": true,
|
||||||
"license": "Apache-2.0",
|
"license": "Apache-2.0",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
@@ -11,7 +11,7 @@
|
|||||||
"build-next": "next build",
|
"build-next": "next build",
|
||||||
"setup": "tsx -r dotenv/config setup.ts && sleep 5 && pnpm run migration:run",
|
"setup": "tsx -r dotenv/config setup.ts && sleep 5 && pnpm run migration:run",
|
||||||
"reset-password": "node -r dotenv/config dist/reset-password.mjs",
|
"reset-password": "node -r dotenv/config dist/reset-password.mjs",
|
||||||
"dev": "tsx -r dotenv/config ./server/server.ts --project tsconfig.server.json ",
|
"dev": "TURBOPACK=1 tsx -r dotenv/config ./server/server.ts --project tsconfig.server.json ",
|
||||||
"studio": "drizzle-kit studio --config ./server/db/drizzle.config.ts",
|
"studio": "drizzle-kit studio --config ./server/db/drizzle.config.ts",
|
||||||
"migration:generate": "drizzle-kit generate --config ./server/db/drizzle.config.ts",
|
"migration:generate": "drizzle-kit generate --config ./server/db/drizzle.config.ts",
|
||||||
"migration:run": "tsx -r dotenv/config migration.ts",
|
"migration:run": "tsx -r dotenv/config migration.ts",
|
||||||
@@ -34,12 +34,12 @@
|
|||||||
"test": "vitest --config __test__/vitest.config.ts"
|
"test": "vitest --config __test__/vitest.config.ts"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@dokploy/server": "workspace:*",
|
|
||||||
"@codemirror/lang-json": "^6.0.1",
|
"@codemirror/lang-json": "^6.0.1",
|
||||||
"@codemirror/lang-yaml": "^6.1.1",
|
"@codemirror/lang-yaml": "^6.1.1",
|
||||||
"@codemirror/language": "^6.10.1",
|
"@codemirror/language": "^6.10.1",
|
||||||
"@codemirror/legacy-modes": "6.4.0",
|
"@codemirror/legacy-modes": "6.4.0",
|
||||||
"@codemirror/view": "6.29.0",
|
"@codemirror/view": "6.29.0",
|
||||||
|
"@dokploy/server": "workspace:*",
|
||||||
"@dokploy/trpc-openapi": "0.0.4",
|
"@dokploy/trpc-openapi": "0.0.4",
|
||||||
"@hookform/resolvers": "^3.3.4",
|
"@hookform/resolvers": "^3.3.4",
|
||||||
"@octokit/webhooks": "^13.2.7",
|
"@octokit/webhooks": "^13.2.7",
|
||||||
@@ -61,6 +61,7 @@
|
|||||||
"@radix-ui/react-tabs": "^1.0.4",
|
"@radix-ui/react-tabs": "^1.0.4",
|
||||||
"@radix-ui/react-toggle": "^1.0.3",
|
"@radix-ui/react-toggle": "^1.0.3",
|
||||||
"@radix-ui/react-tooltip": "^1.0.7",
|
"@radix-ui/react-tooltip": "^1.0.7",
|
||||||
|
"@stripe/stripe-js": "4.8.0",
|
||||||
"@tanstack/react-query": "^4.36.1",
|
"@tanstack/react-query": "^4.36.1",
|
||||||
"@tanstack/react-table": "^8.16.0",
|
"@tanstack/react-table": "^8.16.0",
|
||||||
"@trpc/client": "^10.43.6",
|
"@trpc/client": "^10.43.6",
|
||||||
@@ -89,7 +90,7 @@
|
|||||||
"lucia": "^3.0.1",
|
"lucia": "^3.0.1",
|
||||||
"lucide-react": "^0.312.0",
|
"lucide-react": "^0.312.0",
|
||||||
"nanoid": "3",
|
"nanoid": "3",
|
||||||
"next": "^14.1.3",
|
"next": "^15.0.1",
|
||||||
"next-themes": "^0.2.1",
|
"next-themes": "^0.2.1",
|
||||||
"node-pty": "1.0.0",
|
"node-pty": "1.0.0",
|
||||||
"node-schedule": "2.1.1",
|
"node-schedule": "2.1.1",
|
||||||
@@ -102,6 +103,8 @@
|
|||||||
"recharts": "^2.12.7",
|
"recharts": "^2.12.7",
|
||||||
"slugify": "^1.6.6",
|
"slugify": "^1.6.6",
|
||||||
"sonner": "^1.4.0",
|
"sonner": "^1.4.0",
|
||||||
|
"ssh2": "1.15.0",
|
||||||
|
"stripe": "17.2.0",
|
||||||
"superjson": "^2.2.1",
|
"superjson": "^2.2.1",
|
||||||
"swagger-ui-react": "^5.17.14",
|
"swagger-ui-react": "^5.17.14",
|
||||||
"tailwind-merge": "^2.2.0",
|
"tailwind-merge": "^2.2.0",
|
||||||
@@ -111,11 +114,9 @@
|
|||||||
"ws": "8.16.0",
|
"ws": "8.16.0",
|
||||||
"xterm-addon-fit": "^0.8.0",
|
"xterm-addon-fit": "^0.8.0",
|
||||||
"zod": "^3.23.4",
|
"zod": "^3.23.4",
|
||||||
"zod-form-data": "^2.0.2",
|
"zod-form-data": "^2.0.2"
|
||||||
"ssh2": "1.15.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"autoprefixer": "10.4.12",
|
|
||||||
"@types/adm-zip": "^0.5.5",
|
"@types/adm-zip": "^0.5.5",
|
||||||
"@types/bcrypt": "5.0.2",
|
"@types/bcrypt": "5.0.2",
|
||||||
"@types/js-yaml": "4.0.9",
|
"@types/js-yaml": "4.0.9",
|
||||||
@@ -124,8 +125,10 @@
|
|||||||
"@types/node-schedule": "2.1.6",
|
"@types/node-schedule": "2.1.6",
|
||||||
"@types/react": "^18.2.37",
|
"@types/react": "^18.2.37",
|
||||||
"@types/react-dom": "^18.2.15",
|
"@types/react-dom": "^18.2.15",
|
||||||
|
"@types/ssh2": "1.15.1",
|
||||||
"@types/swagger-ui-react": "^4.18.3",
|
"@types/swagger-ui-react": "^4.18.3",
|
||||||
"@types/ws": "8.5.10",
|
"@types/ws": "8.5.10",
|
||||||
|
"autoprefixer": "10.4.12",
|
||||||
"drizzle-kit": "^0.21.1",
|
"drizzle-kit": "^0.21.1",
|
||||||
"esbuild": "0.20.2",
|
"esbuild": "0.20.2",
|
||||||
"lint-staged": "^15.2.7",
|
"lint-staged": "^15.2.7",
|
||||||
@@ -134,8 +137,7 @@
|
|||||||
"tsx": "^4.7.0",
|
"tsx": "^4.7.0",
|
||||||
"typescript": "^5.4.2",
|
"typescript": "^5.4.2",
|
||||||
"vite-tsconfig-paths": "4.3.2",
|
"vite-tsconfig-paths": "4.3.2",
|
||||||
"vitest": "^1.6.0",
|
"vitest": "^1.6.0"
|
||||||
"@types/ssh2": "1.15.1"
|
|
||||||
},
|
},
|
||||||
"ct3aMetadata": {
|
"ct3aMetadata": {
|
||||||
"initVersion": "7.25.2"
|
"initVersion": "7.25.2"
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import { ThemeProvider } from "next-themes";
|
|||||||
import type { AppProps } from "next/app";
|
import type { AppProps } from "next/app";
|
||||||
import { Inter } from "next/font/google";
|
import { Inter } from "next/font/google";
|
||||||
import Head from "next/head";
|
import Head from "next/head";
|
||||||
|
import Script from "next/script";
|
||||||
import type { ReactElement, ReactNode } from "react";
|
import type { ReactElement, ReactNode } from "react";
|
||||||
|
|
||||||
const inter = Inter({ subsets: ["latin"] });
|
const inter = Inter({ subsets: ["latin"] });
|
||||||
@@ -36,6 +37,14 @@ const MyApp = ({
|
|||||||
<Head>
|
<Head>
|
||||||
<title>Dokploy</title>
|
<title>Dokploy</title>
|
||||||
</Head>
|
</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
|
<ThemeProvider
|
||||||
attribute="class"
|
attribute="class"
|
||||||
defaultTheme="system"
|
defaultTheme="system"
|
||||||
|
|||||||
@@ -3,8 +3,9 @@ import { Head, Html, Main, NextScript } from "next/document";
|
|||||||
export default function Document() {
|
export default function Document() {
|
||||||
return (
|
return (
|
||||||
<Html lang="en" className="font-sans">
|
<Html lang="en" className="font-sans">
|
||||||
<Head />
|
<Head>
|
||||||
|
<link rel="icon" href="/icon.svg" />
|
||||||
|
</Head>
|
||||||
<body className="flex h-full flex-col font-sans">
|
<body className="flex h-full flex-col font-sans">
|
||||||
<Main />
|
<Main />
|
||||||
<NextScript />
|
<NextScript />
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { db } from "@/server/db";
|
import { db } from "@/server/db";
|
||||||
import { applications } from "@/server/db/schema";
|
import { applications } from "@/server/db/schema";
|
||||||
import type { DeploymentJob } from "@/server/queues/deployments-queue";
|
import type { DeploymentJob } from "@/server/queues/queue-types";
|
||||||
import { myQueue } from "@/server/queues/queueSetup";
|
import { myQueue } from "@/server/queues/queueSetup";
|
||||||
import { deploy } from "@/server/utils/deploy";
|
import { deploy } from "@/server/utils/deploy";
|
||||||
import { IS_CLOUD } from "@dokploy/server";
|
import { IS_CLOUD } from "@dokploy/server";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { db } from "@/server/db";
|
import { db } from "@/server/db";
|
||||||
import { compose } from "@/server/db/schema";
|
import { compose } from "@/server/db/schema";
|
||||||
import type { DeploymentJob } from "@/server/queues/deployments-queue";
|
import type { DeploymentJob } from "@/server/queues/queue-types";
|
||||||
import { myQueue } from "@/server/queues/queueSetup";
|
import { myQueue } from "@/server/queues/queueSetup";
|
||||||
import { deploy } from "@/server/utils/deploy";
|
import { deploy } from "@/server/utils/deploy";
|
||||||
import { IS_CLOUD } from "@dokploy/server";
|
import { IS_CLOUD } from "@dokploy/server";
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { db } from "@/server/db";
|
import { db } from "@/server/db";
|
||||||
import { applications, compose, github } from "@/server/db/schema";
|
import { applications, compose, github } from "@/server/db/schema";
|
||||||
import type { DeploymentJob } from "@/server/queues/deployments-queue";
|
import type { DeploymentJob } from "@/server/queues/queue-types";
|
||||||
import { myQueue } from "@/server/queues/queueSetup";
|
import { myQueue } from "@/server/queues/queueSetup";
|
||||||
import { deploy } from "@/server/utils/deploy";
|
import { deploy } from "@/server/utils/deploy";
|
||||||
import { IS_CLOUD } from "@dokploy/server";
|
import { IS_CLOUD } from "@dokploy/server";
|
||||||
|
|||||||
274
apps/dokploy/pages/api/stripe/webhook.ts
Normal file
274
apps/dokploy/pages/api/stripe/webhook.ts
Normal file
@@ -0,0 +1,274 @@
|
|||||||
|
import { buffer } from "node:stream/consumers";
|
||||||
|
import { db } from "@/server/db";
|
||||||
|
import { admins, server } from "@/server/db/schema";
|
||||||
|
import { findAdminById } from "@dokploy/server";
|
||||||
|
import { asc, eq } from "drizzle-orm";
|
||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import Stripe from "stripe";
|
||||||
|
|
||||||
|
const endpointSecret = process.env.STRIPE_WEBHOOK_SECRET || "";
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
api: {
|
||||||
|
bodyParser: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
export default async function handler(
|
||||||
|
req: NextApiRequest,
|
||||||
|
res: NextApiResponse,
|
||||||
|
) {
|
||||||
|
if (!endpointSecret) {
|
||||||
|
return res.status(400).send("Webhook Error: Missing Stripe Secret Key");
|
||||||
|
}
|
||||||
|
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || "", {
|
||||||
|
apiVersion: "2024-09-30.acacia",
|
||||||
|
maxNetworkRetries: 3,
|
||||||
|
});
|
||||||
|
|
||||||
|
const buf = await buffer(req);
|
||||||
|
const sig = req.headers["stripe-signature"] as string;
|
||||||
|
|
||||||
|
let event: Stripe.Event;
|
||||||
|
|
||||||
|
try {
|
||||||
|
event = stripe.webhooks.constructEvent(buf, sig, endpointSecret);
|
||||||
|
} catch (err) {
|
||||||
|
console.error(
|
||||||
|
"Webhook signature verification failed.",
|
||||||
|
err instanceof Error ? err.message : err,
|
||||||
|
);
|
||||||
|
return res.status(400).send("Webhook Error: ");
|
||||||
|
}
|
||||||
|
|
||||||
|
const webhooksAllowed = [
|
||||||
|
"customer.subscription.created",
|
||||||
|
"customer.subscription.deleted",
|
||||||
|
"customer.subscription.updated",
|
||||||
|
"invoice.payment_succeeded",
|
||||||
|
"invoice.payment_failed",
|
||||||
|
"customer.deleted",
|
||||||
|
"checkout.session.completed",
|
||||||
|
];
|
||||||
|
|
||||||
|
if (!webhooksAllowed.includes(event.type)) {
|
||||||
|
return res.status(400).send("Webhook Error: Invalid Event Type");
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (event.type) {
|
||||||
|
case "checkout.session.completed": {
|
||||||
|
const session = event.data.object as Stripe.Checkout.Session;
|
||||||
|
const adminId = session?.metadata?.adminId as string;
|
||||||
|
|
||||||
|
const subscription = await stripe.subscriptions.retrieve(
|
||||||
|
session.subscription as string,
|
||||||
|
);
|
||||||
|
await db
|
||||||
|
.update(admins)
|
||||||
|
.set({
|
||||||
|
stripeCustomerId: session.customer as string,
|
||||||
|
stripeSubscriptionId: session.subscription as string,
|
||||||
|
serversQuantity: subscription?.items?.data?.[0]?.quantity ?? 0,
|
||||||
|
})
|
||||||
|
.where(eq(admins.adminId, adminId))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
const admin = await findAdminById(adminId);
|
||||||
|
if (!admin) {
|
||||||
|
return res.status(400).send("Webhook Error: Admin not found");
|
||||||
|
}
|
||||||
|
const newServersQuantity = admin.serversQuantity;
|
||||||
|
await updateServersBasedOnQuantity(admin.adminId, newServersQuantity);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "customer.subscription.created": {
|
||||||
|
const newSubscription = event.data.object as Stripe.Subscription;
|
||||||
|
|
||||||
|
await db
|
||||||
|
.update(admins)
|
||||||
|
.set({
|
||||||
|
stripeSubscriptionId: newSubscription.id,
|
||||||
|
serversQuantity: 0,
|
||||||
|
stripeCustomerId: newSubscription.customer as string,
|
||||||
|
})
|
||||||
|
.where(eq(admins.stripeCustomerId, newSubscription.customer as string))
|
||||||
|
.returning();
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "customer.subscription.deleted": {
|
||||||
|
const newSubscription = event.data.object as Stripe.Subscription;
|
||||||
|
|
||||||
|
await db
|
||||||
|
.update(admins)
|
||||||
|
.set({
|
||||||
|
stripeSubscriptionId: null,
|
||||||
|
serversQuantity: 0,
|
||||||
|
})
|
||||||
|
.where(eq(admins.stripeCustomerId, newSubscription.customer as string));
|
||||||
|
|
||||||
|
const admin = await findAdminByStripeCustomerId(
|
||||||
|
newSubscription.customer as string,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!admin) {
|
||||||
|
return res.status(400).send("Webhook Error: Admin not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
await disableServers(admin.adminId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "customer.subscription.updated": {
|
||||||
|
const newSubscription = event.data.object as Stripe.Subscription;
|
||||||
|
await db
|
||||||
|
.update(admins)
|
||||||
|
.set({
|
||||||
|
serversQuantity: newSubscription?.items?.data?.[0]?.quantity ?? 0,
|
||||||
|
})
|
||||||
|
.where(eq(admins.stripeCustomerId, newSubscription.customer as string));
|
||||||
|
|
||||||
|
const admin = await findAdminByStripeCustomerId(
|
||||||
|
newSubscription.customer as string,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!admin) {
|
||||||
|
return res.status(400).send("Webhook Error: Admin not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
const newServersQuantity = admin.serversQuantity;
|
||||||
|
await updateServersBasedOnQuantity(admin.adminId, newServersQuantity);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "invoice.payment_succeeded": {
|
||||||
|
const newInvoice = event.data.object as Stripe.Invoice;
|
||||||
|
|
||||||
|
const suscription = await stripe.subscriptions.retrieve(
|
||||||
|
newInvoice.subscription as string,
|
||||||
|
);
|
||||||
|
|
||||||
|
await db
|
||||||
|
.update(admins)
|
||||||
|
.set({
|
||||||
|
serversQuantity: suscription?.items?.data?.[0]?.quantity ?? 0,
|
||||||
|
})
|
||||||
|
.where(eq(admins.stripeCustomerId, suscription.customer as string));
|
||||||
|
|
||||||
|
const admin = await findAdminByStripeCustomerId(
|
||||||
|
suscription.customer as string,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!admin) {
|
||||||
|
return res.status(400).send("Webhook Error: Admin not found");
|
||||||
|
}
|
||||||
|
const newServersQuantity = admin.serversQuantity;
|
||||||
|
await updateServersBasedOnQuantity(admin.adminId, newServersQuantity);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
case "invoice.payment_failed": {
|
||||||
|
const newInvoice = event.data.object as Stripe.Invoice;
|
||||||
|
await db
|
||||||
|
.update(admins)
|
||||||
|
.set({
|
||||||
|
serversQuantity: 0,
|
||||||
|
})
|
||||||
|
.where(eq(admins.stripeCustomerId, newInvoice.customer as string));
|
||||||
|
|
||||||
|
const admin = await findAdminByStripeCustomerId(
|
||||||
|
newInvoice.customer as string,
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!admin) {
|
||||||
|
return res.status(400).send("Webhook Error: Admin not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
await disableServers(admin.adminId);
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
case "customer.deleted": {
|
||||||
|
const customer = event.data.object as Stripe.Customer;
|
||||||
|
|
||||||
|
const admin = await findAdminByStripeCustomerId(customer.id);
|
||||||
|
if (!admin) {
|
||||||
|
return res.status(400).send("Webhook Error: Admin not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
await disableServers(admin.adminId);
|
||||||
|
await db
|
||||||
|
.update(admins)
|
||||||
|
.set({
|
||||||
|
stripeCustomerId: null,
|
||||||
|
stripeSubscriptionId: null,
|
||||||
|
serversQuantity: 0,
|
||||||
|
})
|
||||||
|
.where(eq(admins.stripeCustomerId, customer.id));
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
console.log(`Unhandled event type: ${event.type}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(200).json({ received: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
const disableServers = async (adminId: string) => {
|
||||||
|
await db
|
||||||
|
.update(server)
|
||||||
|
.set({
|
||||||
|
serverStatus: "inactive",
|
||||||
|
})
|
||||||
|
.where(eq(server.adminId, adminId));
|
||||||
|
};
|
||||||
|
|
||||||
|
const findAdminByStripeCustomerId = async (stripeCustomerId: string) => {
|
||||||
|
const admin = db.query.admins.findFirst({
|
||||||
|
where: eq(admins.stripeCustomerId, stripeCustomerId),
|
||||||
|
});
|
||||||
|
return admin;
|
||||||
|
};
|
||||||
|
|
||||||
|
const activateServer = async (serverId: string) => {
|
||||||
|
await db
|
||||||
|
.update(server)
|
||||||
|
.set({ serverStatus: "active" })
|
||||||
|
.where(eq(server.serverId, serverId));
|
||||||
|
};
|
||||||
|
|
||||||
|
const deactivateServer = async (serverId: string) => {
|
||||||
|
await db
|
||||||
|
.update(server)
|
||||||
|
.set({ serverStatus: "inactive" })
|
||||||
|
.where(eq(server.serverId, serverId));
|
||||||
|
};
|
||||||
|
|
||||||
|
export const findServersByAdminIdSorted = async (adminId: string) => {
|
||||||
|
const servers = await db.query.server.findMany({
|
||||||
|
where: eq(server.adminId, adminId),
|
||||||
|
orderBy: asc(server.createdAt),
|
||||||
|
});
|
||||||
|
|
||||||
|
return servers;
|
||||||
|
};
|
||||||
|
export const updateServersBasedOnQuantity = async (
|
||||||
|
adminId: string,
|
||||||
|
newServersQuantity: number,
|
||||||
|
) => {
|
||||||
|
const servers = await findServersByAdminIdSorted(adminId);
|
||||||
|
|
||||||
|
if (servers.length > newServersQuantity) {
|
||||||
|
for (const [index, server] of servers.entries()) {
|
||||||
|
if (index < newServersQuantity) {
|
||||||
|
await activateServer(server.serverId);
|
||||||
|
} else {
|
||||||
|
await deactivateServer(server.serverId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for (const server of servers) {
|
||||||
|
await activateServer(server.serverId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
96
apps/dokploy/pages/confirm-email.tsx
Normal file
96
apps/dokploy/pages/confirm-email.tsx
Normal 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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -22,13 +22,20 @@ import {
|
|||||||
BreadcrumbItem,
|
BreadcrumbItem,
|
||||||
BreadcrumbLink,
|
BreadcrumbLink,
|
||||||
} from "@/components/ui/breadcrumb";
|
} from "@/components/ui/breadcrumb";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { validateRequest } from "@dokploy/server";
|
import { validateRequest } from "@dokploy/server";
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import { GlobeIcon } from "lucide-react";
|
import { GlobeIcon, HelpCircle, ServerOff } from "lucide-react";
|
||||||
import type {
|
import type {
|
||||||
GetServerSidePropsContext,
|
GetServerSidePropsContext,
|
||||||
InferGetServerSidePropsType,
|
InferGetServerSidePropsType,
|
||||||
@@ -100,8 +107,40 @@ const Service = (
|
|||||||
</h1>
|
</h1>
|
||||||
<span className="text-sm">{data?.appName}</span>
|
<span className="text-sm">{data?.appName}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="flex flex-row h-fit w-fit gap-2">
|
||||||
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
|
<Badge
|
||||||
|
variant={
|
||||||
|
!data?.serverId
|
||||||
|
? "default"
|
||||||
|
: data?.server?.serverStatus === "active"
|
||||||
|
? "default"
|
||||||
|
: "destructive"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{data?.server?.name || "Dokploy Server"}
|
||||||
|
</Badge>
|
||||||
|
{data?.server?.serverStatus === "inactive" && (
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Label className="break-all w-fit flex flex-row gap-1 items-center">
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground" />
|
||||||
|
</Label>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
className="z-[999] w-[300px]"
|
||||||
|
align="start"
|
||||||
|
side="top"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
You cannot, deploy this application because the server
|
||||||
|
is inactive, please upgrade your plan to add more
|
||||||
|
servers.
|
||||||
|
</span>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{data?.description && (
|
{data?.description && (
|
||||||
@@ -119,90 +158,111 @@ const Service = (
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
</div>
|
</div>
|
||||||
<Tabs
|
|
||||||
value={tab}
|
{data?.server?.serverStatus === "inactive" ? (
|
||||||
defaultValue="general"
|
<div className="flex h-[55vh] border-2 rounded-xl border-dashed p-4">
|
||||||
className="w-full"
|
<div className="max-w-3xl mx-auto flex flex-col items-center justify-center self-center gap-3">
|
||||||
onValueChange={(e) => {
|
<ServerOff className="size-10 text-muted-foreground self-center" />
|
||||||
setSab(e as TabState);
|
<span className="text-center text-base text-muted-foreground">
|
||||||
const newPath = `/dashboard/project/${projectId}/services/application/${applicationId}?tab=${e}`;
|
This service is hosted on the server {data.server.name}, but this
|
||||||
router.push(newPath, undefined, { shallow: true });
|
server has been disabled because your current plan doesn't include
|
||||||
}}
|
enough servers. Please purchase more servers to regain access to
|
||||||
>
|
this application.
|
||||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
</span>
|
||||||
<TabsList
|
<span className="text-center text-base text-muted-foreground">
|
||||||
className={cn(
|
Go to{" "}
|
||||||
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
<Link href="/dashboard/settings/billing" className="text-primary">
|
||||||
data?.serverId ? "md:grid-cols-6" : "md:grid-cols-7",
|
Billing
|
||||||
)}
|
</Link>
|
||||||
>
|
</span>
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
|
||||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
|
||||||
{!data?.serverId && (
|
|
||||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
|
||||||
)}
|
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
|
||||||
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
|
||||||
<TabsTrigger value="domains">Domains</TabsTrigger>
|
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
<div className="flex flex-row gap-2">
|
|
||||||
<UpdateApplication applicationId={applicationId} />
|
|
||||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
|
||||||
<DeleteApplication applicationId={applicationId} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<Tabs
|
||||||
|
value={tab}
|
||||||
|
defaultValue="general"
|
||||||
|
className="w-full"
|
||||||
|
onValueChange={(e) => {
|
||||||
|
setSab(e as TabState);
|
||||||
|
const newPath = `/dashboard/project/${projectId}/services/application/${applicationId}?tab=${e}`;
|
||||||
|
router.push(newPath, undefined, { shallow: true });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||||
|
<TabsList
|
||||||
|
className={cn(
|
||||||
|
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
||||||
|
data?.serverId ? "md:grid-cols-6" : "md:grid-cols-7",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
|
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||||
|
{!data?.serverId && (
|
||||||
|
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||||
|
)}
|
||||||
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
|
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
||||||
|
<TabsTrigger value="domains">Domains</TabsTrigger>
|
||||||
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<div className="flex flex-row gap-2">
|
||||||
|
<UpdateApplication applicationId={applicationId} />
|
||||||
|
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
||||||
|
<DeleteApplication applicationId={applicationId} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<TabsContent value="general">
|
<TabsContent value="general">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
|
||||||
<ShowGeneralApplication applicationId={applicationId} />
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value="environment">
|
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
|
||||||
<ShowEnvironment applicationId={applicationId} />
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
{!data?.serverId && (
|
|
||||||
<TabsContent value="monitoring">
|
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<DockerMonitoring appName={data?.appName || ""} />
|
<ShowGeneralApplication applicationId={applicationId} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
)}
|
<TabsContent value="environment">
|
||||||
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
|
<ShowEnvironment applicationId={applicationId} />
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
{!data?.serverId && (
|
||||||
|
<TabsContent value="monitoring">
|
||||||
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
|
<DockerMonitoring appName={data?.appName || ""} />
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
)}
|
||||||
|
|
||||||
<TabsContent value="logs">
|
<TabsContent value="logs">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowDockerLogs
|
<ShowDockerLogs
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
serverId={data?.serverId || ""}
|
serverId={data?.serverId || ""}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="deployments" className="w-full">
|
<TabsContent value="deployments" className="w-full">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowDeployments applicationId={applicationId} />
|
<ShowDeployments applicationId={applicationId} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="domains" className="w-full">
|
<TabsContent value="domains" className="w-full">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowDomains applicationId={applicationId} />
|
<ShowDomains applicationId={applicationId} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="advanced">
|
<TabsContent value="advanced">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<AddCommand applicationId={applicationId} />
|
<AddCommand applicationId={applicationId} />
|
||||||
<ShowClusterSettings applicationId={applicationId} />
|
<ShowClusterSettings applicationId={applicationId} />
|
||||||
<ShowApplicationResources applicationId={applicationId} />
|
<ShowApplicationResources applicationId={applicationId} />
|
||||||
<ShowVolumes applicationId={applicationId} />
|
<ShowVolumes applicationId={applicationId} />
|
||||||
<ShowRedirects applicationId={applicationId} />
|
<ShowRedirects applicationId={applicationId} />
|
||||||
<ShowSecurity applicationId={applicationId} />
|
<ShowSecurity applicationId={applicationId} />
|
||||||
<ShowPorts applicationId={applicationId} />
|
<ShowPorts applicationId={applicationId} />
|
||||||
<ShowTraefikConfig applicationId={applicationId} />
|
<ShowTraefikConfig applicationId={applicationId} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,13 +16,21 @@ import {
|
|||||||
BreadcrumbItem,
|
BreadcrumbItem,
|
||||||
BreadcrumbLink,
|
BreadcrumbLink,
|
||||||
} from "@/components/ui/breadcrumb";
|
} from "@/components/ui/breadcrumb";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { validateRequest } from "@dokploy/server";
|
import { validateRequest } from "@dokploy/server";
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import { CircuitBoard } from "lucide-react";
|
import { CircuitBoard, ServerOff } from "lucide-react";
|
||||||
|
import { HelpCircle } from "lucide-react";
|
||||||
import type {
|
import type {
|
||||||
GetServerSidePropsContext,
|
GetServerSidePropsContext,
|
||||||
InferGetServerSidePropsType,
|
InferGetServerSidePropsType,
|
||||||
@@ -94,8 +102,40 @@ const Service = (
|
|||||||
</h1>
|
</h1>
|
||||||
<span className="text-sm">{data?.appName}</span>
|
<span className="text-sm">{data?.appName}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="flex flex-row h-fit w-fit gap-2">
|
||||||
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
|
<Badge
|
||||||
|
variant={
|
||||||
|
!data?.serverId
|
||||||
|
? "default"
|
||||||
|
: data?.server?.serverStatus === "active"
|
||||||
|
? "default"
|
||||||
|
: "destructive"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{data?.server?.name || "Dokploy Server"}
|
||||||
|
</Badge>
|
||||||
|
{data?.server?.serverStatus === "inactive" && (
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Label className="break-all w-fit flex flex-row gap-1 items-center">
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground" />
|
||||||
|
</Label>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
className="z-[999] w-[300px]"
|
||||||
|
align="start"
|
||||||
|
side="top"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
You cannot, deploy this application because the server
|
||||||
|
is inactive, please upgrade your plan to add more
|
||||||
|
servers.
|
||||||
|
</span>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{data?.description && (
|
{data?.description && (
|
||||||
<p className="text-sm text-muted-foreground max-w-6xl">
|
<p className="text-sm text-muted-foreground max-w-6xl">
|
||||||
@@ -113,98 +153,118 @@ const Service = (
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
</div>
|
</div>
|
||||||
<Tabs
|
{data?.server?.serverStatus === "inactive" ? (
|
||||||
value={tab}
|
<div className="flex h-[55vh] border-2 rounded-xl border-dashed p-4">
|
||||||
defaultValue="general"
|
<div className="max-w-3xl mx-auto flex flex-col items-center justify-center self-center gap-3">
|
||||||
className="w-full"
|
<ServerOff className="size-10 text-muted-foreground self-center" />
|
||||||
onValueChange={(e) => {
|
<span className="text-center text-base text-muted-foreground">
|
||||||
setSab(e as TabState);
|
This service is hosted on the server {data.server.name}, but this
|
||||||
const newPath = `/dashboard/project/${projectId}/services/compose/${composeId}?tab=${e}`;
|
server has been disabled because your current plan doesn't include
|
||||||
router.push(newPath, undefined, { shallow: true });
|
enough servers. Please purchase more servers to regain access to
|
||||||
}}
|
this application.
|
||||||
>
|
</span>
|
||||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
<span className="text-center text-base text-muted-foreground">
|
||||||
<TabsList
|
Go to{" "}
|
||||||
className={cn(
|
<Link href="/dashboard/settings/billing" className="text-primary">
|
||||||
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
Billing
|
||||||
data?.serverId ? "md:grid-cols-6" : "md:grid-cols-7",
|
</Link>
|
||||||
data?.composeType === "docker-compose" ? "" : "md:grid-cols-6",
|
</span>
|
||||||
data?.serverId && data?.composeType === "stack"
|
|
||||||
? "md:grid-cols-5"
|
|
||||||
: "",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
|
||||||
{data?.composeType === "docker-compose" && (
|
|
||||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
|
||||||
)}
|
|
||||||
{!data?.serverId && (
|
|
||||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
|
||||||
)}
|
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
|
||||||
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
|
||||||
<TabsTrigger value="domains">Domains</TabsTrigger>
|
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
<div className="flex flex-row gap-2">
|
|
||||||
<UpdateCompose composeId={composeId} />
|
|
||||||
|
|
||||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
|
||||||
<DeleteCompose composeId={composeId} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<Tabs
|
||||||
|
value={tab}
|
||||||
|
defaultValue="general"
|
||||||
|
className="w-full"
|
||||||
|
onValueChange={(e) => {
|
||||||
|
setSab(e as TabState);
|
||||||
|
const newPath = `/dashboard/project/${projectId}/services/compose/${composeId}?tab=${e}`;
|
||||||
|
router.push(newPath, undefined, { shallow: true });
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||||
|
<TabsList
|
||||||
|
className={cn(
|
||||||
|
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
||||||
|
data?.serverId ? "md:grid-cols-6" : "md:grid-cols-7",
|
||||||
|
data?.composeType === "docker-compose" ? "" : "md:grid-cols-6",
|
||||||
|
data?.serverId && data?.composeType === "stack"
|
||||||
|
? "md:grid-cols-5"
|
||||||
|
: "",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
|
{data?.composeType === "docker-compose" && (
|
||||||
|
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||||
|
)}
|
||||||
|
{!data?.serverId && (
|
||||||
|
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||||
|
)}
|
||||||
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
|
<TabsTrigger value="deployments">Deployments</TabsTrigger>
|
||||||
|
<TabsTrigger value="domains">Domains</TabsTrigger>
|
||||||
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<div className="flex flex-row gap-2">
|
||||||
|
<UpdateCompose composeId={composeId} />
|
||||||
|
|
||||||
<TabsContent value="general">
|
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<DeleteCompose composeId={composeId} />
|
||||||
<ShowGeneralCompose composeId={composeId} />
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value="environment">
|
<TabsContent value="general">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
|
||||||
<ShowEnvironmentCompose composeId={composeId} />
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
{!data?.serverId && (
|
|
||||||
<TabsContent value="monitoring">
|
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowMonitoringCompose
|
<ShowGeneralCompose composeId={composeId} />
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="environment">
|
||||||
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
|
<ShowEnvironmentCompose composeId={composeId} />
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
{!data?.serverId && (
|
||||||
|
<TabsContent value="monitoring">
|
||||||
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
|
<ShowMonitoringCompose
|
||||||
|
serverId={data?.serverId || ""}
|
||||||
|
appName={data?.appName || ""}
|
||||||
|
appType={data?.composeType || "docker-compose"}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<TabsContent value="logs">
|
||||||
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
|
<ShowDockerLogsCompose
|
||||||
serverId={data?.serverId || ""}
|
serverId={data?.serverId || ""}
|
||||||
appName={data?.appName || ""}
|
appName={data?.appName || ""}
|
||||||
appType={data?.composeType || "docker-compose"}
|
appType={data?.composeType || "docker-compose"}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
)}
|
|
||||||
|
|
||||||
<TabsContent value="logs">
|
<TabsContent value="deployments">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowDockerLogsCompose
|
<ShowDeploymentsCompose composeId={composeId} />
|
||||||
serverId={data?.serverId || ""}
|
</div>
|
||||||
appName={data?.appName || ""}
|
</TabsContent>
|
||||||
appType={data?.composeType || "docker-compose"}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
|
|
||||||
<TabsContent value="deployments">
|
<TabsContent value="domains">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<ShowDeploymentsCompose composeId={composeId} />
|
<ShowDomainsCompose composeId={composeId} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
<TabsContent value="advanced">
|
||||||
<TabsContent value="domains">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<AddCommandCompose composeId={composeId} />
|
||||||
<ShowDomainsCompose composeId={composeId} />
|
<ShowVolumesCompose composeId={composeId} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
<TabsContent value="advanced">
|
</Tabs>
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
)}
|
||||||
<AddCommandCompose composeId={composeId} />
|
|
||||||
<ShowVolumesCompose composeId={composeId} />
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
</Tabs>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,12 +17,20 @@ import {
|
|||||||
BreadcrumbItem,
|
BreadcrumbItem,
|
||||||
BreadcrumbLink,
|
BreadcrumbLink,
|
||||||
} from "@/components/ui/breadcrumb";
|
} from "@/components/ui/breadcrumb";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { validateRequest } from "@dokploy/server";
|
import { validateRequest } from "@dokploy/server";
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
|
import { HelpCircle, ServerOff } from "lucide-react";
|
||||||
import type {
|
import type {
|
||||||
GetServerSidePropsContext,
|
GetServerSidePropsContext,
|
||||||
InferGetServerSidePropsType,
|
InferGetServerSidePropsType,
|
||||||
@@ -82,8 +90,40 @@ const Mariadb = (
|
|||||||
</h1>
|
</h1>
|
||||||
<span className="text-sm">{data?.appName}</span>
|
<span className="text-sm">{data?.appName}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="flex flex-row h-fit w-fit gap-2">
|
||||||
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
|
<Badge
|
||||||
|
variant={
|
||||||
|
!data?.serverId
|
||||||
|
? "default"
|
||||||
|
: data?.server?.serverStatus === "active"
|
||||||
|
? "default"
|
||||||
|
: "destructive"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{data?.server?.name || "Dokploy Server"}
|
||||||
|
</Badge>
|
||||||
|
{data?.server?.serverStatus === "inactive" && (
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Label className="break-all w-fit flex flex-row gap-1 items-center">
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground" />
|
||||||
|
</Label>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
className="z-[999] w-[300px]"
|
||||||
|
align="start"
|
||||||
|
side="top"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
You cannot, deploy this application because the server
|
||||||
|
is inactive, please upgrade your plan to add more
|
||||||
|
servers.
|
||||||
|
</span>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{data?.description && (
|
{data?.description && (
|
||||||
<p className="text-sm text-muted-foreground max-w-6xl">
|
<p className="text-sm text-muted-foreground max-w-6xl">
|
||||||
@@ -99,79 +139,99 @@ const Mariadb = (
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
</div>
|
</div>
|
||||||
<Tabs
|
{data?.server?.serverStatus === "inactive" ? (
|
||||||
value={tab}
|
<div className="flex h-[55vh] border-2 rounded-xl border-dashed p-4">
|
||||||
defaultValue="general"
|
<div className="max-w-3xl mx-auto flex flex-col items-center justify-center self-center gap-3">
|
||||||
className="w-full"
|
<ServerOff className="size-10 text-muted-foreground self-center" />
|
||||||
onValueChange={(e) => {
|
<span className="text-center text-base text-muted-foreground">
|
||||||
setSab(e as TabState);
|
This service is hosted on the server {data.server.name}, but this
|
||||||
const newPath = `/dashboard/project/${projectId}/services/mariadb/${mariadbId}?tab=${e}`;
|
server has been disabled because your current plan doesn't include
|
||||||
|
enough servers. Please purchase more servers to regain access to
|
||||||
router.push(newPath, undefined, { shallow: true });
|
this application.
|
||||||
}}
|
</span>
|
||||||
>
|
<span className="text-center text-base text-muted-foreground">
|
||||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
Go to{" "}
|
||||||
<TabsList
|
<Link href="/dashboard/settings/billing" className="text-primary">
|
||||||
className={cn(
|
Billing
|
||||||
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
</Link>
|
||||||
data?.serverId ? "md:grid-cols-5" : "md:grid-cols-6",
|
</span>
|
||||||
)}
|
|
||||||
>
|
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
|
||||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
|
||||||
{!data?.serverId && (
|
|
||||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
|
||||||
)}
|
|
||||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
<div className="flex flex-row gap-2">
|
|
||||||
<UpdateMariadb mariadbId={mariadbId} />
|
|
||||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
|
||||||
<DeleteMariadb mariadbId={mariadbId} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<Tabs
|
||||||
|
value={tab}
|
||||||
|
defaultValue="general"
|
||||||
|
className="w-full"
|
||||||
|
onValueChange={(e) => {
|
||||||
|
setSab(e as TabState);
|
||||||
|
const newPath = `/dashboard/project/${projectId}/services/mariadb/${mariadbId}?tab=${e}`;
|
||||||
|
|
||||||
<TabsContent value="general">
|
router.push(newPath, undefined, { shallow: true });
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
}}
|
||||||
<ShowGeneralMariadb mariadbId={mariadbId} />
|
>
|
||||||
<ShowInternalMariadbCredentials mariadbId={mariadbId} />
|
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||||
<ShowExternalMariadbCredentials mariadbId={mariadbId} />
|
<TabsList
|
||||||
|
className={cn(
|
||||||
|
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
||||||
|
data?.serverId ? "md:grid-cols-5" : "md:grid-cols-6",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
|
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||||
|
{!data?.serverId && (
|
||||||
|
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||||
|
)}
|
||||||
|
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||||
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<div className="flex flex-row gap-2">
|
||||||
|
<UpdateMariadb mariadbId={mariadbId} />
|
||||||
|
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
||||||
|
<DeleteMariadb mariadbId={mariadbId} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value="environment">
|
<TabsContent value="general">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
|
||||||
<ShowMariadbEnvironment mariadbId={mariadbId} />
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
{!data?.serverId && (
|
|
||||||
<TabsContent value="monitoring">
|
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<DockerMonitoring appName={data?.appName || ""} />
|
<ShowGeneralMariadb mariadbId={mariadbId} />
|
||||||
|
<ShowInternalMariadbCredentials mariadbId={mariadbId} />
|
||||||
|
<ShowExternalMariadbCredentials mariadbId={mariadbId} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
)}
|
<TabsContent value="environment">
|
||||||
<TabsContent value="logs">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<ShowMariadbEnvironment mariadbId={mariadbId} />
|
||||||
<ShowDockerLogs
|
</div>
|
||||||
serverId={data?.serverId || ""}
|
</TabsContent>
|
||||||
appName={data?.appName || ""}
|
{!data?.serverId && (
|
||||||
/>
|
<TabsContent value="monitoring">
|
||||||
</div>
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
</TabsContent>
|
<DockerMonitoring appName={data?.appName || ""} />
|
||||||
<TabsContent value="backups">
|
</div>
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
</TabsContent>
|
||||||
<ShowBackupMariadb mariadbId={mariadbId} />
|
)}
|
||||||
</div>
|
<TabsContent value="logs">
|
||||||
</TabsContent>
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<TabsContent value="advanced">
|
<ShowDockerLogs
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
serverId={data?.serverId || ""}
|
||||||
<ShowAdvancedMariadb mariadbId={mariadbId} />
|
appName={data?.appName || ""}
|
||||||
</div>
|
/>
|
||||||
</TabsContent>
|
</div>
|
||||||
</Tabs>
|
</TabsContent>
|
||||||
|
<TabsContent value="backups">
|
||||||
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
|
<ShowBackupMariadb mariadbId={mariadbId} />
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="advanced">
|
||||||
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
|
<ShowAdvancedMariadb mariadbId={mariadbId} />
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,12 +17,20 @@ import {
|
|||||||
BreadcrumbItem,
|
BreadcrumbItem,
|
||||||
BreadcrumbLink,
|
BreadcrumbLink,
|
||||||
} from "@/components/ui/breadcrumb";
|
} from "@/components/ui/breadcrumb";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { validateRequest } from "@dokploy/server";
|
import { validateRequest } from "@dokploy/server";
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
|
import { HelpCircle, ServerOff } from "lucide-react";
|
||||||
import type {
|
import type {
|
||||||
GetServerSidePropsContext,
|
GetServerSidePropsContext,
|
||||||
InferGetServerSidePropsType,
|
InferGetServerSidePropsType,
|
||||||
@@ -83,8 +91,40 @@ const Mongo = (
|
|||||||
</h1>
|
</h1>
|
||||||
<span className="text-sm">{data?.appName}</span>
|
<span className="text-sm">{data?.appName}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="flex flex-row h-fit w-fit gap-2">
|
||||||
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
|
<Badge
|
||||||
|
variant={
|
||||||
|
!data?.serverId
|
||||||
|
? "default"
|
||||||
|
: data?.server?.serverStatus === "active"
|
||||||
|
? "default"
|
||||||
|
: "destructive"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{data?.server?.name || "Dokploy Server"}
|
||||||
|
</Badge>
|
||||||
|
{data?.server?.serverStatus === "inactive" && (
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Label className="break-all w-fit flex flex-row gap-1 items-center">
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground" />
|
||||||
|
</Label>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
className="z-[999] w-[300px]"
|
||||||
|
align="start"
|
||||||
|
side="top"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
You cannot, deploy this application because the server
|
||||||
|
is inactive, please upgrade your plan to add more
|
||||||
|
servers.
|
||||||
|
</span>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{data?.description && (
|
{data?.description && (
|
||||||
<p className="text-sm text-muted-foreground max-w-6xl">
|
<p className="text-sm text-muted-foreground max-w-6xl">
|
||||||
@@ -100,80 +140,100 @@ const Mongo = (
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
</div>
|
</div>
|
||||||
<Tabs
|
{data?.server?.serverStatus === "inactive" ? (
|
||||||
value={tab}
|
<div className="flex h-[55vh] border-2 rounded-xl border-dashed p-4">
|
||||||
defaultValue="general"
|
<div className="max-w-3xl mx-auto flex flex-col items-center justify-center self-center gap-3">
|
||||||
className="w-full"
|
<ServerOff className="size-10 text-muted-foreground self-center" />
|
||||||
onValueChange={(e) => {
|
<span className="text-center text-base text-muted-foreground">
|
||||||
setSab(e as TabState);
|
This service is hosted on the server {data.server.name}, but this
|
||||||
const newPath = `/dashboard/project/${projectId}/services/mongo/${mongoId}?tab=${e}`;
|
server has been disabled because your current plan doesn't include
|
||||||
|
enough servers. Please purchase more servers to regain access to
|
||||||
router.push(newPath, undefined, { shallow: true });
|
this application.
|
||||||
}}
|
</span>
|
||||||
>
|
<span className="text-center text-base text-muted-foreground">
|
||||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
Go to{" "}
|
||||||
<TabsList
|
<Link href="/dashboard/settings/billing" className="text-primary">
|
||||||
className={cn(
|
Billing
|
||||||
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
</Link>
|
||||||
data?.serverId ? "md:grid-cols-5" : "md:grid-cols-6",
|
</span>
|
||||||
)}
|
|
||||||
>
|
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
|
||||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
|
||||||
{!data?.serverId && (
|
|
||||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
|
||||||
)}
|
|
||||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
<div className="flex flex-row gap-2">
|
|
||||||
<UpdateMongo mongoId={mongoId} />
|
|
||||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
|
||||||
<DeleteMongo mongoId={mongoId} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<Tabs
|
||||||
|
value={tab}
|
||||||
|
defaultValue="general"
|
||||||
|
className="w-full"
|
||||||
|
onValueChange={(e) => {
|
||||||
|
setSab(e as TabState);
|
||||||
|
const newPath = `/dashboard/project/${projectId}/services/mongo/${mongoId}?tab=${e}`;
|
||||||
|
|
||||||
<TabsContent value="general">
|
router.push(newPath, undefined, { shallow: true });
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
}}
|
||||||
<ShowGeneralMongo mongoId={mongoId} />
|
>
|
||||||
<ShowInternalMongoCredentials mongoId={mongoId} />
|
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||||
<ShowExternalMongoCredentials mongoId={mongoId} />
|
<TabsList
|
||||||
|
className={cn(
|
||||||
|
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
||||||
|
data?.serverId ? "md:grid-cols-5" : "md:grid-cols-6",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
|
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||||
|
{!data?.serverId && (
|
||||||
|
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||||
|
)}
|
||||||
|
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||||
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<div className="flex flex-row gap-2">
|
||||||
|
<UpdateMongo mongoId={mongoId} />
|
||||||
|
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
||||||
|
<DeleteMongo mongoId={mongoId} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value="environment">
|
<TabsContent value="general">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
|
||||||
<ShowMongoEnvironment mongoId={mongoId} />
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
{!data?.serverId && (
|
|
||||||
<TabsContent value="monitoring">
|
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<DockerMonitoring appName={data?.appName || ""} />
|
<ShowGeneralMongo mongoId={mongoId} />
|
||||||
|
<ShowInternalMongoCredentials mongoId={mongoId} />
|
||||||
|
<ShowExternalMongoCredentials mongoId={mongoId} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
)}
|
<TabsContent value="environment">
|
||||||
<TabsContent value="logs">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<ShowMongoEnvironment mongoId={mongoId} />
|
||||||
<ShowDockerLogs
|
</div>
|
||||||
serverId={data?.serverId || ""}
|
</TabsContent>
|
||||||
appName={data?.appName || ""}
|
{!data?.serverId && (
|
||||||
/>
|
<TabsContent value="monitoring">
|
||||||
</div>
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
</TabsContent>
|
<DockerMonitoring appName={data?.appName || ""} />
|
||||||
<TabsContent value="backups">
|
</div>
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
</TabsContent>
|
||||||
<ShowBackupMongo mongoId={mongoId} />
|
)}
|
||||||
</div>
|
<TabsContent value="logs">
|
||||||
</TabsContent>
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<TabsContent value="advanced">
|
<ShowDockerLogs
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
serverId={data?.serverId || ""}
|
||||||
<ShowAdvancedMongo mongoId={mongoId} />
|
appName={data?.appName || ""}
|
||||||
</div>
|
/>
|
||||||
</TabsContent>
|
</div>
|
||||||
</Tabs>
|
</TabsContent>
|
||||||
|
<TabsContent value="backups">
|
||||||
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
|
<ShowBackupMongo mongoId={mongoId} />
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="advanced">
|
||||||
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
|
<ShowAdvancedMongo mongoId={mongoId} />
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,12 +17,20 @@ import {
|
|||||||
BreadcrumbItem,
|
BreadcrumbItem,
|
||||||
BreadcrumbLink,
|
BreadcrumbLink,
|
||||||
} from "@/components/ui/breadcrumb";
|
} from "@/components/ui/breadcrumb";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { validateRequest } from "@dokploy/server";
|
import { validateRequest } from "@dokploy/server";
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
|
import { HelpCircle, ServerOff } from "lucide-react";
|
||||||
import type {
|
import type {
|
||||||
GetServerSidePropsContext,
|
GetServerSidePropsContext,
|
||||||
InferGetServerSidePropsType,
|
InferGetServerSidePropsType,
|
||||||
@@ -81,8 +89,40 @@ const MySql = (
|
|||||||
</h1>
|
</h1>
|
||||||
<span className="text-sm">{data?.appName}</span>
|
<span className="text-sm">{data?.appName}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="flex flex-row h-fit w-fit gap-2">
|
||||||
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
|
<Badge
|
||||||
|
variant={
|
||||||
|
!data?.serverId
|
||||||
|
? "default"
|
||||||
|
: data?.server?.serverStatus === "active"
|
||||||
|
? "default"
|
||||||
|
: "destructive"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{data?.server?.name || "Dokploy Server"}
|
||||||
|
</Badge>
|
||||||
|
{data?.server?.serverStatus === "inactive" && (
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Label className="break-all w-fit flex flex-row gap-1 items-center">
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground" />
|
||||||
|
</Label>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
className="z-[999] w-[300px]"
|
||||||
|
align="start"
|
||||||
|
side="top"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
You cannot, deploy this application because the server
|
||||||
|
is inactive, please upgrade your plan to add more
|
||||||
|
servers.
|
||||||
|
</span>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{data?.description && (
|
{data?.description && (
|
||||||
<p className="text-sm text-muted-foreground max-w-6xl">
|
<p className="text-sm text-muted-foreground max-w-6xl">
|
||||||
@@ -99,80 +139,100 @@ const MySql = (
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
</div>
|
</div>
|
||||||
<Tabs
|
{data?.server?.serverStatus === "inactive" ? (
|
||||||
value={tab}
|
<div className="flex h-[55vh] border-2 rounded-xl border-dashed p-4">
|
||||||
defaultValue="general"
|
<div className="max-w-3xl mx-auto flex flex-col items-center justify-center self-center gap-3">
|
||||||
className="w-full"
|
<ServerOff className="size-10 text-muted-foreground self-center" />
|
||||||
onValueChange={(e) => {
|
<span className="text-center text-base text-muted-foreground">
|
||||||
setSab(e as TabState);
|
This service is hosted on the server {data.server.name}, but this
|
||||||
const newPath = `/dashboard/project/${projectId}/services/mysql/${mysqlId}?tab=${e}`;
|
server has been disabled because your current plan doesn't include
|
||||||
|
enough servers. Please purchase more servers to regain access to
|
||||||
router.push(newPath, undefined, { shallow: true });
|
this application.
|
||||||
}}
|
</span>
|
||||||
>
|
<span className="text-center text-base text-muted-foreground">
|
||||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
Go to{" "}
|
||||||
<TabsList
|
<Link href="/dashboard/settings/billing" className="text-primary">
|
||||||
className={cn(
|
Billing
|
||||||
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
</Link>
|
||||||
data?.serverId ? "md:grid-cols-5" : "md:grid-cols-6",
|
</span>
|
||||||
)}
|
|
||||||
>
|
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
|
||||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
|
||||||
{!data?.serverId && (
|
|
||||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
|
||||||
)}
|
|
||||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
<div className="flex flex-row gap-2">
|
|
||||||
<UpdateMysql mysqlId={mysqlId} />
|
|
||||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
|
||||||
<DeleteMysql mysqlId={mysqlId} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<Tabs
|
||||||
|
value={tab}
|
||||||
|
defaultValue="general"
|
||||||
|
className="w-full"
|
||||||
|
onValueChange={(e) => {
|
||||||
|
setSab(e as TabState);
|
||||||
|
const newPath = `/dashboard/project/${projectId}/services/mysql/${mysqlId}?tab=${e}`;
|
||||||
|
|
||||||
<TabsContent value="general">
|
router.push(newPath, undefined, { shallow: true });
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
}}
|
||||||
<ShowGeneralMysql mysqlId={mysqlId} />
|
>
|
||||||
<ShowInternalMysqlCredentials mysqlId={mysqlId} />
|
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||||
<ShowExternalMysqlCredentials mysqlId={mysqlId} />
|
<TabsList
|
||||||
|
className={cn(
|
||||||
|
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
||||||
|
data?.serverId ? "md:grid-cols-5" : "md:grid-cols-6",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
|
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||||
|
{!data?.serverId && (
|
||||||
|
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||||
|
)}
|
||||||
|
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||||
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<div className="flex flex-row gap-2">
|
||||||
|
<UpdateMysql mysqlId={mysqlId} />
|
||||||
|
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
||||||
|
<DeleteMysql mysqlId={mysqlId} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value="environment" className="w-full">
|
<TabsContent value="general">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
|
||||||
<ShowMysqlEnvironment mysqlId={mysqlId} />
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
{!data?.serverId && (
|
|
||||||
<TabsContent value="monitoring">
|
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<DockerMonitoring appName={data?.appName || ""} />
|
<ShowGeneralMysql mysqlId={mysqlId} />
|
||||||
|
<ShowInternalMysqlCredentials mysqlId={mysqlId} />
|
||||||
|
<ShowExternalMysqlCredentials mysqlId={mysqlId} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
)}
|
<TabsContent value="environment" className="w-full">
|
||||||
<TabsContent value="logs">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<ShowMysqlEnvironment mysqlId={mysqlId} />
|
||||||
<ShowDockerLogs
|
</div>
|
||||||
serverId={data?.serverId || ""}
|
</TabsContent>
|
||||||
appName={data?.appName || ""}
|
{!data?.serverId && (
|
||||||
/>
|
<TabsContent value="monitoring">
|
||||||
</div>
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
</TabsContent>
|
<DockerMonitoring appName={data?.appName || ""} />
|
||||||
<TabsContent value="backups">
|
</div>
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
</TabsContent>
|
||||||
<ShowBackupMySql mysqlId={mysqlId} />
|
)}
|
||||||
</div>
|
<TabsContent value="logs">
|
||||||
</TabsContent>
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<TabsContent value="advanced">
|
<ShowDockerLogs
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
serverId={data?.serverId || ""}
|
||||||
<ShowAdvancedMysql mysqlId={mysqlId} />
|
appName={data?.appName || ""}
|
||||||
</div>
|
/>
|
||||||
</TabsContent>
|
</div>
|
||||||
</Tabs>
|
</TabsContent>
|
||||||
|
<TabsContent value="backups">
|
||||||
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
|
<ShowBackupMySql mysqlId={mysqlId} />
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="advanced">
|
||||||
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
|
<ShowAdvancedMysql mysqlId={mysqlId} />
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,12 +17,20 @@ import {
|
|||||||
BreadcrumbItem,
|
BreadcrumbItem,
|
||||||
BreadcrumbLink,
|
BreadcrumbLink,
|
||||||
} from "@/components/ui/breadcrumb";
|
} from "@/components/ui/breadcrumb";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { validateRequest } from "@dokploy/server";
|
import { validateRequest } from "@dokploy/server";
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
|
import { HelpCircle, ServerOff } from "lucide-react";
|
||||||
import type {
|
import type {
|
||||||
GetServerSidePropsContext,
|
GetServerSidePropsContext,
|
||||||
InferGetServerSidePropsType,
|
InferGetServerSidePropsType,
|
||||||
@@ -82,8 +90,40 @@ const Postgresql = (
|
|||||||
</h1>
|
</h1>
|
||||||
<span className="text-sm">{data?.appName}</span>
|
<span className="text-sm">{data?.appName}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="flex flex-row h-fit w-fit gap-2">
|
||||||
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
|
<Badge
|
||||||
|
variant={
|
||||||
|
!data?.serverId
|
||||||
|
? "default"
|
||||||
|
: data?.server?.serverStatus === "active"
|
||||||
|
? "default"
|
||||||
|
: "destructive"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{data?.server?.name || "Dokploy Server"}
|
||||||
|
</Badge>
|
||||||
|
{data?.server?.serverStatus === "inactive" && (
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Label className="break-all w-fit flex flex-row gap-1 items-center">
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground" />
|
||||||
|
</Label>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
className="z-[999] w-[300px]"
|
||||||
|
align="start"
|
||||||
|
side="top"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
You cannot, deploy this application because the server
|
||||||
|
is inactive, please upgrade your plan to add more
|
||||||
|
servers.
|
||||||
|
</span>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{data?.description && (
|
{data?.description && (
|
||||||
<p className="text-sm text-muted-foreground max-w-6xl">
|
<p className="text-sm text-muted-foreground max-w-6xl">
|
||||||
@@ -100,80 +140,100 @@ const Postgresql = (
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
</div>
|
</div>
|
||||||
<Tabs
|
{data?.server?.serverStatus === "inactive" ? (
|
||||||
value={tab}
|
<div className="flex h-[55vh] border-2 rounded-xl border-dashed p-4">
|
||||||
defaultValue="general"
|
<div className="max-w-3xl mx-auto flex flex-col items-center justify-center self-center gap-3">
|
||||||
className="w-full"
|
<ServerOff className="size-10 text-muted-foreground self-center" />
|
||||||
onValueChange={(e) => {
|
<span className="text-center text-base text-muted-foreground">
|
||||||
setSab(e as TabState);
|
This service is hosted on the server {data.server.name}, but this
|
||||||
const newPath = `/dashboard/project/${projectId}/services/postgres/${postgresId}?tab=${e}`;
|
server has been disabled because your current plan doesn't include
|
||||||
|
enough servers. Please purchase more servers to regain access to
|
||||||
router.push(newPath, undefined, { shallow: true });
|
this application.
|
||||||
}}
|
</span>
|
||||||
>
|
<span className="text-center text-base text-muted-foreground">
|
||||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
Go to{" "}
|
||||||
<TabsList
|
<Link href="/dashboard/settings/billing" className="text-primary">
|
||||||
className={cn(
|
Billing
|
||||||
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
</Link>
|
||||||
data?.serverId ? "md:grid-cols-5" : "md:grid-cols-6",
|
</span>
|
||||||
)}
|
|
||||||
>
|
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
|
||||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
|
||||||
{!data?.serverId && (
|
|
||||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
|
||||||
)}
|
|
||||||
<TabsTrigger value="backups">Backups</TabsTrigger>
|
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
<div className="flex flex-row gap-2">
|
|
||||||
<UpdatePostgres postgresId={postgresId} />
|
|
||||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
|
||||||
<DeletePostgres postgresId={postgresId} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<Tabs
|
||||||
|
value={tab}
|
||||||
|
defaultValue="general"
|
||||||
|
className="w-full"
|
||||||
|
onValueChange={(e) => {
|
||||||
|
setSab(e as TabState);
|
||||||
|
const newPath = `/dashboard/project/${projectId}/services/postgres/${postgresId}?tab=${e}`;
|
||||||
|
|
||||||
<TabsContent value="general">
|
router.push(newPath, undefined, { shallow: true });
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
}}
|
||||||
<ShowGeneralPostgres postgresId={postgresId} />
|
>
|
||||||
<ShowInternalPostgresCredentials postgresId={postgresId} />
|
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||||
<ShowExternalPostgresCredentials postgresId={postgresId} />
|
<TabsList
|
||||||
|
className={cn(
|
||||||
|
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
||||||
|
data?.serverId ? "md:grid-cols-5" : "md:grid-cols-6",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
|
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||||
|
{!data?.serverId && (
|
||||||
|
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||||
|
)}
|
||||||
|
<TabsTrigger value="backups">Backups</TabsTrigger>
|
||||||
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<div className="flex flex-row gap-2">
|
||||||
|
<UpdatePostgres postgresId={postgresId} />
|
||||||
|
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
||||||
|
<DeletePostgres postgresId={postgresId} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value="environment">
|
<TabsContent value="general">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
|
||||||
<ShowPostgresEnvironment postgresId={postgresId} />
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
{!data?.serverId && (
|
|
||||||
<TabsContent value="monitoring">
|
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<DockerMonitoring appName={data?.appName || ""} />
|
<ShowGeneralPostgres postgresId={postgresId} />
|
||||||
|
<ShowInternalPostgresCredentials postgresId={postgresId} />
|
||||||
|
<ShowExternalPostgresCredentials postgresId={postgresId} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
)}
|
<TabsContent value="environment">
|
||||||
<TabsContent value="logs">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<ShowPostgresEnvironment postgresId={postgresId} />
|
||||||
<ShowDockerLogs
|
</div>
|
||||||
serverId={data?.serverId || ""}
|
</TabsContent>
|
||||||
appName={data?.appName || ""}
|
{!data?.serverId && (
|
||||||
/>
|
<TabsContent value="monitoring">
|
||||||
</div>
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
</TabsContent>
|
<DockerMonitoring appName={data?.appName || ""} />
|
||||||
<TabsContent value="backups">
|
</div>
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
</TabsContent>
|
||||||
<ShowBackupPostgres postgresId={postgresId} />
|
)}
|
||||||
</div>
|
<TabsContent value="logs">
|
||||||
</TabsContent>
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<TabsContent value="advanced">
|
<ShowDockerLogs
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
serverId={data?.serverId || ""}
|
||||||
<ShowAdvancedPostgres postgresId={postgresId} />
|
appName={data?.appName || ""}
|
||||||
</div>
|
/>
|
||||||
</TabsContent>
|
</div>
|
||||||
</Tabs>
|
</TabsContent>
|
||||||
|
<TabsContent value="backups">
|
||||||
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
|
<ShowBackupPostgres postgresId={postgresId} />
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="advanced">
|
||||||
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
|
<ShowAdvancedPostgres postgresId={postgresId} />
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -16,12 +16,20 @@ import {
|
|||||||
BreadcrumbItem,
|
BreadcrumbItem,
|
||||||
BreadcrumbLink,
|
BreadcrumbLink,
|
||||||
} from "@/components/ui/breadcrumb";
|
} from "@/components/ui/breadcrumb";
|
||||||
|
import { Label } from "@/components/ui/label";
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
|
||||||
|
import {
|
||||||
|
Tooltip,
|
||||||
|
TooltipContent,
|
||||||
|
TooltipProvider,
|
||||||
|
TooltipTrigger,
|
||||||
|
} from "@/components/ui/tooltip";
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { appRouter } from "@/server/api/root";
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { validateRequest } from "@dokploy/server";
|
import { validateRequest } from "@dokploy/server";
|
||||||
import { createServerSideHelpers } from "@trpc/react-query/server";
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
|
import { HelpCircle, ServerOff } from "lucide-react";
|
||||||
import type {
|
import type {
|
||||||
GetServerSidePropsContext,
|
GetServerSidePropsContext,
|
||||||
InferGetServerSidePropsType,
|
InferGetServerSidePropsType,
|
||||||
@@ -81,8 +89,40 @@ const Redis = (
|
|||||||
</h1>
|
</h1>
|
||||||
<span className="text-sm">{data?.appName}</span>
|
<span className="text-sm">{data?.appName}</span>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
<div className="flex flex-row h-fit w-fit gap-2">
|
||||||
<Badge>{data?.server?.name || "Dokploy Server"}</Badge>
|
<Badge
|
||||||
|
variant={
|
||||||
|
!data?.serverId
|
||||||
|
? "default"
|
||||||
|
: data?.server?.serverStatus === "active"
|
||||||
|
? "default"
|
||||||
|
: "destructive"
|
||||||
|
}
|
||||||
|
>
|
||||||
|
{data?.server?.name || "Dokploy Server"}
|
||||||
|
</Badge>
|
||||||
|
{data?.server?.serverStatus === "inactive" && (
|
||||||
|
<TooltipProvider delayDuration={0}>
|
||||||
|
<Tooltip>
|
||||||
|
<TooltipTrigger asChild>
|
||||||
|
<Label className="break-all w-fit flex flex-row gap-1 items-center">
|
||||||
|
<HelpCircle className="size-4 text-muted-foreground" />
|
||||||
|
</Label>
|
||||||
|
</TooltipTrigger>
|
||||||
|
<TooltipContent
|
||||||
|
className="z-[999] w-[300px]"
|
||||||
|
align="start"
|
||||||
|
side="top"
|
||||||
|
>
|
||||||
|
<span>
|
||||||
|
You cannot, deploy this application because the server
|
||||||
|
is inactive, please upgrade your plan to add more
|
||||||
|
servers.
|
||||||
|
</span>
|
||||||
|
</TooltipContent>
|
||||||
|
</Tooltip>
|
||||||
|
</TooltipProvider>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{data?.description && (
|
{data?.description && (
|
||||||
<p className="text-sm text-muted-foreground max-w-6xl">
|
<p className="text-sm text-muted-foreground max-w-6xl">
|
||||||
@@ -99,74 +139,94 @@ const Redis = (
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
</div>
|
</div>
|
||||||
<Tabs
|
{data?.server?.serverStatus === "inactive" ? (
|
||||||
value={tab}
|
<div className="flex h-[55vh] border-2 rounded-xl border-dashed p-4">
|
||||||
defaultValue="general"
|
<div className="max-w-3xl mx-auto flex flex-col items-center justify-center self-center gap-3">
|
||||||
className="w-full"
|
<ServerOff className="size-10 text-muted-foreground self-center" />
|
||||||
onValueChange={(e) => {
|
<span className="text-center text-base text-muted-foreground">
|
||||||
setSab(e as TabState);
|
This service is hosted on the server {data.server.name}, but this
|
||||||
const newPath = `/dashboard/project/${projectId}/services/redis/${redisId}?tab=${e}`;
|
server has been disabled because your current plan doesn't include
|
||||||
|
enough servers. Please purchase more servers to regain access to
|
||||||
router.push(newPath, undefined, { shallow: true });
|
this application.
|
||||||
}}
|
</span>
|
||||||
>
|
<span className="text-center text-base text-muted-foreground">
|
||||||
<div className="flex flex-row items-center justify-between w-full gap-4">
|
Go to{" "}
|
||||||
<TabsList
|
<Link href="/dashboard/settings/billing" className="text-primary">
|
||||||
className={cn(
|
Billing
|
||||||
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
</Link>
|
||||||
data?.serverId ? "md:grid-cols-4" : "md:grid-cols-5",
|
</span>
|
||||||
)}
|
|
||||||
>
|
|
||||||
<TabsTrigger value="general">General</TabsTrigger>
|
|
||||||
<TabsTrigger value="environment">Environment</TabsTrigger>
|
|
||||||
{!data?.serverId && (
|
|
||||||
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
|
||||||
)}
|
|
||||||
<TabsTrigger value="logs">Logs</TabsTrigger>
|
|
||||||
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
|
||||||
</TabsList>
|
|
||||||
|
|
||||||
<div className="flex flex-row gap-2">
|
|
||||||
<UpdateRedis redisId={redisId} />
|
|
||||||
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
|
||||||
<DeleteRedis redisId={redisId} />
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
) : (
|
||||||
|
<Tabs
|
||||||
|
value={tab}
|
||||||
|
defaultValue="general"
|
||||||
|
className="w-full"
|
||||||
|
onValueChange={(e) => {
|
||||||
|
setSab(e as TabState);
|
||||||
|
const newPath = `/dashboard/project/${projectId}/services/redis/${redisId}?tab=${e}`;
|
||||||
|
|
||||||
<TabsContent value="general">
|
router.push(newPath, undefined, { shallow: true });
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
}}
|
||||||
<ShowGeneralRedis redisId={redisId} />
|
>
|
||||||
<ShowInternalRedisCredentials redisId={redisId} />
|
<div className="flex flex-row items-center justify-between w-full gap-4">
|
||||||
<ShowExternalRedisCredentials redisId={redisId} />
|
<TabsList
|
||||||
|
className={cn(
|
||||||
|
"md:grid md:w-fit max-md:overflow-y-scroll justify-start",
|
||||||
|
data?.serverId ? "md:grid-cols-4" : "md:grid-cols-5",
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<TabsTrigger value="general">General</TabsTrigger>
|
||||||
|
<TabsTrigger value="environment">Environment</TabsTrigger>
|
||||||
|
{!data?.serverId && (
|
||||||
|
<TabsTrigger value="monitoring">Monitoring</TabsTrigger>
|
||||||
|
)}
|
||||||
|
<TabsTrigger value="logs">Logs</TabsTrigger>
|
||||||
|
<TabsTrigger value="advanced">Advanced</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
|
||||||
|
<div className="flex flex-row gap-2">
|
||||||
|
<UpdateRedis redisId={redisId} />
|
||||||
|
{(auth?.rol === "admin" || user?.canDeleteServices) && (
|
||||||
|
<DeleteRedis redisId={redisId} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
|
||||||
<TabsContent value="environment">
|
<TabsContent value="general">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
|
||||||
<ShowRedisEnvironment redisId={redisId} />
|
|
||||||
</div>
|
|
||||||
</TabsContent>
|
|
||||||
{!data?.serverId && (
|
|
||||||
<TabsContent value="monitoring">
|
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<DockerMonitoring appName={data?.appName || ""} />
|
<ShowGeneralRedis redisId={redisId} />
|
||||||
|
<ShowInternalRedisCredentials redisId={redisId} />
|
||||||
|
<ShowExternalRedisCredentials redisId={redisId} />
|
||||||
</div>
|
</div>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
)}
|
<TabsContent value="environment">
|
||||||
<TabsContent value="logs">
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
<ShowRedisEnvironment redisId={redisId} />
|
||||||
<ShowDockerLogs
|
</div>
|
||||||
serverId={data?.serverId || ""}
|
</TabsContent>
|
||||||
appName={data?.appName || ""}
|
{!data?.serverId && (
|
||||||
/>
|
<TabsContent value="monitoring">
|
||||||
</div>
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
</TabsContent>
|
<DockerMonitoring appName={data?.appName || ""} />
|
||||||
<TabsContent value="advanced">
|
</div>
|
||||||
<div className="flex flex-col gap-4 pt-2.5">
|
</TabsContent>
|
||||||
<ShowAdvancedRedis redisId={redisId} />
|
)}
|
||||||
</div>
|
<TabsContent value="logs">
|
||||||
</TabsContent>
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
</Tabs>
|
<ShowDockerLogs
|
||||||
|
serverId={data?.serverId || ""}
|
||||||
|
appName={data?.appName || ""}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="advanced">
|
||||||
|
<div className="flex flex-col gap-4 pt-2.5">
|
||||||
|
<ShowAdvancedRedis redisId={redisId} />
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
import { ShowProjects } from "@/components/dashboard/projects/show";
|
import { ShowProjects } from "@/components/dashboard/projects/show";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
|
import { appRouter } from "@/server/api/root";
|
||||||
import { validateRequest } from "@dokploy/server";
|
import { validateRequest } from "@dokploy/server";
|
||||||
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import React, { type ReactElement } from "react";
|
import React, { type ReactElement } from "react";
|
||||||
|
import superjson from "superjson";
|
||||||
|
|
||||||
const Dashboard = () => {
|
const Dashboard = () => {
|
||||||
return <ShowProjects />;
|
return <ShowProjects />;
|
||||||
@@ -16,7 +19,22 @@ Dashboard.getLayout = (page: ReactElement) => {
|
|||||||
export async function getServerSideProps(
|
export async function getServerSideProps(
|
||||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||||
) {
|
) {
|
||||||
const { user } = await validateRequest(ctx.req, ctx.res);
|
const { req, res } = ctx;
|
||||||
|
const { user, session } = await validateRequest(req, res);
|
||||||
|
|
||||||
|
const helpers = createServerSideHelpers({
|
||||||
|
router: appRouter,
|
||||||
|
ctx: {
|
||||||
|
req: req as any,
|
||||||
|
res: res as any,
|
||||||
|
db: null as any,
|
||||||
|
session: session,
|
||||||
|
user: user,
|
||||||
|
},
|
||||||
|
transformer: superjson,
|
||||||
|
});
|
||||||
|
|
||||||
|
await helpers.settings.isCloud.prefetch();
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
@@ -27,6 +45,8 @@ export async function getServerSideProps(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
props: {},
|
props: {
|
||||||
|
trpcState: helpers.dehydrate(),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { AppearanceForm } from "@/components/dashboard/settings/appearance-form";
|
import { AppearanceForm } from "@/components/dashboard/settings/appearance-form";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||||
|
import { appRouter } from "@/server/api/root";
|
||||||
import { validateRequest } from "@dokploy/server";
|
import { validateRequest } from "@dokploy/server";
|
||||||
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import React, { type ReactElement } from "react";
|
import React, { type ReactElement } from "react";
|
||||||
|
import superjson from "superjson";
|
||||||
|
|
||||||
const Page = () => {
|
const Page = () => {
|
||||||
return (
|
return (
|
||||||
@@ -25,7 +28,30 @@ Page.getLayout = (page: ReactElement) => {
|
|||||||
export async function getServerSideProps(
|
export async function getServerSideProps(
|
||||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||||
) {
|
) {
|
||||||
const { user } = await validateRequest(ctx.req, ctx.res);
|
const { req, res } = ctx;
|
||||||
|
const { user, session } = await validateRequest(req, res);
|
||||||
|
|
||||||
|
const helpers = createServerSideHelpers({
|
||||||
|
router: appRouter,
|
||||||
|
ctx: {
|
||||||
|
req: req as any,
|
||||||
|
res: res as any,
|
||||||
|
db: null as any,
|
||||||
|
session: session,
|
||||||
|
user: user,
|
||||||
|
},
|
||||||
|
transformer: superjson,
|
||||||
|
});
|
||||||
|
|
||||||
|
await helpers.settings.isCloud.prefetch();
|
||||||
|
|
||||||
|
await helpers.auth.get.prefetch();
|
||||||
|
if (user?.rol === "user") {
|
||||||
|
await helpers.user.byAuthId.prefetch({
|
||||||
|
authId: user.authId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -34,8 +60,9 @@ export async function getServerSideProps(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {},
|
props: {
|
||||||
|
trpcState: helpers.dehydrate(),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
67
apps/dokploy/pages/dashboard/settings/billing.tsx
Normal file
67
apps/dokploy/pages/dashboard/settings/billing.tsx
Normal 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(),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
import { ShowCertificates } from "@/components/dashboard/settings/certificates/show-certificates";
|
import { ShowCertificates } from "@/components/dashboard/settings/certificates/show-certificates";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||||
|
import { appRouter } from "@/server/api/root";
|
||||||
import { validateRequest } from "@dokploy/server";
|
import { validateRequest } from "@dokploy/server";
|
||||||
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import React, { type ReactElement } from "react";
|
import React, { type ReactElement } from "react";
|
||||||
|
import superjson from "superjson";
|
||||||
const Page = () => {
|
const Page = () => {
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col gap-4 w-full">
|
<div className="flex flex-col gap-4 w-full">
|
||||||
@@ -25,7 +27,8 @@ Page.getLayout = (page: ReactElement) => {
|
|||||||
export async function getServerSideProps(
|
export async function getServerSideProps(
|
||||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||||
) {
|
) {
|
||||||
const { user, session } = await validateRequest(ctx.req, ctx.res);
|
const { req, res } = ctx;
|
||||||
|
const { user, session } = await validateRequest(req, res);
|
||||||
if (!user || user.rol === "user") {
|
if (!user || user.rol === "user") {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -35,7 +38,23 @@ export async function getServerSideProps(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const helpers = createServerSideHelpers({
|
||||||
|
router: appRouter,
|
||||||
|
ctx: {
|
||||||
|
req: req as any,
|
||||||
|
res: res as any,
|
||||||
|
db: null as any,
|
||||||
|
session: session,
|
||||||
|
user: user,
|
||||||
|
},
|
||||||
|
transformer: superjson,
|
||||||
|
});
|
||||||
|
await helpers.auth.get.prefetch();
|
||||||
|
await helpers.settings.isCloud.prefetch();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {},
|
props: {
|
||||||
|
trpcState: helpers.dehydrate(),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { ShowNodes } from "@/components/dashboard/settings/cluster/nodes/show-nodes";
|
import { ShowNodes } from "@/components/dashboard/settings/cluster/nodes/show-nodes";
|
||||||
import { ShowRegistry } from "@/components/dashboard/settings/cluster/registry/show-registry";
|
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||||
|
import { appRouter } from "@/server/api/root";
|
||||||
import { IS_CLOUD, validateRequest } from "@dokploy/server";
|
import { IS_CLOUD, validateRequest } from "@dokploy/server";
|
||||||
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import React, { type ReactElement } from "react";
|
import React, { type ReactElement } from "react";
|
||||||
|
import superjson from "superjson";
|
||||||
|
|
||||||
const Page = () => {
|
const Page = () => {
|
||||||
return (
|
return (
|
||||||
@@ -26,6 +28,7 @@ Page.getLayout = (page: ReactElement) => {
|
|||||||
export async function getServerSideProps(
|
export async function getServerSideProps(
|
||||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||||
) {
|
) {
|
||||||
|
const { req, res } = ctx;
|
||||||
if (IS_CLOUD) {
|
if (IS_CLOUD) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
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 {
|
return {
|
||||||
props: {},
|
props: {
|
||||||
|
trpcState: helpers.dehydrate(),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { ShowDestinations } from "@/components/dashboard/settings/destination/show-destinations";
|
import { ShowDestinations } from "@/components/dashboard/settings/destination/show-destinations";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||||
|
import { appRouter } from "@/server/api/root";
|
||||||
import { validateRequest } from "@dokploy/server";
|
import { validateRequest } from "@dokploy/server";
|
||||||
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import React, { type ReactElement } from "react";
|
import React, { type ReactElement } from "react";
|
||||||
|
import superjson from "superjson";
|
||||||
|
|
||||||
const Page = () => {
|
const Page = () => {
|
||||||
return (
|
return (
|
||||||
@@ -25,7 +28,8 @@ Page.getLayout = (page: ReactElement) => {
|
|||||||
export async function getServerSideProps(
|
export async function getServerSideProps(
|
||||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||||
) {
|
) {
|
||||||
const { user, session } = await validateRequest(ctx.req, ctx.res);
|
const { req, res } = ctx;
|
||||||
|
const { user, session } = await validateRequest(req, res);
|
||||||
if (!user || user.rol === "user") {
|
if (!user || user.rol === "user") {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -35,7 +39,23 @@ export async function getServerSideProps(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const helpers = createServerSideHelpers({
|
||||||
|
router: appRouter,
|
||||||
|
ctx: {
|
||||||
|
req: req as any,
|
||||||
|
res: res as any,
|
||||||
|
db: null as any,
|
||||||
|
session: session,
|
||||||
|
user: user,
|
||||||
|
},
|
||||||
|
transformer: superjson,
|
||||||
|
});
|
||||||
|
await helpers.auth.get.prefetch();
|
||||||
|
await helpers.settings.isCloud.prefetch();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {},
|
props: {
|
||||||
|
trpcState: helpers.dehydrate(),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -49,9 +49,10 @@ export async function getServerSideProps(
|
|||||||
},
|
},
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
});
|
});
|
||||||
|
await helpers.auth.get.prefetch();
|
||||||
try {
|
try {
|
||||||
await helpers.project.all.prefetch();
|
await helpers.project.all.prefetch();
|
||||||
|
await helpers.settings.isCloud.prefetch();
|
||||||
const auth = await helpers.auth.get.fetch();
|
const auth = await helpers.auth.get.fetch();
|
||||||
|
|
||||||
if (auth.rol === "user") {
|
if (auth.rol === "user") {
|
||||||
|
|||||||
@@ -1,10 +1,12 @@
|
|||||||
import { ShowDestinations } from "@/components/dashboard/settings/destination/show-destinations";
|
|
||||||
import { ShowNotifications } from "@/components/dashboard/settings/notifications/show-notifications";
|
import { ShowNotifications } from "@/components/dashboard/settings/notifications/show-notifications";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||||
|
import { appRouter } from "@/server/api/root";
|
||||||
import { validateRequest } from "@dokploy/server";
|
import { validateRequest } from "@dokploy/server";
|
||||||
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import React, { type ReactElement } from "react";
|
import React, { type ReactElement } from "react";
|
||||||
|
import superjson from "superjson";
|
||||||
|
|
||||||
const Page = () => {
|
const Page = () => {
|
||||||
return (
|
return (
|
||||||
@@ -26,7 +28,8 @@ Page.getLayout = (page: ReactElement) => {
|
|||||||
export async function getServerSideProps(
|
export async function getServerSideProps(
|
||||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||||
) {
|
) {
|
||||||
const { user, session } = await validateRequest(ctx.req, ctx.res);
|
const { req, res } = ctx;
|
||||||
|
const { user, session } = await validateRequest(req, res);
|
||||||
if (!user || user.rol === "user") {
|
if (!user || user.rol === "user") {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -36,7 +39,23 @@ export async function getServerSideProps(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const helpers = createServerSideHelpers({
|
||||||
|
router: appRouter,
|
||||||
|
ctx: {
|
||||||
|
req: req as any,
|
||||||
|
res: res as any,
|
||||||
|
db: null as any,
|
||||||
|
session: session,
|
||||||
|
user: user,
|
||||||
|
},
|
||||||
|
transformer: superjson,
|
||||||
|
});
|
||||||
|
await helpers.auth.get.prefetch();
|
||||||
|
await helpers.settings.isCloud.prefetch();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {},
|
props: {
|
||||||
|
trpcState: helpers.dehydrate(),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,13 @@ import { GenerateToken } from "@/components/dashboard/settings/profile/generate-
|
|||||||
import { ProfileForm } from "@/components/dashboard/settings/profile/profile-form";
|
import { ProfileForm } from "@/components/dashboard/settings/profile/profile-form";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||||
|
import { appRouter } from "@/server/api/root";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { validateRequest } from "@dokploy/server";
|
import { validateRequest } from "@dokploy/server";
|
||||||
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import React, { type ReactElement } from "react";
|
import React, { type ReactElement } from "react";
|
||||||
|
import superjson from "superjson";
|
||||||
|
|
||||||
const Page = () => {
|
const Page = () => {
|
||||||
const { data } = api.auth.get.useQuery();
|
const { data } = api.auth.get.useQuery();
|
||||||
@@ -37,7 +40,29 @@ Page.getLayout = (page: ReactElement) => {
|
|||||||
export async function getServerSideProps(
|
export async function getServerSideProps(
|
||||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||||
) {
|
) {
|
||||||
const { user } = await validateRequest(ctx.req, ctx.res);
|
const { req, res } = ctx;
|
||||||
|
const { user, session } = await validateRequest(req, res);
|
||||||
|
|
||||||
|
const helpers = createServerSideHelpers({
|
||||||
|
router: appRouter,
|
||||||
|
ctx: {
|
||||||
|
req: req as any,
|
||||||
|
res: res as any,
|
||||||
|
db: null as any,
|
||||||
|
session: session,
|
||||||
|
user: user,
|
||||||
|
},
|
||||||
|
transformer: superjson,
|
||||||
|
});
|
||||||
|
|
||||||
|
await helpers.settings.isCloud.prefetch();
|
||||||
|
await helpers.auth.get.prefetch();
|
||||||
|
if (user?.rol === "user") {
|
||||||
|
await helpers.user.byAuthId.prefetch({
|
||||||
|
authId: user.authId,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -48,6 +73,8 @@ export async function getServerSideProps(
|
|||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {},
|
props: {
|
||||||
|
trpcState: helpers.dehydrate(),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { ShowRegistry } from "@/components/dashboard/settings/cluster/registry/show-registry";
|
import { ShowRegistry } from "@/components/dashboard/settings/cluster/registry/show-registry";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||||
|
import { appRouter } from "@/server/api/root";
|
||||||
import { validateRequest } from "@dokploy/server";
|
import { validateRequest } from "@dokploy/server";
|
||||||
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import React, { type ReactElement } from "react";
|
import React, { type ReactElement } from "react";
|
||||||
|
import superjson from "superjson";
|
||||||
|
|
||||||
const Page = () => {
|
const Page = () => {
|
||||||
return (
|
return (
|
||||||
@@ -25,7 +28,8 @@ Page.getLayout = (page: ReactElement) => {
|
|||||||
export async function getServerSideProps(
|
export async function getServerSideProps(
|
||||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||||
) {
|
) {
|
||||||
const { user, session } = await validateRequest(ctx.req, ctx.res);
|
const { req, res } = ctx;
|
||||||
|
const { user, session } = await validateRequest(req, res);
|
||||||
if (!user || user.rol === "user") {
|
if (!user || user.rol === "user") {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -34,8 +38,23 @@ export async function getServerSideProps(
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
const helpers = createServerSideHelpers({
|
||||||
|
router: appRouter,
|
||||||
|
ctx: {
|
||||||
|
req: req as any,
|
||||||
|
res: res as any,
|
||||||
|
db: null as any,
|
||||||
|
session: session,
|
||||||
|
user: user,
|
||||||
|
},
|
||||||
|
transformer: superjson,
|
||||||
|
});
|
||||||
|
await helpers.auth.get.prefetch();
|
||||||
|
await helpers.settings.isCloud.prefetch();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {},
|
props: {
|
||||||
|
trpcState: helpers.dehydrate(),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,9 +2,12 @@ import { WebDomain } from "@/components/dashboard/settings/web-domain";
|
|||||||
import { WebServer } from "@/components/dashboard/settings/web-server";
|
import { WebServer } from "@/components/dashboard/settings/web-server";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||||
|
import { appRouter } from "@/server/api/root";
|
||||||
import { IS_CLOUD, validateRequest } from "@dokploy/server";
|
import { IS_CLOUD, validateRequest } from "@dokploy/server";
|
||||||
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import React, { type ReactElement } from "react";
|
import React, { type ReactElement } from "react";
|
||||||
|
import superjson from "superjson";
|
||||||
|
|
||||||
const Page = () => {
|
const Page = () => {
|
||||||
return (
|
return (
|
||||||
@@ -27,6 +30,7 @@ Page.getLayout = (page: ReactElement) => {
|
|||||||
export async function getServerSideProps(
|
export async function getServerSideProps(
|
||||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||||
) {
|
) {
|
||||||
|
const { req, res } = ctx;
|
||||||
if (IS_CLOUD) {
|
if (IS_CLOUD) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
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) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
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 {
|
return {
|
||||||
props: {},
|
props: {
|
||||||
|
trpcState: helpers.dehydrate(),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { ShowServers } from "@/components/dashboard/settings/servers/show-servers";
|
import { ShowServers } from "@/components/dashboard/settings/servers/show-servers";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||||
|
import { appRouter } from "@/server/api/root";
|
||||||
import { validateRequest } from "@dokploy/server";
|
import { validateRequest } from "@dokploy/server";
|
||||||
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import React, { type ReactElement } from "react";
|
import React, { type ReactElement } from "react";
|
||||||
|
import superjson from "superjson";
|
||||||
|
|
||||||
const Page = () => {
|
const Page = () => {
|
||||||
return (
|
return (
|
||||||
@@ -25,7 +28,8 @@ Page.getLayout = (page: ReactElement) => {
|
|||||||
export async function getServerSideProps(
|
export async function getServerSideProps(
|
||||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||||
) {
|
) {
|
||||||
const { user } = await validateRequest(ctx.req, ctx.res);
|
const { req, res } = ctx;
|
||||||
|
const { user, session } = await validateRequest(req, res);
|
||||||
if (!user) {
|
if (!user) {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -43,7 +47,23 @@ export async function getServerSideProps(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const helpers = createServerSideHelpers({
|
||||||
|
router: appRouter,
|
||||||
|
ctx: {
|
||||||
|
req: req as any,
|
||||||
|
res: res as any,
|
||||||
|
db: null as any,
|
||||||
|
session: session,
|
||||||
|
user: user,
|
||||||
|
},
|
||||||
|
transformer: superjson,
|
||||||
|
});
|
||||||
|
await helpers.auth.get.prefetch();
|
||||||
|
await helpers.settings.isCloud.prefetch();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {},
|
props: {
|
||||||
|
trpcState: helpers.dehydrate(),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,7 @@ export async function getServerSideProps(
|
|||||||
try {
|
try {
|
||||||
await helpers.project.all.prefetch();
|
await helpers.project.all.prefetch();
|
||||||
const auth = await helpers.auth.get.fetch();
|
const auth = await helpers.auth.get.fetch();
|
||||||
|
await helpers.settings.isCloud.prefetch();
|
||||||
|
|
||||||
if (auth.rol === "user") {
|
if (auth.rol === "user") {
|
||||||
const user = await helpers.user.byAuthId.fetch({
|
const user = await helpers.user.byAuthId.fetch({
|
||||||
|
|||||||
@@ -1,9 +1,12 @@
|
|||||||
import { ShowUsers } from "@/components/dashboard/settings/users/show-users";
|
import { ShowUsers } from "@/components/dashboard/settings/users/show-users";
|
||||||
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
import { DashboardLayout } from "@/components/layouts/dashboard-layout";
|
||||||
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
import { SettingsLayout } from "@/components/layouts/settings-layout";
|
||||||
|
import { appRouter } from "@/server/api/root";
|
||||||
import { validateRequest } from "@dokploy/server";
|
import { validateRequest } from "@dokploy/server";
|
||||||
|
import { createServerSideHelpers } from "@trpc/react-query/server";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import React, { type ReactElement } from "react";
|
import React, { type ReactElement } from "react";
|
||||||
|
import superjson from "superjson";
|
||||||
|
|
||||||
const Page = () => {
|
const Page = () => {
|
||||||
return (
|
return (
|
||||||
@@ -25,7 +28,8 @@ Page.getLayout = (page: ReactElement) => {
|
|||||||
export async function getServerSideProps(
|
export async function getServerSideProps(
|
||||||
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
ctx: GetServerSidePropsContext<{ serviceId: string }>,
|
||||||
) {
|
) {
|
||||||
const { user } = await validateRequest(ctx.req, ctx.res);
|
const { req, res } = ctx;
|
||||||
|
const { user, session } = await validateRequest(req, res);
|
||||||
if (!user || user.rol === "user") {
|
if (!user || user.rol === "user") {
|
||||||
return {
|
return {
|
||||||
redirect: {
|
redirect: {
|
||||||
@@ -35,7 +39,23 @@ export async function getServerSideProps(
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const helpers = createServerSideHelpers({
|
||||||
|
router: appRouter,
|
||||||
|
ctx: {
|
||||||
|
req: req as any,
|
||||||
|
res: res as any,
|
||||||
|
db: null as any,
|
||||||
|
session: session,
|
||||||
|
user: user,
|
||||||
|
},
|
||||||
|
transformer: superjson,
|
||||||
|
});
|
||||||
|
await helpers.auth.get.prefetch();
|
||||||
|
await helpers.settings.isCloud.prefetch();
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {},
|
props: {
|
||||||
|
trpcState: helpers.dehydrate(),
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Login2FA } from "@/components/auth/login-2fa";
|
import { Login2FA } from "@/components/auth/login-2fa";
|
||||||
import { OnboardingLayout } from "@/components/layouts/onboarding-layout";
|
import { OnboardingLayout } from "@/components/layouts/onboarding-layout";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { Logo } from "@/components/shared/logo";
|
import { Logo } from "@/components/shared/logo";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -55,12 +56,16 @@ type AuthResponse = {
|
|||||||
authId: string;
|
authId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function Home() {
|
interface Props {
|
||||||
|
IS_CLOUD: boolean;
|
||||||
|
}
|
||||||
|
export default function Home({ IS_CLOUD }: Props) {
|
||||||
const [temp, setTemp] = useState<AuthResponse>({
|
const [temp, setTemp] = useState<AuthResponse>({
|
||||||
is2FAEnabled: false,
|
is2FAEnabled: false,
|
||||||
authId: "",
|
authId: "",
|
||||||
});
|
});
|
||||||
const { mutateAsync, isLoading } = api.auth.login.useMutation();
|
const { mutateAsync, isLoading, error, isError } =
|
||||||
|
api.auth.login.useMutation();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const form = useForm<Login>({
|
const form = useForm<Login>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -112,6 +117,12 @@ export default function Home() {
|
|||||||
</CardDescription>
|
</CardDescription>
|
||||||
<Card className="mx-auto w-full max-w-lg bg-transparent ">
|
<Card className="mx-auto w-full max-w-lg bg-transparent ">
|
||||||
<div className="p-3.5" />
|
<div className="p-3.5" />
|
||||||
|
{isError && (
|
||||||
|
<AlertBlock type="error" className="mx-4 my-2">
|
||||||
|
<span>{error?.message}</span>
|
||||||
|
</AlertBlock>
|
||||||
|
)}
|
||||||
|
|
||||||
<CardContent>
|
<CardContent>
|
||||||
{!temp.is2FAEnabled ? (
|
{!temp.is2FAEnabled ? (
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
@@ -167,22 +178,33 @@ export default function Home() {
|
|||||||
|
|
||||||
<div className="flex flex-row justify-between flex-wrap">
|
<div className="flex flex-row justify-between flex-wrap">
|
||||||
<div className="mt-4 text-center text-sm flex flex-row justify-center gap-2">
|
<div className="mt-4 text-center text-sm flex flex-row justify-center gap-2">
|
||||||
<Link
|
{IS_CLOUD && (
|
||||||
className="hover:underline text-muted-foreground"
|
<Link
|
||||||
href="/register"
|
className="hover:underline text-muted-foreground"
|
||||||
>
|
href="/register"
|
||||||
Create an account
|
>
|
||||||
</Link>
|
Create an account
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="mt-4 text-sm flex flex-row justify-center gap-2">
|
<div className="mt-4 text-sm flex flex-row justify-center gap-2">
|
||||||
<Link
|
{IS_CLOUD ? (
|
||||||
className="hover:underline text-muted-foreground"
|
<Link
|
||||||
href="https://docs.dokploy.com/docs/core/get-started/reset-password"
|
className="hover:underline text-muted-foreground"
|
||||||
target="_blank"
|
href="/send-reset-password"
|
||||||
>
|
>
|
||||||
Lost your password?
|
Lost your password?
|
||||||
</Link>
|
</Link>
|
||||||
|
) : (
|
||||||
|
<Link
|
||||||
|
className="hover:underline text-muted-foreground"
|
||||||
|
href="https://docs.dokploy.com/docs/core/get-started/reset-password"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
Lost your password?
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="p-2" />
|
<div className="p-2" />
|
||||||
@@ -212,7 +234,9 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
} catch (error) {}
|
} catch (error) {}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {},
|
props: {
|
||||||
|
IS_CLOUD: IS_CLOUD,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const hasAdmin = await isAdminPresent();
|
const hasAdmin = await isAdminPresent();
|
||||||
|
|||||||
@@ -16,7 +16,7 @@ import {
|
|||||||
} from "@/components/ui/form";
|
} from "@/components/ui/form";
|
||||||
import { Input } from "@/components/ui/input";
|
import { Input } from "@/components/ui/input";
|
||||||
import { api } from "@/utils/api";
|
import { api } from "@/utils/api";
|
||||||
import { getUserByToken } from "@dokploy/server";
|
import { IS_CLOUD, getUserByToken } from "@dokploy/server";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { AlertTriangle } from "lucide-react";
|
import { AlertTriangle } from "lucide-react";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
@@ -69,9 +69,10 @@ type Register = z.infer<typeof registerSchema>;
|
|||||||
interface Props {
|
interface Props {
|
||||||
token: string;
|
token: string;
|
||||||
invitation: Awaited<ReturnType<typeof getUserByToken>>;
|
invitation: Awaited<ReturnType<typeof getUserByToken>>;
|
||||||
|
isCloud: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Invitation = ({ token, invitation }: Props) => {
|
const Invitation = ({ token, invitation, isCloud }: Props) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { data } = api.admin.getUserByToken.useQuery(
|
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>({
|
const form = useForm<Register>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -112,7 +114,9 @@ const Invitation = ({ token, invitation }: Props) => {
|
|||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("User registration succesfuly", {
|
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");
|
router.push("/dashboard/projects");
|
||||||
})
|
})
|
||||||
@@ -146,6 +150,7 @@ const Invitation = ({ token, invitation }: Props) => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
@@ -210,6 +215,25 @@ const Invitation = ({ token, invitation }: Props) => {
|
|||||||
Register
|
Register
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</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>
|
||||||
</Form>
|
</Form>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
@@ -250,6 +274,7 @@ export async function getServerSideProps(ctx: GetServerSidePropsContext) {
|
|||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
|
isCloud: IS_CLOUD,
|
||||||
token: token,
|
token: token,
|
||||||
invitation: invitation,
|
invitation: invitation,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
import { Logo } from "@/components/shared/logo";
|
import { Logo } from "@/components/shared/logo";
|
||||||
import { Button } from "@/components/ui/button";
|
import { Button } from "@/components/ui/button";
|
||||||
import {
|
import {
|
||||||
@@ -72,7 +73,8 @@ interface Props {
|
|||||||
|
|
||||||
const Register = ({ isCloud }: Props) => {
|
const Register = ({ isCloud }: Props) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { mutateAsync, error, isError } = api.auth.createAdmin.useMutation();
|
const { mutateAsync, error, isError, data } =
|
||||||
|
api.auth.createAdmin.useMutation();
|
||||||
|
|
||||||
const form = useForm<Register>({
|
const form = useForm<Register>({
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
@@ -89,14 +91,16 @@ const Register = ({ isCloud }: Props) => {
|
|||||||
|
|
||||||
const onSubmit = async (values: Register) => {
|
const onSubmit = async (values: Register) => {
|
||||||
await mutateAsync({
|
await mutateAsync({
|
||||||
email: values.email,
|
email: values.email.toLowerCase(),
|
||||||
password: values.password,
|
password: values.password,
|
||||||
})
|
})
|
||||||
.then(() => {
|
.then(() => {
|
||||||
toast.success("User registration succesfuly", {
|
toast.success("User registration succesfuly", {
|
||||||
duration: 2000,
|
duration: 2000,
|
||||||
});
|
});
|
||||||
router.push("/");
|
if (!isCloud) {
|
||||||
|
router.push("/");
|
||||||
|
}
|
||||||
})
|
})
|
||||||
.catch((e) => e);
|
.catch((e) => e);
|
||||||
};
|
};
|
||||||
@@ -130,6 +134,14 @@ const Register = ({ isCloud }: Props) => {
|
|||||||
</span>
|
</span>
|
||||||
</div>
|
</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>
|
<CardContent>
|
||||||
<Form {...form}>
|
<Form {...form}>
|
||||||
<form
|
<form
|
||||||
|
|||||||
229
apps/dokploy/pages/reset-password.tsx
Normal file
229
apps/dokploy/pages/reset-password.tsx
Normal 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,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
172
apps/dokploy/pages/send-reset-password.tsx
Normal file
172
apps/dokploy/pages/send-reset-password.tsx
Normal file
@@ -0,0 +1,172 @@
|
|||||||
|
import { Login2FA } from "@/components/auth/login-2fa";
|
||||||
|
import { OnboardingLayout } from "@/components/layouts/onboarding-layout";
|
||||||
|
import { AlertBlock } from "@/components/shared/alert-block";
|
||||||
|
import { Logo } from "@/components/shared/logo";
|
||||||
|
import { Button } from "@/components/ui/button";
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
CardContent,
|
||||||
|
CardDescription,
|
||||||
|
CardTitle,
|
||||||
|
} from "@/components/ui/card";
|
||||||
|
import {
|
||||||
|
Form,
|
||||||
|
FormControl,
|
||||||
|
FormField,
|
||||||
|
FormItem,
|
||||||
|
FormLabel,
|
||||||
|
FormMessage,
|
||||||
|
} from "@/components/ui/form";
|
||||||
|
import { Input } from "@/components/ui/input";
|
||||||
|
import { api } from "@/utils/api";
|
||||||
|
import { IS_CLOUD } from "@dokploy/server";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import type { GetServerSidePropsContext } from "next";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { useRouter } from "next/router";
|
||||||
|
import { type ReactElement, useEffect, useState } from "react";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { toast } from "sonner";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
const loginSchema = z.object({
|
||||||
|
email: z
|
||||||
|
.string()
|
||||||
|
.min(1, {
|
||||||
|
message: "Email is required",
|
||||||
|
})
|
||||||
|
.email({
|
||||||
|
message: "Email must be a valid email",
|
||||||
|
}),
|
||||||
|
});
|
||||||
|
|
||||||
|
type Login = z.infer<typeof loginSchema>;
|
||||||
|
|
||||||
|
type AuthResponse = {
|
||||||
|
is2FAEnabled: boolean;
|
||||||
|
authId: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function Home() {
|
||||||
|
const [temp, setTemp] = useState<AuthResponse>({
|
||||||
|
is2FAEnabled: false,
|
||||||
|
authId: "",
|
||||||
|
});
|
||||||
|
const { mutateAsync, isLoading, isError, error } =
|
||||||
|
api.auth.sendResetPasswordEmail.useMutation();
|
||||||
|
const router = useRouter();
|
||||||
|
const form = useForm<Login>({
|
||||||
|
defaultValues: {
|
||||||
|
email: "",
|
||||||
|
},
|
||||||
|
resolver: zodResolver(loginSchema),
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
form.reset();
|
||||||
|
}, [form, form.reset, form.formState.isSubmitSuccessful]);
|
||||||
|
|
||||||
|
const onSubmit = async (values: Login) => {
|
||||||
|
await mutateAsync({
|
||||||
|
email: values.email,
|
||||||
|
})
|
||||||
|
.then((data) => {
|
||||||
|
toast.success("Email sent", {
|
||||||
|
duration: 2000,
|
||||||
|
});
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
toast.error("Error to send email", {
|
||||||
|
duration: 2000,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
return (
|
||||||
|
<div className="flex h-screen w-full items-center justify-center ">
|
||||||
|
<div className="flex flex-col items-center gap-4 w-full">
|
||||||
|
<Link href="/" className="flex flex-row items-center gap-2">
|
||||||
|
<Logo />
|
||||||
|
<span className="font-medium text-sm">Dokploy</span>
|
||||||
|
</Link>
|
||||||
|
<CardTitle className="text-2xl font-bold">Reset Password</CardTitle>
|
||||||
|
<CardDescription>
|
||||||
|
Enter your email to reset your password
|
||||||
|
</CardDescription>
|
||||||
|
|
||||||
|
<Card className="mx-auto w-full max-w-lg bg-transparent ">
|
||||||
|
<div className="p-3.5" />
|
||||||
|
<CardContent>
|
||||||
|
{isError && (
|
||||||
|
<AlertBlock type="error" className="my-2">
|
||||||
|
{error?.message}
|
||||||
|
</AlertBlock>
|
||||||
|
)}
|
||||||
|
{!temp.is2FAEnabled ? (
|
||||||
|
<Form {...form}>
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
className="grid gap-4"
|
||||||
|
>
|
||||||
|
<div className="space-y-4">
|
||||||
|
<FormField
|
||||||
|
control={form.control}
|
||||||
|
name="email"
|
||||||
|
render={({ field }) => (
|
||||||
|
<FormItem>
|
||||||
|
<FormLabel>Email</FormLabel>
|
||||||
|
<FormControl>
|
||||||
|
<Input placeholder="Email" {...field} />
|
||||||
|
</FormControl>
|
||||||
|
<FormMessage />
|
||||||
|
</FormItem>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
isLoading={isLoading}
|
||||||
|
className="w-full"
|
||||||
|
>
|
||||||
|
Send Reset Link
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</Form>
|
||||||
|
) : (
|
||||||
|
<Login2FA authId={temp.authId} />
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex flex-row justify-between flex-wrap">
|
||||||
|
<div className="mt-4 text-center text-sm flex flex-row justify-center gap-2">
|
||||||
|
<Link
|
||||||
|
className="hover:underline text-muted-foreground"
|
||||||
|
href="/"
|
||||||
|
>
|
||||||
|
Login
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
Home.getLayout = (page: ReactElement) => {
|
||||||
|
return <OnboardingLayout>{page}</OnboardingLayout>;
|
||||||
|
};
|
||||||
|
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||||
|
if (!IS_CLOUD) {
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
permanent: true,
|
||||||
|
destination: "/",
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -15,7 +15,6 @@ const Home: NextPage = () => {
|
|||||||
const [spec, setSpec] = useState({});
|
const [spec, setSpec] = useState({});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Esto solo se ejecutará en el cliente
|
|
||||||
if (data) {
|
if (data) {
|
||||||
const protocolAndHost = `${window.location.protocol}//${window.location.host}/api`;
|
const protocolAndHost = `${window.location.protocol}//${window.location.host}/api`;
|
||||||
const newSpec = {
|
const newSpec = {
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 3.4 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 17 KiB |
@@ -29,6 +29,7 @@ import { securityRouter } from "./routers/security";
|
|||||||
import { serverRouter } from "./routers/server";
|
import { serverRouter } from "./routers/server";
|
||||||
import { settingsRouter } from "./routers/settings";
|
import { settingsRouter } from "./routers/settings";
|
||||||
import { sshRouter } from "./routers/ssh-key";
|
import { sshRouter } from "./routers/ssh-key";
|
||||||
|
import { stripeRouter } from "./routers/stripe";
|
||||||
import { userRouter } from "./routers/user";
|
import { userRouter } from "./routers/user";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -69,6 +70,7 @@ export const appRouter = createTRPCRouter({
|
|||||||
gitlab: gitlabRouter,
|
gitlab: gitlabRouter,
|
||||||
github: githubRouter,
|
github: githubRouter,
|
||||||
server: serverRouter,
|
server: serverRouter,
|
||||||
|
stripe: stripeRouter,
|
||||||
});
|
});
|
||||||
|
|
||||||
// export type definition of API
|
// export type definition of API
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import {
|
|||||||
apiRemoveUser,
|
apiRemoveUser,
|
||||||
users,
|
users,
|
||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
createInvitation,
|
createInvitation,
|
||||||
findAdminById,
|
findAdminById,
|
||||||
|
|||||||
@@ -19,11 +19,8 @@ import {
|
|||||||
apiUpdateApplication,
|
apiUpdateApplication,
|
||||||
applications,
|
applications,
|
||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
import {
|
import type { DeploymentJob } from "@/server/queues/queue-types";
|
||||||
type DeploymentJob,
|
import { cleanQueuesByApplication, myQueue } from "@/server/queues/queueSetup";
|
||||||
cleanQueuesByApplication,
|
|
||||||
} from "@/server/queues/deployments-queue";
|
|
||||||
import { myQueue } from "@/server/queues/queueSetup";
|
|
||||||
import { deploy } from "@/server/utils/deploy";
|
import { deploy } from "@/server/utils/deploy";
|
||||||
import { uploadFileSchema } from "@/utils/schema";
|
import { uploadFileSchema } from "@/utils/schema";
|
||||||
import {
|
import {
|
||||||
|
|||||||
@@ -4,11 +4,13 @@ import {
|
|||||||
apiFindOneAuth,
|
apiFindOneAuth,
|
||||||
apiLogin,
|
apiLogin,
|
||||||
apiUpdateAuth,
|
apiUpdateAuth,
|
||||||
apiUpdateAuthByAdmin,
|
|
||||||
apiVerify2FA,
|
apiVerify2FA,
|
||||||
apiVerifyLogin2FA,
|
apiVerifyLogin2FA,
|
||||||
|
auth,
|
||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
|
import { WEBSITE_URL } from "@/server/utils/stripe";
|
||||||
import {
|
import {
|
||||||
|
type Auth,
|
||||||
IS_CLOUD,
|
IS_CLOUD,
|
||||||
createAdmin,
|
createAdmin,
|
||||||
createUser,
|
createUser,
|
||||||
@@ -18,12 +20,18 @@ import {
|
|||||||
getUserByToken,
|
getUserByToken,
|
||||||
lucia,
|
lucia,
|
||||||
luciaToken,
|
luciaToken,
|
||||||
|
sendDiscordNotification,
|
||||||
|
sendEmailNotification,
|
||||||
updateAuthById,
|
updateAuthById,
|
||||||
validateRequest,
|
validateRequest,
|
||||||
verify2FA,
|
verify2FA,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import * as bcrypt from "bcrypt";
|
import * as bcrypt from "bcrypt";
|
||||||
|
import { isBefore } from "date-fns";
|
||||||
|
import { eq } from "drizzle-orm";
|
||||||
|
import { nanoid } from "nanoid";
|
||||||
|
import { z } from "zod";
|
||||||
import { db } from "../../db";
|
import { db } from "../../db";
|
||||||
import {
|
import {
|
||||||
adminProcedure,
|
adminProcedure,
|
||||||
@@ -47,6 +55,12 @@ export const authRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
const newAdmin = await createAdmin(input);
|
const newAdmin = await createAdmin(input);
|
||||||
|
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
await sendDiscordNotificationWelcome(newAdmin);
|
||||||
|
await sendVerificationEmail(newAdmin.id);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
const session = await lucia.createSession(newAdmin.id || "", {});
|
const session = await lucia.createSession(newAdmin.id || "", {});
|
||||||
ctx.res.appendHeader(
|
ctx.res.appendHeader(
|
||||||
"Set-Cookie",
|
"Set-Cookie",
|
||||||
@@ -54,7 +68,12 @@ export const authRouter = createTRPCRouter({
|
|||||||
);
|
);
|
||||||
return true;
|
return true;
|
||||||
} catch (error) {
|
} 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
|
createUser: publicProcedure
|
||||||
@@ -68,7 +87,13 @@ export const authRouter = createTRPCRouter({
|
|||||||
message: "Invalid token",
|
message: "Invalid token",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
const newUser = await createUser(input);
|
const newUser = await createUser(input);
|
||||||
|
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
await sendVerificationEmail(token.authId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
const session = await lucia.createSession(newUser?.authId || "", {});
|
const session = await lucia.createSession(newUser?.authId || "", {});
|
||||||
ctx.res.appendHeader(
|
ctx.res.appendHeader(
|
||||||
"Set-Cookie",
|
"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) {
|
if (auth?.is2FAEnabled) {
|
||||||
return {
|
return {
|
||||||
is2FAEnabled: true,
|
is2FAEnabled: true,
|
||||||
@@ -120,7 +154,7 @@ export const authRouter = createTRPCRouter({
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
message: "Credentials do not match",
|
message: `Error: ${error instanceof Error ? error.message : "Error to login"}`,
|
||||||
cause: error,
|
cause: error,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -145,7 +179,7 @@ export const authRouter = createTRPCRouter({
|
|||||||
.input(apiUpdateAuth)
|
.input(apiUpdateAuth)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
const auth = await updateAuthById(ctx.user.authId, {
|
const auth = await updateAuthById(ctx.user.authId, {
|
||||||
...(input.email && { email: input.email }),
|
...(input.email && { email: input.email.toLowerCase() }),
|
||||||
...(input.password && {
|
...(input.password && {
|
||||||
password: bcrypt.hashSync(input.password, 10),
|
password: bcrypt.hashSync(input.password, 10),
|
||||||
}),
|
}),
|
||||||
@@ -177,19 +211,6 @@ export const authRouter = createTRPCRouter({
|
|||||||
return auth;
|
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 }) => {
|
generate2FASecret: protectedProcedure.query(async ({ ctx }) => {
|
||||||
return await generate2FASecret(ctx.user.authId);
|
return await generate2FASecret(ctx.user.authId);
|
||||||
}),
|
}),
|
||||||
@@ -230,7 +251,208 @@ export const authRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
return auth;
|
return auth;
|
||||||
}),
|
}),
|
||||||
verifyToken: protectedProcedure.mutation(async () => {
|
sendResetPasswordEmail: publicProcedure
|
||||||
return true;
|
.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",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import {
|
|||||||
findMongoByBackupId,
|
findMongoByBackupId,
|
||||||
findMySqlByBackupId,
|
findMySqlByBackupId,
|
||||||
findPostgresByBackupId,
|
findPostgresByBackupId,
|
||||||
|
findServerById,
|
||||||
removeBackupById,
|
removeBackupById,
|
||||||
removeScheduleBackup,
|
removeScheduleBackup,
|
||||||
runMariadbBackup,
|
runMariadbBackup,
|
||||||
@@ -36,6 +37,25 @@ export const backupRouter = createTRPCRouter({
|
|||||||
const backup = await findBackupById(newBackup.backupId);
|
const backup = await findBackupById(newBackup.backupId);
|
||||||
|
|
||||||
if (IS_CLOUD && backup.enabled) {
|
if (IS_CLOUD && backup.enabled) {
|
||||||
|
const databaseType = backup.databaseType;
|
||||||
|
let serverId = "";
|
||||||
|
if (databaseType === "postgres" && backup.postgres?.serverId) {
|
||||||
|
serverId = backup.postgres.serverId;
|
||||||
|
} else if (databaseType === "mysql" && backup.mysql?.serverId) {
|
||||||
|
serverId = backup.mysql.serverId;
|
||||||
|
} else if (databaseType === "mongo" && backup.mongo?.serverId) {
|
||||||
|
serverId = backup.mongo.serverId;
|
||||||
|
} else if (databaseType === "mariadb" && backup.mariadb?.serverId) {
|
||||||
|
serverId = backup.mariadb.serverId;
|
||||||
|
}
|
||||||
|
const server = await findServerById(serverId);
|
||||||
|
|
||||||
|
if (server.serverStatus === "inactive") {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: "Server is inactive",
|
||||||
|
});
|
||||||
|
}
|
||||||
await schedule({
|
await schedule({
|
||||||
cronSchedule: backup.schedule,
|
cronSchedule: backup.schedule,
|
||||||
backupId: backup.backupId,
|
backupId: backup.backupId,
|
||||||
|
|||||||
@@ -9,11 +9,7 @@ import {
|
|||||||
apiUpdateCompose,
|
apiUpdateCompose,
|
||||||
compose,
|
compose,
|
||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
import {
|
import { cleanQueuesByCompose, myQueue } from "@/server/queues/queueSetup";
|
||||||
type DeploymentJob,
|
|
||||||
cleanQueuesByCompose,
|
|
||||||
} from "@/server/queues/deployments-queue";
|
|
||||||
import { myQueue } from "@/server/queues/queueSetup";
|
|
||||||
import { templates } from "@/templates/templates";
|
import { templates } from "@/templates/templates";
|
||||||
import type { TemplatesKeys } from "@/templates/types/templates-data.type";
|
import type { TemplatesKeys } from "@/templates/types/templates-data.type";
|
||||||
import {
|
import {
|
||||||
@@ -28,6 +24,7 @@ import _ from "lodash";
|
|||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
import { createTRPCRouter, protectedProcedure } from "../trpc";
|
||||||
|
|
||||||
|
import type { DeploymentJob } from "@/server/queues/queue-types";
|
||||||
import { deploy } from "@/server/utils/deploy";
|
import { deploy } from "@/server/utils/deploy";
|
||||||
import {
|
import {
|
||||||
IS_CLOUD,
|
IS_CLOUD,
|
||||||
@@ -41,7 +38,6 @@ import {
|
|||||||
createComposeByTemplate,
|
createComposeByTemplate,
|
||||||
createDomain,
|
createDomain,
|
||||||
createMount,
|
createMount,
|
||||||
findAdmin,
|
|
||||||
findAdminById,
|
findAdminById,
|
||||||
findComposeById,
|
findComposeById,
|
||||||
findDomainsByComposeId,
|
findDomainsByComposeId,
|
||||||
@@ -252,7 +248,6 @@ export const composeRouter = createTRPCRouter({
|
|||||||
descriptionLog: "",
|
descriptionLog: "",
|
||||||
server: !!compose.serverId,
|
server: !!compose.serverId,
|
||||||
};
|
};
|
||||||
console.log(jobData);
|
|
||||||
|
|
||||||
if (IS_CLOUD && compose.serverId) {
|
if (IS_CLOUD && compose.serverId) {
|
||||||
jobData.serverId = compose.serverId;
|
jobData.serverId = compose.serverId;
|
||||||
|
|||||||
@@ -12,9 +12,10 @@ import {
|
|||||||
destinations,
|
destinations,
|
||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
import {
|
import {
|
||||||
|
IS_CLOUD,
|
||||||
createDestintation,
|
createDestintation,
|
||||||
execAsync,
|
execAsync,
|
||||||
findAdmin,
|
execAsyncRemote,
|
||||||
findDestinationById,
|
findDestinationById,
|
||||||
removeDestinationById,
|
removeDestinationById,
|
||||||
updateDestinationById,
|
updateDestinationById,
|
||||||
@@ -53,11 +54,26 @@ export const destinationRouter = createTRPCRouter({
|
|||||||
];
|
];
|
||||||
const rcloneDestination = `:s3:${bucket}`;
|
const rcloneDestination = `:s3:${bucket}`;
|
||||||
const rcloneCommand = `rclone ls ${rcloneFlags.join(" ")} "${rcloneDestination}"`;
|
const rcloneCommand = `rclone ls ${rcloneFlags.join(" ")} "${rcloneDestination}"`;
|
||||||
await execAsync(rcloneCommand);
|
|
||||||
|
if (IS_CLOUD && !input.serverId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: "Server not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
await execAsyncRemote(input.serverId || "", rcloneCommand);
|
||||||
|
} else {
|
||||||
|
await execAsync(rcloneCommand);
|
||||||
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
message: "Error to connect to bucket",
|
message:
|
||||||
|
error instanceof Error
|
||||||
|
? error?.message
|
||||||
|
: "Error to connect to bucket",
|
||||||
cause: error,
|
cause: error,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@ import {
|
|||||||
deployMariadb,
|
deployMariadb,
|
||||||
findMariadbById,
|
findMariadbById,
|
||||||
findProjectById,
|
findProjectById,
|
||||||
|
findServerById,
|
||||||
removeMariadbById,
|
removeMariadbById,
|
||||||
removeService,
|
removeService,
|
||||||
startService,
|
startService,
|
||||||
@@ -151,6 +152,7 @@ export const mariadbRouter = createTRPCRouter({
|
|||||||
message: "You are not authorized to deploy this mariadb",
|
message: "You are not authorized to deploy this mariadb",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return deployMariadb(input.mariadbId);
|
return deployMariadb(input.mariadbId);
|
||||||
}),
|
}),
|
||||||
changeStatus: protectedProcedure
|
changeStatus: protectedProcedure
|
||||||
|
|||||||
@@ -148,12 +148,6 @@ export const notificationRouter = createTRPCRouter({
|
|||||||
.input(apiCreateDiscord)
|
.input(apiCreateDiscord)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
try {
|
try {
|
||||||
// go to your discord server
|
|
||||||
// go to settings
|
|
||||||
// go to integrations
|
|
||||||
// add a new integration
|
|
||||||
// select webhook
|
|
||||||
// copy the webhook url
|
|
||||||
return await createDiscordNotification(input, ctx.user.adminId);
|
return await createDiscordNotification(input, ctx.user.adminId);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
|
|||||||
@@ -20,10 +20,12 @@ import { and, desc, eq, sql } from "drizzle-orm";
|
|||||||
import type { AnyPgColumn } from "drizzle-orm/pg-core";
|
import type { AnyPgColumn } from "drizzle-orm/pg-core";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
|
IS_CLOUD,
|
||||||
addNewProject,
|
addNewProject,
|
||||||
checkProjectAccess,
|
checkProjectAccess,
|
||||||
createProject,
|
createProject,
|
||||||
deleteProject,
|
deleteProject,
|
||||||
|
findAdminById,
|
||||||
findProjectById,
|
findProjectById,
|
||||||
findUserByAuthId,
|
findUserByAuthId,
|
||||||
updateProjectById,
|
updateProjectById,
|
||||||
@@ -38,6 +40,15 @@ export const projectRouter = createTRPCRouter({
|
|||||||
await checkProjectAccess(ctx.user.authId, "create");
|
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);
|
const project = await createProject(input, ctx.user.adminId);
|
||||||
if (ctx.user.rol === "user") {
|
if (ctx.user.rol === "user") {
|
||||||
await addNewProject(ctx.user.authId, project.projectId);
|
await addNewProject(ctx.user.authId, project.projectId);
|
||||||
@@ -47,7 +58,7 @@ export const projectRouter = createTRPCRouter({
|
|||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw new TRPCError({
|
throw new TRPCError({
|
||||||
code: "BAD_REQUEST",
|
code: "BAD_REQUEST",
|
||||||
message: "Error to create the project",
|
message: `Error to create the project: ${error instanceof Error ? error.message : error}`,
|
||||||
cause: error,
|
cause: error,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -128,9 +139,9 @@ export const projectRouter = createTRPCRouter({
|
|||||||
)})`,
|
)})`,
|
||||||
with: {
|
with: {
|
||||||
applications: {
|
applications: {
|
||||||
where: buildServiceFilter(
|
where: and(
|
||||||
applications.applicationId,
|
buildServiceFilter(applications.applicationId, accesedServices),
|
||||||
accesedServices,
|
eq(projects.adminId, ctx.user.adminId),
|
||||||
),
|
),
|
||||||
with: { domains: true },
|
with: { domains: true },
|
||||||
},
|
},
|
||||||
@@ -230,5 +241,5 @@ function buildServiceFilter(fieldName: AnyPgColumn, accesedServices: string[]) {
|
|||||||
accesedServices.map((serviceId) => sql`${serviceId}`),
|
accesedServices.map((serviceId) => sql`${serviceId}`),
|
||||||
sql`, `,
|
sql`, `,
|
||||||
)})`
|
)})`
|
||||||
: sql`1 = 0`; // Always false condition
|
: sql`1 = 0`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import {
|
import {
|
||||||
apiCreateRegistry,
|
apiCreateRegistry,
|
||||||
apiEnableSelfHostedRegistry,
|
|
||||||
apiFindOneRegistry,
|
apiFindOneRegistry,
|
||||||
apiRemoveRegistry,
|
apiRemoveRegistry,
|
||||||
apiTestRegistry,
|
apiTestRegistry,
|
||||||
@@ -13,8 +12,6 @@ import {
|
|||||||
execAsyncRemote,
|
execAsyncRemote,
|
||||||
findAllRegistryByAdminId,
|
findAllRegistryByAdminId,
|
||||||
findRegistryById,
|
findRegistryById,
|
||||||
initializeRegistry,
|
|
||||||
manageRegistry,
|
|
||||||
removeRegistry,
|
removeRegistry,
|
||||||
updateRegistry,
|
updateRegistry,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
@@ -84,6 +81,13 @@ export const registryRouter = createTRPCRouter({
|
|||||||
try {
|
try {
|
||||||
const loginCommand = `echo ${input.password} | docker login ${input.registryUrl} --username ${input.username} --password-stdin`;
|
const loginCommand = `echo ${input.password} | docker login ${input.registryUrl} --username ${input.username} --password-stdin`;
|
||||||
|
|
||||||
|
if (IS_CLOUD && !input.serverId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: "Select a server to test the registry",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
if (input.serverId && input.serverId !== "none") {
|
if (input.serverId && input.serverId !== "none") {
|
||||||
await execAsyncRemote(input.serverId, loginCommand);
|
await execAsyncRemote(input.serverId, loginCommand);
|
||||||
} else {
|
} else {
|
||||||
@@ -96,34 +100,4 @@ export const registryRouter = createTRPCRouter({
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|
||||||
enableSelfHostedRegistry: adminProcedure
|
|
||||||
.input(apiEnableSelfHostedRegistry)
|
|
||||||
.mutation(async ({ input, ctx }) => {
|
|
||||||
if (IS_CLOUD) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "Self Hosted Registry is not available in the cloud version",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const selfHostedRegistry = await createRegistry(
|
|
||||||
{
|
|
||||||
...input,
|
|
||||||
registryName: "Self Hosted Registry",
|
|
||||||
registryType: "selfHosted",
|
|
||||||
registryUrl:
|
|
||||||
process.env.NODE_ENV === "production"
|
|
||||||
? input.registryUrl
|
|
||||||
: "dokploy-registry.docker.localhost",
|
|
||||||
imagePrefix: null,
|
|
||||||
serverId: undefined,
|
|
||||||
},
|
|
||||||
ctx.user.adminId,
|
|
||||||
);
|
|
||||||
|
|
||||||
await manageRegistry(selfHostedRegistry);
|
|
||||||
await initializeRegistry(input.username, input.password);
|
|
||||||
|
|
||||||
return selfHostedRegistry;
|
|
||||||
}),
|
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
import { updateServersBasedOnQuantity } from "@/pages/api/stripe/webhook";
|
||||||
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
import { createTRPCRouter, protectedProcedure } from "@/server/api/trpc";
|
||||||
import { db } from "@/server/db";
|
import { db } from "@/server/db";
|
||||||
import {
|
import {
|
||||||
@@ -15,15 +16,17 @@ import {
|
|||||||
server,
|
server,
|
||||||
} from "@/server/db/schema";
|
} from "@/server/db/schema";
|
||||||
import {
|
import {
|
||||||
|
IS_CLOUD,
|
||||||
createServer,
|
createServer,
|
||||||
deleteServer,
|
deleteServer,
|
||||||
|
findAdminById,
|
||||||
findServerById,
|
findServerById,
|
||||||
|
findServersByAdminId,
|
||||||
haveActiveServices,
|
haveActiveServices,
|
||||||
removeDeploymentsByServerId,
|
removeDeploymentsByServerId,
|
||||||
serverSetup,
|
serverSetup,
|
||||||
updateServerById,
|
updateServerById,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
// import { serverSetup } from "@/server/setup/server-setup";
|
|
||||||
import { TRPCError } from "@trpc/server";
|
import { TRPCError } from "@trpc/server";
|
||||||
import { and, desc, eq, getTableColumns, isNotNull, sql } from "drizzle-orm";
|
import { and, desc, eq, getTableColumns, isNotNull, sql } from "drizzle-orm";
|
||||||
|
|
||||||
@@ -32,6 +35,14 @@ export const serverRouter = createTRPCRouter({
|
|||||||
.input(apiCreateServer)
|
.input(apiCreateServer)
|
||||||
.mutation(async ({ ctx, input }) => {
|
.mutation(async ({ ctx, input }) => {
|
||||||
try {
|
try {
|
||||||
|
const admin = await findAdminById(ctx.user.adminId);
|
||||||
|
const servers = await findServersByAdminId(admin.adminId);
|
||||||
|
if (IS_CLOUD && servers.length >= admin.serversQuantity) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "You cannot create more servers",
|
||||||
|
});
|
||||||
|
}
|
||||||
const project = await createServer(input, ctx.user.adminId);
|
const project = await createServer(input, ctx.user.adminId);
|
||||||
return project;
|
return project;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -77,13 +88,17 @@ export const serverRouter = createTRPCRouter({
|
|||||||
return result;
|
return result;
|
||||||
}),
|
}),
|
||||||
withSSHKey: protectedProcedure.query(async ({ ctx }) => {
|
withSSHKey: protectedProcedure.query(async ({ ctx }) => {
|
||||||
return await db.query.server.findMany({
|
const result = await db.query.server.findMany({
|
||||||
orderBy: desc(server.createdAt),
|
orderBy: desc(server.createdAt),
|
||||||
where: and(
|
where: IS_CLOUD
|
||||||
isNotNull(server.sshKeyId),
|
? and(
|
||||||
eq(server.adminId, ctx.user.adminId),
|
isNotNull(server.sshKeyId),
|
||||||
),
|
eq(server.adminId, ctx.user.adminId),
|
||||||
|
eq(server.serverStatus, "active"),
|
||||||
|
)
|
||||||
|
: and(isNotNull(server.sshKeyId), eq(server.adminId, ctx.user.adminId)),
|
||||||
});
|
});
|
||||||
|
return result;
|
||||||
}),
|
}),
|
||||||
setup: protectedProcedure
|
setup: protectedProcedure
|
||||||
.input(apiFindOneServer)
|
.input(apiFindOneServer)
|
||||||
@@ -125,6 +140,15 @@ export const serverRouter = createTRPCRouter({
|
|||||||
await removeDeploymentsByServerId(currentServer);
|
await removeDeploymentsByServerId(currentServer);
|
||||||
await deleteServer(input.serverId);
|
await deleteServer(input.serverId);
|
||||||
|
|
||||||
|
if (IS_CLOUD) {
|
||||||
|
const admin = await findAdminById(ctx.user.adminId);
|
||||||
|
|
||||||
|
await updateServersBasedOnQuantity(
|
||||||
|
admin.adminId,
|
||||||
|
admin.serversQuantity,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
return currentServer;
|
return currentServer;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
throw error;
|
throw error;
|
||||||
@@ -141,6 +165,13 @@ export const serverRouter = createTRPCRouter({
|
|||||||
message: "You are not authorized to update this server",
|
message: "You are not authorized to update this server",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (server.serverStatus === "inactive") {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: "Server is inactive",
|
||||||
|
});
|
||||||
|
}
|
||||||
const currentServer = await updateServerById(input.serverId, {
|
const currentServer = await updateServerById(input.serverId, {
|
||||||
...input,
|
...input,
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -221,6 +221,13 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (server.enableDockerCleanup) {
|
if (server.enableDockerCleanup) {
|
||||||
|
const server = await findServerById(input.serverId);
|
||||||
|
if (server.serverStatus === "inactive") {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "NOT_FOUND",
|
||||||
|
message: "Server is inactive",
|
||||||
|
});
|
||||||
|
}
|
||||||
if (IS_CLOUD) {
|
if (IS_CLOUD) {
|
||||||
await schedule({
|
await schedule({
|
||||||
cronSchedule: "0 0 * * *",
|
cronSchedule: "0 0 * * *",
|
||||||
@@ -503,7 +510,7 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
if (input?.serverId) {
|
if (input?.serverId) {
|
||||||
const result = await execAsyncRemote(input.serverId, command);
|
const result = await execAsyncRemote(input.serverId, command);
|
||||||
stdout = result.stdout;
|
stdout = result.stdout;
|
||||||
} else {
|
} else if (!IS_CLOUD) {
|
||||||
const result = await execAsync(
|
const result = await execAsync(
|
||||||
"docker service inspect --format='{{json .Endpoint.Ports}}' dokploy-traefik",
|
"docker service inspect --format='{{json .Endpoint.Ports}}' dokploy-traefik",
|
||||||
);
|
);
|
||||||
@@ -635,7 +642,7 @@ export const settingsRouter = createTRPCRouter({
|
|||||||
|
|
||||||
return true;
|
return true;
|
||||||
}),
|
}),
|
||||||
isCloud: adminProcedure.query(async () => {
|
isCloud: protectedProcedure.query(async () => {
|
||||||
return IS_CLOUD;
|
return IS_CLOUD;
|
||||||
}),
|
}),
|
||||||
health: publicProcedure.query(async () => {
|
health: publicProcedure.query(async () => {
|
||||||
|
|||||||
130
apps/dokploy/server/api/routers/stripe.ts
Normal file
130
apps/dokploy/server/api/routers/stripe.ts
Normal file
@@ -0,0 +1,130 @@
|
|||||||
|
import { WEBSITE_URL, getStripeItems } from "@/server/utils/stripe";
|
||||||
|
import {
|
||||||
|
IS_CLOUD,
|
||||||
|
findAdminById,
|
||||||
|
findServersByAdminId,
|
||||||
|
updateAdmin,
|
||||||
|
} from "@dokploy/server";
|
||||||
|
import { TRPCError } from "@trpc/server";
|
||||||
|
import Stripe from "stripe";
|
||||||
|
import { z } from "zod";
|
||||||
|
import { adminProcedure, createTRPCRouter } from "../trpc";
|
||||||
|
|
||||||
|
export const stripeRouter = createTRPCRouter({
|
||||||
|
getProducts: adminProcedure.query(async ({ ctx }) => {
|
||||||
|
const admin = await findAdminById(ctx.user.adminId);
|
||||||
|
const stripeCustomerId = admin.stripeCustomerId;
|
||||||
|
|
||||||
|
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || "", {
|
||||||
|
apiVersion: "2024-09-30.acacia",
|
||||||
|
});
|
||||||
|
|
||||||
|
const products = await stripe.products.list({
|
||||||
|
expand: ["data.default_price"],
|
||||||
|
active: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!stripeCustomerId) {
|
||||||
|
return {
|
||||||
|
products: products.data,
|
||||||
|
subscriptions: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const subscriptions = await stripe.subscriptions.list({
|
||||||
|
customer: stripeCustomerId,
|
||||||
|
status: "active",
|
||||||
|
expand: ["data.items.data.price"],
|
||||||
|
});
|
||||||
|
|
||||||
|
return {
|
||||||
|
products: products.data,
|
||||||
|
subscriptions: subscriptions.data,
|
||||||
|
};
|
||||||
|
}),
|
||||||
|
createCheckoutSession: adminProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
productId: z.string(),
|
||||||
|
serverQuantity: z.number().min(1),
|
||||||
|
isAnnual: z.boolean(),
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
.mutation(async ({ ctx, input }) => {
|
||||||
|
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || "", {
|
||||||
|
apiVersion: "2024-09-30.acacia",
|
||||||
|
});
|
||||||
|
|
||||||
|
const items = getStripeItems(input.serverQuantity, input.isAnnual);
|
||||||
|
const admin = await findAdminById(ctx.user.adminId);
|
||||||
|
|
||||||
|
let stripeCustomerId = admin.stripeCustomerId;
|
||||||
|
|
||||||
|
if (stripeCustomerId) {
|
||||||
|
const customer = await stripe.customers.retrieve(stripeCustomerId);
|
||||||
|
|
||||||
|
if (customer.deleted) {
|
||||||
|
await updateAdmin(admin.authId, {
|
||||||
|
stripeCustomerId: null,
|
||||||
|
});
|
||||||
|
stripeCustomerId = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const session = await stripe.checkout.sessions.create({
|
||||||
|
mode: "subscription",
|
||||||
|
line_items: items,
|
||||||
|
...(stripeCustomerId && {
|
||||||
|
customer: stripeCustomerId,
|
||||||
|
}),
|
||||||
|
metadata: {
|
||||||
|
adminId: admin.adminId,
|
||||||
|
},
|
||||||
|
success_url: `${WEBSITE_URL}/dashboard/settings/billing`,
|
||||||
|
cancel_url: `${WEBSITE_URL}/dashboard/settings/billing`,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { sessionId: session.id };
|
||||||
|
}),
|
||||||
|
createCustomerPortalSession: adminProcedure.mutation(
|
||||||
|
async ({ ctx, input }) => {
|
||||||
|
const admin = await findAdminById(ctx.user.adminId);
|
||||||
|
|
||||||
|
if (!admin.stripeCustomerId) {
|
||||||
|
throw new TRPCError({
|
||||||
|
code: "BAD_REQUEST",
|
||||||
|
message: "Stripe Customer ID not found",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const stripeCustomerId = admin.stripeCustomerId;
|
||||||
|
|
||||||
|
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY || "", {
|
||||||
|
apiVersion: "2024-09-30.acacia",
|
||||||
|
});
|
||||||
|
|
||||||
|
try {
|
||||||
|
const session = await stripe.billingPortal.sessions.create({
|
||||||
|
customer: stripeCustomerId,
|
||||||
|
return_url: `${WEBSITE_URL}/dashboard/settings/billing`,
|
||||||
|
});
|
||||||
|
|
||||||
|
return { url: session.url };
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
url: "",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
},
|
||||||
|
),
|
||||||
|
|
||||||
|
canCreateMoreServers: adminProcedure.query(async ({ ctx }) => {
|
||||||
|
const admin = await findAdminById(ctx.user.adminId);
|
||||||
|
const servers = await findServersByAdminId(admin.adminId);
|
||||||
|
|
||||||
|
if (!IS_CLOUD) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return servers.length < admin.serversQuantity;
|
||||||
|
}),
|
||||||
|
});
|
||||||
@@ -1 +1 @@
|
|||||||
export * from "@dokploy/server/dist/db/schema";
|
export * from "@dokploy/server/db/schema";
|
||||||
|
|||||||
@@ -11,29 +11,8 @@ import {
|
|||||||
updateCompose,
|
updateCompose,
|
||||||
} from "@dokploy/server";
|
} from "@dokploy/server";
|
||||||
import { type Job, Worker } from "bullmq";
|
import { type Job, Worker } from "bullmq";
|
||||||
import { myQueue, redisConfig } from "./queueSetup";
|
import type { DeploymentJob } from "./queue-types";
|
||||||
|
import { redisConfig } from "./redis-connection";
|
||||||
type DeployJob =
|
|
||||||
| {
|
|
||||||
applicationId: string;
|
|
||||||
titleLog: string;
|
|
||||||
descriptionLog: string;
|
|
||||||
server?: boolean;
|
|
||||||
type: "deploy" | "redeploy";
|
|
||||||
applicationType: "application";
|
|
||||||
serverId?: string;
|
|
||||||
}
|
|
||||||
| {
|
|
||||||
composeId: string;
|
|
||||||
titleLog: string;
|
|
||||||
descriptionLog: string;
|
|
||||||
server?: boolean;
|
|
||||||
type: "deploy" | "redeploy";
|
|
||||||
applicationType: "compose";
|
|
||||||
serverId?: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export type DeploymentJob = DeployJob;
|
|
||||||
|
|
||||||
export const deploymentWorker = new Worker(
|
export const deploymentWorker = new Worker(
|
||||||
"deployments",
|
"deployments",
|
||||||
@@ -114,25 +93,3 @@ export const deploymentWorker = new Worker(
|
|||||||
connection: redisConfig,
|
connection: redisConfig,
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
export const cleanQueuesByApplication = async (applicationId: string) => {
|
|
||||||
const jobs = await myQueue.getJobs(["waiting", "delayed"]);
|
|
||||||
|
|
||||||
for (const job of jobs) {
|
|
||||||
if (job?.data?.applicationId === applicationId) {
|
|
||||||
await job.remove();
|
|
||||||
console.log(`Removed job ${job.id} for application ${applicationId}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export const cleanQueuesByCompose = async (composeId: string) => {
|
|
||||||
const jobs = await myQueue.getJobs(["waiting", "delayed"]);
|
|
||||||
|
|
||||||
for (const job of jobs) {
|
|
||||||
if (job?.data?.composeId === composeId) {
|
|
||||||
await job.remove();
|
|
||||||
console.log(`Removed job ${job.id} for compose ${composeId}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|||||||
21
apps/dokploy/server/queues/queue-types.ts
Normal file
21
apps/dokploy/server/queues/queue-types.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
type DeployJob =
|
||||||
|
| {
|
||||||
|
applicationId: string;
|
||||||
|
titleLog: string;
|
||||||
|
descriptionLog: string;
|
||||||
|
server?: boolean;
|
||||||
|
type: "deploy" | "redeploy";
|
||||||
|
applicationType: "application";
|
||||||
|
serverId?: string;
|
||||||
|
}
|
||||||
|
| {
|
||||||
|
composeId: string;
|
||||||
|
titleLog: string;
|
||||||
|
descriptionLog: string;
|
||||||
|
server?: boolean;
|
||||||
|
type: "deploy" | "redeploy";
|
||||||
|
applicationType: "compose";
|
||||||
|
serverId?: string;
|
||||||
|
};
|
||||||
|
|
||||||
|
export type DeploymentJob = DeployJob;
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
import { type ConnectionOptions, Queue } from "bullmq";
|
import { Queue } from "bullmq";
|
||||||
|
import { redisConfig } from "./redis-connection";
|
||||||
|
|
||||||
export const redisConfig: ConnectionOptions = {
|
|
||||||
host: process.env.NODE_ENV === "production" ? "dokploy-redis" : "127.0.0.1",
|
|
||||||
};
|
|
||||||
const myQueue = new Queue("deployments", {
|
const myQueue = new Queue("deployments", {
|
||||||
connection: redisConfig,
|
connection: redisConfig,
|
||||||
});
|
});
|
||||||
@@ -21,4 +19,26 @@ myQueue.on("error", (error) => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const cleanQueuesByApplication = async (applicationId: string) => {
|
||||||
|
const jobs = await myQueue.getJobs(["waiting", "delayed"]);
|
||||||
|
|
||||||
|
for (const job of jobs) {
|
||||||
|
if (job?.data?.applicationId === applicationId) {
|
||||||
|
await job.remove();
|
||||||
|
console.log(`Removed job ${job.id} for application ${applicationId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
export const cleanQueuesByCompose = async (composeId: string) => {
|
||||||
|
const jobs = await myQueue.getJobs(["waiting", "delayed"]);
|
||||||
|
|
||||||
|
for (const job of jobs) {
|
||||||
|
if (job?.data?.composeId === composeId) {
|
||||||
|
await job.remove();
|
||||||
|
console.log(`Removed job ${job.id} for compose ${composeId}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
export { myQueue };
|
export { myQueue };
|
||||||
|
|||||||
5
apps/dokploy/server/queues/redis-connection.ts
Normal file
5
apps/dokploy/server/queues/redis-connection.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import type { ConnectionOptions } from "bullmq";
|
||||||
|
|
||||||
|
export const redisConfig: ConnectionOptions = {
|
||||||
|
host: process.env.NODE_ENV === "production" ? "dokploy-redis" : "127.0.0.1",
|
||||||
|
};
|
||||||
@@ -19,15 +19,12 @@ import { setupDockerContainerLogsWebSocketServer } from "./wss/docker-container-
|
|||||||
import { setupDockerContainerTerminalWebSocketServer } from "./wss/docker-container-terminal";
|
import { setupDockerContainerTerminalWebSocketServer } from "./wss/docker-container-terminal";
|
||||||
import { setupDockerStatsMonitoringSocketServer } from "./wss/docker-stats";
|
import { setupDockerStatsMonitoringSocketServer } from "./wss/docker-stats";
|
||||||
import { setupDeploymentLogsWebSocketServer } from "./wss/listen-deployment";
|
import { setupDeploymentLogsWebSocketServer } from "./wss/listen-deployment";
|
||||||
import {
|
import { setupTerminalWebSocketServer } from "./wss/terminal";
|
||||||
getPublicIpWithFallback,
|
|
||||||
setupTerminalWebSocketServer,
|
|
||||||
} from "./wss/terminal";
|
|
||||||
|
|
||||||
config({ path: ".env" });
|
config({ path: ".env" });
|
||||||
const PORT = Number.parseInt(process.env.PORT || "3000", 10);
|
const PORT = Number.parseInt(process.env.PORT || "3000", 10);
|
||||||
const dev = process.env.NODE_ENV !== "production";
|
const dev = process.env.NODE_ENV !== "production";
|
||||||
const app = next({ dev });
|
const app = next({ dev, turbopack: dev });
|
||||||
const handle = app.getRequestHandler();
|
const handle = app.getRequestHandler();
|
||||||
void app.prepare().then(async () => {
|
void app.prepare().then(async () => {
|
||||||
try {
|
try {
|
||||||
@@ -40,7 +37,9 @@ void app.prepare().then(async () => {
|
|||||||
setupDockerContainerLogsWebSocketServer(server);
|
setupDockerContainerLogsWebSocketServer(server);
|
||||||
setupDockerContainerTerminalWebSocketServer(server);
|
setupDockerContainerTerminalWebSocketServer(server);
|
||||||
setupTerminalWebSocketServer(server);
|
setupTerminalWebSocketServer(server);
|
||||||
setupDockerStatsMonitoringSocketServer(server);
|
if (!IS_CLOUD) {
|
||||||
|
setupDockerStatsMonitoringSocketServer(server);
|
||||||
|
}
|
||||||
|
|
||||||
if (process.env.NODE_ENV === "production" && !IS_CLOUD) {
|
if (process.env.NODE_ENV === "production" && !IS_CLOUD) {
|
||||||
setupDirectories();
|
setupDirectories();
|
||||||
@@ -53,7 +52,6 @@ void app.prepare().then(async () => {
|
|||||||
await initializeRedis();
|
await initializeRedis();
|
||||||
|
|
||||||
initCronJobs();
|
initCronJobs();
|
||||||
welcomeServer();
|
|
||||||
|
|
||||||
// Timeout to wait for the database to be ready
|
// Timeout to wait for the database to be ready
|
||||||
await new Promise((resolve) => setTimeout(resolve, 7000));
|
await new Promise((resolve) => setTimeout(resolve, 7000));
|
||||||
@@ -76,18 +74,3 @@ void app.prepare().then(async () => {
|
|||||||
console.error("Main Server Error", e);
|
console.error("Main Server Error", e);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
async function welcomeServer() {
|
|
||||||
const ip = await getPublicIpWithFallback();
|
|
||||||
console.log(
|
|
||||||
[
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
"Dokploy server is up and running!",
|
|
||||||
"Please wait for 15 seconds before opening the browser.",
|
|
||||||
` http://${ip}:${PORT}`,
|
|
||||||
"",
|
|
||||||
"",
|
|
||||||
].join("\n"),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import type { DeploymentJob } from "../queues/deployments-queue";
|
import { findServerById } from "@dokploy/server";
|
||||||
|
import type { DeploymentJob } from "../queues/queue-types";
|
||||||
|
|
||||||
export const deploy = async (jobData: DeploymentJob) => {
|
export const deploy = async (jobData: DeploymentJob) => {
|
||||||
try {
|
try {
|
||||||
|
const server = await findServerById(jobData.serverId as string);
|
||||||
|
if (server.serverStatus === "inactive") {
|
||||||
|
throw new Error("Server is inactive");
|
||||||
|
}
|
||||||
const result = await fetch(`${process.env.SERVER_URL}/deploy`, {
|
const result = await fetch(`${process.env.SERVER_URL}/deploy`, {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
27
apps/dokploy/server/utils/stripe.ts
Normal file
27
apps/dokploy/server/utils/stripe.ts
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
export const WEBSITE_URL =
|
||||||
|
process.env.NODE_ENV === "development"
|
||||||
|
? "http://localhost:3000"
|
||||||
|
: 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;
|
||||||
|
};
|
||||||
@@ -3,15 +3,15 @@ import {
|
|||||||
createDefaultServerTraefikConfig,
|
createDefaultServerTraefikConfig,
|
||||||
createDefaultTraefikConfig,
|
createDefaultTraefikConfig,
|
||||||
initializeTraefik,
|
initializeTraefik,
|
||||||
} from "@dokploy/server/dist/setup/traefik-setup";
|
} from "@dokploy/server/setup/traefik-setup";
|
||||||
|
|
||||||
import { setupDirectories } from "@dokploy/server/dist/setup/config-paths";
|
import { setupDirectories } from "@dokploy/server/setup/config-paths";
|
||||||
import { initializePostgres } from "@dokploy/server/dist/setup/postgres-setup";
|
import { initializePostgres } from "@dokploy/server/setup/postgres-setup";
|
||||||
import { initializeRedis } from "@dokploy/server/dist/setup/redis-setup";
|
import { initializeRedis } from "@dokploy/server/setup/redis-setup";
|
||||||
import {
|
import {
|
||||||
initializeNetwork,
|
initializeNetwork,
|
||||||
initializeSwarm,
|
initializeSwarm,
|
||||||
} from "@dokploy/server/dist/setup/setup";
|
} from "@dokploy/server/setup/setup";
|
||||||
(async () => {
|
(async () => {
|
||||||
try {
|
try {
|
||||||
setupDirectories();
|
setupDirectories();
|
||||||
|
|||||||
@@ -26,7 +26,8 @@
|
|||||||
/* Path Aliases */
|
/* Path Aliases */
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./*"]
|
"@/*": ["./*"],
|
||||||
|
"@dokploy/server/*": ["../../packages/server/src/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
|||||||
@@ -9,7 +9,8 @@
|
|||||||
"moduleResolution": "Node",
|
"moduleResolution": "Node",
|
||||||
"baseUrl": ".",
|
"baseUrl": ".",
|
||||||
"paths": {
|
"paths": {
|
||||||
"@/*": ["./*"]
|
"@/*": ["./*"],
|
||||||
|
"@dokploy/server/*": ["../../packages/server/src/*"]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "./server/**/*"]
|
"include": ["next-env.d.ts", "./server/**/*"]
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import {
|
|||||||
cleanUpSystemPrune,
|
cleanUpSystemPrune,
|
||||||
cleanUpUnusedImages,
|
cleanUpUnusedImages,
|
||||||
findBackupById,
|
findBackupById,
|
||||||
|
findServerById,
|
||||||
runMariadbBackup,
|
runMariadbBackup,
|
||||||
runMongoBackup,
|
runMongoBackup,
|
||||||
runMySqlBackup,
|
runMySqlBackup,
|
||||||
@@ -21,22 +22,47 @@ export const runJobs = async (job: QueueJob) => {
|
|||||||
const { backupId } = job;
|
const { backupId } = job;
|
||||||
const backup = await findBackupById(backupId);
|
const backup = await findBackupById(backupId);
|
||||||
const { databaseType, postgres, mysql, mongo, mariadb } = backup;
|
const { databaseType, postgres, mysql, mongo, mariadb } = backup;
|
||||||
|
|
||||||
if (databaseType === "postgres" && postgres) {
|
if (databaseType === "postgres" && postgres) {
|
||||||
|
const server = await findServerById(postgres.serverId as string);
|
||||||
|
if (server.serverStatus === "inactive") {
|
||||||
|
logger.info("Server is inactive");
|
||||||
|
return;
|
||||||
|
}
|
||||||
await runPostgresBackup(postgres, backup);
|
await runPostgresBackup(postgres, backup);
|
||||||
} else if (databaseType === "mysql" && mysql) {
|
} else if (databaseType === "mysql" && mysql) {
|
||||||
|
const server = await findServerById(mysql.serverId as string);
|
||||||
|
if (server.serverStatus === "inactive") {
|
||||||
|
logger.info("Server is inactive");
|
||||||
|
return;
|
||||||
|
}
|
||||||
await runMySqlBackup(mysql, backup);
|
await runMySqlBackup(mysql, backup);
|
||||||
} else if (databaseType === "mongo" && mongo) {
|
} else if (databaseType === "mongo" && mongo) {
|
||||||
|
const server = await findServerById(mongo.serverId as string);
|
||||||
|
if (server.serverStatus === "inactive") {
|
||||||
|
logger.info("Server is inactive");
|
||||||
|
return;
|
||||||
|
}
|
||||||
await runMongoBackup(mongo, backup);
|
await runMongoBackup(mongo, backup);
|
||||||
} else if (databaseType === "mariadb" && mariadb) {
|
} else if (databaseType === "mariadb" && mariadb) {
|
||||||
|
const server = await findServerById(mariadb.serverId as string);
|
||||||
|
if (server.serverStatus === "inactive") {
|
||||||
|
logger.info("Server is inactive");
|
||||||
|
return;
|
||||||
|
}
|
||||||
await runMariadbBackup(mariadb, backup);
|
await runMariadbBackup(mariadb, backup);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (job.type === "server") {
|
if (job.type === "server") {
|
||||||
const { serverId } = job;
|
const { serverId } = job;
|
||||||
|
const server = await findServerById(serverId);
|
||||||
|
if (server.serverStatus === "inactive") {
|
||||||
|
logger.info("Server is inactive");
|
||||||
|
return;
|
||||||
|
}
|
||||||
await cleanUpUnusedImages(serverId);
|
await cleanUpUnusedImages(serverId);
|
||||||
await cleanUpDockerBuilder(serverId);
|
await cleanUpDockerBuilder(serverId);
|
||||||
await cleanUpSystemPrune(serverId);
|
await cleanUpSystemPrune(serverId);
|
||||||
// await sendDockerCleanupNotifications();
|
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(error);
|
logger.error(error);
|
||||||
|
|||||||
@@ -1,8 +1,6 @@
|
|||||||
import clsx from "clsx";
|
import clsx from "clsx";
|
||||||
import { Inter, Lexend } from "next/font/google";
|
import { Inter, Lexend } from "next/font/google";
|
||||||
import "@/styles/tailwind.css";
|
import "@/styles/tailwind.css";
|
||||||
import GoogleAnalytics from "@/components/analitycs/google";
|
|
||||||
|
|
||||||
import { NextIntlClientProvider } from "next-intl";
|
import { NextIntlClientProvider } from "next-intl";
|
||||||
import { getMessages } from "next-intl/server";
|
import { getMessages } from "next-intl/server";
|
||||||
|
|
||||||
@@ -88,7 +86,14 @@ export default async function RootLayout({
|
|||||||
lang={locale}
|
lang={locale}
|
||||||
className={clsx("h-full scroll-smooth", inter.variable, lexend.variable)}
|
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">
|
<body className="flex h-full flex-col">
|
||||||
<NextIntlClientProvider messages={messages}>
|
<NextIntlClientProvider messages={messages}>
|
||||||
<Header />
|
<Header />
|
||||||
|
|||||||
@@ -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 { Container } from "./Container";
|
||||||
import { NavLink } from "./NavLink";
|
import { NavLink } from "./NavLink";
|
||||||
import { Logo } from "./shared/Logo";
|
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() {
|
export function Footer() {
|
||||||
|
const router = useRouter();
|
||||||
|
const locale = useLocale();
|
||||||
const t = useTranslations("HomePage");
|
const t = useTranslations("HomePage");
|
||||||
const linkT = useTranslations("Link");
|
const linkT = useTranslations("Link");
|
||||||
|
|
||||||
@@ -31,7 +60,7 @@ export function Footer() {
|
|||||||
</nav>
|
</nav>
|
||||||
</div>
|
</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 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
|
<Link
|
||||||
href="https://x.com/getdokploy"
|
href="https://x.com/getdokploy"
|
||||||
className="group"
|
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" />
|
<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>
|
</svg>
|
||||||
</Link>
|
</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>
|
</div>
|
||||||
<p className="mt-6 text-sm text-muted-foreground sm:mt-0">
|
<p className="mt-6 text-sm text-muted-foreground sm:mt-0">
|
||||||
{t("footer.copyright", {
|
{t("footer.copyright", {
|
||||||
|
|||||||
@@ -1,16 +1,10 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import {
|
import { Link } from "@/i18n/routing";
|
||||||
Select,
|
|
||||||
SelectContent,
|
|
||||||
SelectItem,
|
|
||||||
SelectTrigger,
|
|
||||||
} from "@/components/ui/select";
|
|
||||||
import { Link, useRouter } from "@/i18n/routing";
|
|
||||||
import { cn } from "@/lib/utils";
|
import { cn } from "@/lib/utils";
|
||||||
import { Popover, Transition } from "@headlessui/react";
|
import { Popover, Transition } from "@headlessui/react";
|
||||||
import { HeartIcon } from "lucide-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 { Fragment, type JSX, type SVGProps } from "react";
|
||||||
import { Container } from "./Container";
|
import { Container } from "./Container";
|
||||||
import { NavLink } from "./NavLink";
|
import { NavLink } from "./NavLink";
|
||||||
@@ -125,10 +119,7 @@ function MobileNavigation() {
|
|||||||
as="div"
|
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"
|
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">
|
<MobileNavLink href="/pricing">Pricing</MobileNavLink>
|
||||||
{t("navigation.features")}
|
|
||||||
</MobileNavLink>
|
|
||||||
{/* <MobileNavLink href="/#testimonials">Testimonials</MobileNavLink> */}
|
|
||||||
<MobileNavLink href="/#faqs">{t("navigation.faqs")}</MobileNavLink>
|
<MobileNavLink href="/#faqs">{t("navigation.faqs")}</MobileNavLink>
|
||||||
<MobileNavLink href={linkT("docs.intro")} target="_blank">
|
<MobileNavLink href={linkT("docs.intro")} target="_blank">
|
||||||
{t("navigation.docs")}
|
{t("navigation.docs")}
|
||||||
@@ -141,8 +132,6 @@ function MobileNavigation() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function Header() {
|
export function Header() {
|
||||||
const router = useRouter();
|
|
||||||
const locale = useLocale();
|
|
||||||
const t = useTranslations("HomePage");
|
const t = useTranslations("HomePage");
|
||||||
const linkT = useTranslations("Link");
|
const linkT = useTranslations("Link");
|
||||||
|
|
||||||
@@ -155,8 +144,7 @@ export function Header() {
|
|||||||
<Logo className="h-10 w-auto" />
|
<Logo className="h-10 w-auto" />
|
||||||
</Link>
|
</Link>
|
||||||
<div className="hidden md:flex md:gap-x-6">
|
<div className="hidden md:flex md:gap-x-6">
|
||||||
<NavLink href="/#features">{t("navigation.features")}</NavLink>
|
<NavLink href="/pricing">{t("navigation.pricing")}</NavLink>
|
||||||
{/* <NavLink href="/#testimonials">Testimonials</NavLink> */}
|
|
||||||
<NavLink href="/#faqs">{t("navigation.faqs")}</NavLink>
|
<NavLink href="/#faqs">{t("navigation.faqs")}</NavLink>
|
||||||
<NavLink href={linkT("docs.intro")} target="_blank">
|
<NavLink href={linkT("docs.intro")} target="_blank">
|
||||||
{t("navigation.docs")}
|
{t("navigation.docs")}
|
||||||
@@ -164,31 +152,6 @@ export function Header() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-center gap-x-2 md:gap-x-5">
|
<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
|
<Link
|
||||||
className={buttonVariants({
|
className={buttonVariants({
|
||||||
variant: "outline",
|
variant: "outline",
|
||||||
@@ -202,25 +165,14 @@ export function Header() {
|
|||||||
</span>
|
</span>
|
||||||
<HeartIcon className="animate-heartbeat size-4 fill-red-600 text-red-500 " />
|
<HeartIcon className="animate-heartbeat size-4 fill-red-600 text-red-500 " />
|
||||||
</Link>
|
</Link>
|
||||||
<Button
|
<Button className="rounded-xl" asChild>
|
||||||
className="rounded-full bg-[#5965F2] hover:bg-[#4A55E0]"
|
|
||||||
asChild
|
|
||||||
>
|
|
||||||
<Link
|
<Link
|
||||||
href="https://discord.gg/2tBnJ3jDJc"
|
href="https://app.dokploy.com"
|
||||||
aria-label="Dokploy on GitHub"
|
aria-label="Dokploy on GitHub"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
className="flex flex-row items-center gap-2 text-white"
|
// className="flex flex-row items-center gap-2 text-white"
|
||||||
>
|
>
|
||||||
<svg
|
{t("navigation.dashboard")}
|
||||||
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>
|
</Link>
|
||||||
</Button>
|
</Button>
|
||||||
<div className="-mr-1 md:hidden">
|
<div className="-mr-1 md:hidden">
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
"use client";
|
"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 { useTranslations } from "next-intl";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Container } from "./Container";
|
import { Container } from "./Container";
|
||||||
|
import AnimatedShinyText from "./ui/animated-shiny-text";
|
||||||
import { Button } from "./ui/button";
|
import { Button } from "./ui/button";
|
||||||
|
import { HoverBorderGradient } from "./ui/hover-border-gradient";
|
||||||
|
|
||||||
const ProductHunt = () => {
|
const ProductHunt = () => {
|
||||||
return (
|
return (
|
||||||
@@ -69,7 +72,22 @@ export function Hero() {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="relative">
|
<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")}{" "}
|
{t("hero.deploy")}{" "}
|
||||||
<span className="relative whitespace-nowrap text-primary">
|
<span className="relative whitespace-nowrap text-primary">
|
||||||
<svg
|
<svg
|
||||||
@@ -125,7 +143,9 @@ export function Hero() {
|
|||||||
Discord
|
Discord
|
||||||
</Link>
|
</Link>
|
||||||
</Button> */}
|
</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
|
<Link
|
||||||
href="https://github.com/dokploy/dokploy"
|
href="https://github.com/dokploy/dokploy"
|
||||||
aria-label="Dokploy on GitHub"
|
aria-label="Dokploy on GitHub"
|
||||||
@@ -138,6 +158,27 @@ export function Hero() {
|
|||||||
Github
|
Github
|
||||||
</Link>
|
</Link>
|
||||||
</Button>
|
</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>
|
</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">
|
<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 = () => {
|
export const ShowSponsors = () => {
|
||||||
|
const t = useTranslations("HomePage");
|
||||||
return (
|
return (
|
||||||
<div className="mt-20 flex flex-col justify-center gap-y-10">
|
<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">
|
<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>
|
</h1>
|
||||||
<p className="mx-auto max-w-2xl text-lg tracking-tight text-muted-foreground">
|
<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
|
{t("hero.sponsors.description")}
|
||||||
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.
|
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-4 md:gap-6 justify-start">
|
<div className="flex flex-col items-center justify-start gap-4 md:gap-6">
|
||||||
<h2 className="font-display text-2xl font-medium tracking-tight text-primary sm:text-2xl text-left">
|
<h2 className="text-left font-display text-2xl font-medium tracking-tight text-primary sm:text-2xl">
|
||||||
Hero Sponsors 🎖
|
{t("hero.sponsors.level.hero")} 🎖
|
||||||
</h2>
|
</h2>
|
||||||
<div className="flex flex-wrap items-center gap-4">
|
<div className="flex flex-wrap items-center gap-4">
|
||||||
<a
|
<a
|
||||||
@@ -209,7 +248,7 @@ export const ShowSponsors = () => {
|
|||||||
<img
|
<img
|
||||||
src="https://raw.githubusercontent.com/Dokploy/dokploy/canary/.github/sponsors/hostinger.jpg"
|
src="https://raw.githubusercontent.com/Dokploy/dokploy/canary/.github/sponsors/hostinger.jpg"
|
||||||
alt="hostinger.com"
|
alt="hostinger.com"
|
||||||
className="rounded-xl w-[190px] h-auto"
|
className="h-auto w-[190px] rounded-xl"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
@@ -221,16 +260,16 @@ export const ShowSponsors = () => {
|
|||||||
<img
|
<img
|
||||||
src="https://raw.githubusercontent.com/Dokploy/dokploy/canary/.github/sponsors/lxaer.png"
|
src="https://raw.githubusercontent.com/Dokploy/dokploy/canary/.github/sponsors/lxaer.png"
|
||||||
alt="lxaer.com"
|
alt="lxaer.com"
|
||||||
className="rounded-xl w-[70px] h-auto"
|
className="h-auto w-[70px] rounded-xl"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-4 md:gap-8 justify-start">
|
<div className="flex flex-col items-center justify-start gap-4 md:gap-8">
|
||||||
<h2 className="font-display text-2xl font-medium tracking-tight text-primary sm:text-2xl text-left">
|
<h2 className="text-left font-display text-2xl font-medium tracking-tight text-primary sm:text-2xl">
|
||||||
Premium Supporters 🥇
|
{t("hero.sponsors.level.premium")} 🥇
|
||||||
</h2>
|
</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
|
<a
|
||||||
href="https://supafort.com/?ref=dokploy"
|
href="https://supafort.com/?ref=dokploy"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -245,9 +284,9 @@ export const ShowSponsors = () => {
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-4 md:gap-8 justify-start">
|
<div className="flex flex-col items-center justify-start gap-4 md:gap-8">
|
||||||
<h2 className="font-display text-2xl font-medium tracking-tight text-primary sm:text-2xl text-left">
|
<h2 className="text-left font-display text-2xl font-medium tracking-tight text-primary sm:text-2xl">
|
||||||
Supporting Members 🥉
|
{t("hero.sponsors.level.supporting")} 🥉
|
||||||
</h2>
|
</h2>
|
||||||
<div className="flex flex-row gap-10">
|
<div className="flex flex-row gap-10">
|
||||||
<a
|
<a
|
||||||
@@ -276,9 +315,9 @@ export const ShowSponsors = () => {
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-4 md:gap-8 justify-start">
|
<div className="justify-star flex flex-col items-center gap-4 md:gap-8">
|
||||||
<h2 className="font-display text-2xl font-medium tracking-tight text-primary sm:text-2xl text-left">
|
<h2 className="text-left font-display text-2xl font-medium tracking-tight text-primary sm:text-2xl">
|
||||||
Community Backers 🤝
|
{t("hero.sponsors.level.community")} 🤝
|
||||||
</h2>
|
</h2>
|
||||||
<div className="flex flex-row gap-10">
|
<div className="flex flex-row gap-10">
|
||||||
<a
|
<a
|
||||||
@@ -305,11 +344,24 @@ export const ShowSponsors = () => {
|
|||||||
alt="Rivo.gg"
|
alt="Rivo.gg"
|
||||||
/>
|
/>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
|
<a
|
||||||
|
href="https://photoquest.wedding/?ref=dokploy"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
<img
|
||||||
|
src="https://photoquest.wedding/favicon/android-chrome-512x512.png"
|
||||||
|
className="rounded-xl"
|
||||||
|
width="60px"
|
||||||
|
alt="Rivo.gg"
|
||||||
|
/>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-4 md:gap-8 justify-start">
|
<div className="flex flex-col items-center justify-start gap-4 md:gap-8">
|
||||||
<h2 className="font-display text-2xl font-medium tracking-tight text-primary sm:text-2xl text-left">
|
<h2 className="text-left font-display text-2xl font-medium tracking-tight text-primary sm:text-2xl">
|
||||||
Organizations:
|
{t("hero.sponsors.level.organizations")}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="flex flex-row gap-10">
|
<div className="flex flex-row gap-10">
|
||||||
<a
|
<a
|
||||||
@@ -324,9 +376,9 @@ export const ShowSponsors = () => {
|
|||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col gap-4 md:gap-8 justify-start">
|
<div className="flex flex-col items-center justify-start gap-4 md:gap-8">
|
||||||
<h2 className="font-display text-2xl font-medium tracking-tight text-primary sm:text-2xl text-left">
|
<h2 className="text-left font-display text-2xl font-medium tracking-tight text-primary sm:text-2xl">
|
||||||
Individuals:
|
{t("hero.sponsors.level.individuals")}
|
||||||
</h2>
|
</h2>
|
||||||
<div className="flex flex-row gap-10">
|
<div className="flex flex-row gap-10">
|
||||||
<a
|
<a
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import { Link } from "@/i18n/routing";
|
import { Link } from "@/i18n/routing";
|
||||||
import { useTranslations } from "next-intl";
|
import { useTranslations } from "next-intl";
|
||||||
import { Footer } from "./Footer";
|
|
||||||
import { Header } from "./Header";
|
|
||||||
|
|
||||||
export function SlimLayout() {
|
export function SlimLayout() {
|
||||||
const t = useTranslations("404");
|
const t = useTranslations("404");
|
||||||
@@ -16,6 +14,7 @@ export function SlimLayout() {
|
|||||||
<Link href="/" className="text-primary">
|
<Link href="/" className="text-primary">
|
||||||
{t("action")}
|
{t("action")}
|
||||||
</Link>
|
</Link>
|
||||||
|
p{" "}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
|
|||||||
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
Reference in New Issue
Block a user